How to synchronize screen lock between Windows and Linux

So, you have two computers and want to be able to lock/unlock only one of the two connected systems, Windows or Linux, but have the other systems automatically lock/unlock. Quite possible, if you do it the right way.

Locking a Windows box remotely is not hard (see below). Unlocking a Windows box remotely, on the other hand, is near damn impossible. It requires system dll hacking with undocumented API's and will probably set your antivirus/firewall ablaze. It can be done though and I'll give you a hint.

There is a better solution though. Go the other way, lock/unlock your Windows box first and then have the Linux box locked/unlocked automatically, remotely, via some scripting. Here is what you'll need to do:

  1. Install a service on the windows box that monitors its locked/unlocked state and sends a lock/unlock command to the Linux box
  2. Set up an SSH tunnel or reuse one if you have it set up with synergy. You are not sending all your keystrokes in clear text over the network, are you?.
  3. Set your Linux box to never to shut off the display and to simulate user activity, so it does not lock by itself. You could just disable the screensaver, but that means you have to keep remembering to reenable it when not synching lock/unlock.
  4. Run a service on the Linux box that listens for lock/unlock events from Windows and locks/unlocks your Linux accordingly.

Here are all the components, coded in python and bash. They are written for KDE but you could substitute kscreenlocker with the Gnome alternative and the proper qbus message to get it going on GLIB.

Script to monitor windows lock/unlock status

  • ISensLogon interface version that can handle many types of session events
  • HandlerEx can not handle screen saver events

Both are showcased in the following code. To use it, get a python for windows and install the service by running

python windows_lock_monitor.py --interactive --startup=auto install

Use ActivePython to get all the libraries required.

''' Windows lock/unlock and screen saver monitor.
Contains both the ISensLogon Interface and the HandlerEx Callbacks (currently obsoleted since they do not give screensaver status)

use the following commands to run/modify this script as a windows service:
python $0 --interactive --startup=auto install
python $0 update
python $0 remove
'''
import pythoncom
import win32serviceutil, win32service, win32event, win32com.server.policy, win32com.client, win32api
import servicemanager
import socket
import sys

## from Sens.h
SENSGUID_PUBLISHER = "{5fee1bd6-5b9b-11d1-8dd2-00aa004abd5e}"
SENSGUID_EVENTCLASS_LOGON = "{d5978630-5b9f-11d1-8dd2-00aa004abd5e}"
## from EventSys.h
PROGID_EventSystem = "EventSystem.EventSystem"
PROGID_EventSubscription = "EventSystem.EventSubscription"
IID_ISensLogon = "{d597bab3-5b9f-11d1-8dd2-00aa004abd5e}"

HOST, PORT = "localhost", 24809

class SensLogon(win32com.server.policy.DesignatedWrapPolicy):
    _com_interfaces_=[IID_ISensLogon]
    _public_methods_=['Logon','Logoff','StartShell','DisplayLock','DisplayUnlock','StartScreenSaver','StopScreenSaver']

    def __init__(self):
        self._wrap_(self)

    def DisplayLock(self, *args):
    # workstation locked
        # Create a socket (SOCK_STREAM means a TCP socket) (may be done at the start of the service too, but it is more reliable here)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Connect to server and send data
        sock.connect((HOST, PORT))
        sock.send("knock-lock$\n") # security by obscurity. Not good but good enough over SSH with the remote port forwarding
        # Receive data from the server and shut down
        received = sock.recv(1024)
        sock.close()
        logevent('Workstation locked : %s' % args)

    def DisplayUnlock(self, *args):
    # workstation unlocked
        # Create a socket (SOCK_STREAM means a TCP socket) (may be done at the start of the service too, but it is more reliable here)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Connect to server and send data
        sock.connect((HOST, PORT))
        sock.send("knock-unlock#\n")
        # Receive data from the server and shut down
        received = sock.recv(1024)
        sock.close()
        logevent('Workstation unlocked : %s' % args)

    # shortcircuit the other events
    def StartScreenSaver(self, *args):
        logevent('Workstation screensaver started : %s' % args)
    self.DisplayLock(args)

    def StopScreenSaver(self, *args):
        logevent('Workstation screensaver stopped : %s' % args)
    self.DisplayUnlock(args)

    def Logon(self, *args):
        logevent('Workstation log on : %s' % args)
    self.DisplayUnlock(args)

    def Logoff(self, *args):
        logevent('Workstation log off : %s' % args)
    self.DisplayLock(args)

    def StartShell(self, *args):
        logevent('*Workstation shell started : %s' % args)

def logevent(msg, evtid=0xF000):
    servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,evtid,(msg, ''))

# main service handler:
class StateSyncSvc(win32serviceutil.ServiceFramework): # main service for the
    _svc_display_name_ = _svc_name_ = "Workstation Lock Monitor"
    _svc_description_ = "Monitors lock/unlock events on a synergy keyboard server and synchronizes with the client"
    _svc_deps_ = ['EventLog','SENS'] # 'System Event Notification' is the SENS service
    isenslogon_thread_id=0

    def __init__(self,args):
        win32serviceutil.ServiceFramework.__init__(self,args)
        #logevent(self._svc_display_name_, servicemanager.PYS_SERVICE_STARTING)
        self.ReportServiceStatus(win32service.SERVICE_START_PENDING, waitHint=30000)
        self.hWaitStop = win32event.CreateEvent(None,0,0,None)

    # Override the base class so we can accept additional events - use this to enable HandlerEx callbacks from the Service Control Manager (SCM) to detect session actions
    #def GetAcceptedControls(self):
    #    return win32serviceutil.ServiceFramework.GetAcceptedControls(self) | win32service.SERVICE_ACCEPT_SESSIONCHANGE

    def SvcStop(self):
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        #win32event.SetEvent(self.hWaitStop) # for HandlerEx
    #win32api.PostQuitMessage() # kills the whole service (no service stopped messages are visible)
    #  or get the thread ID and call
        win32api.PostThreadMessage(self.isenslogon_thread_id, 18)
    self.ReportServiceStatus(win32service.SERVICE_STOPPED)

    def SvcDoRun(self):
        servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,servicemanager.PYS_SERVICE_STARTED,(self._svc_name_,'')) # started
    # create an isens listener
    sl=SensLogon()
    subscription_interface=pythoncom.WrapObject(sl)
    event_system=win32com.client.Dispatch(PROGID_EventSystem)
        event_subscription=win32com.client.Dispatch(PROGID_EventSubscription)
    event_subscription.EventClassID=SENSGUID_EVENTCLASS_LOGON
    event_subscription.PublisherID=SENSGUID_PUBLISHER
    event_subscription.SubscriptionName='Python subscription'
    event_subscription.SubscriberInterface=subscription_interface
    event_system.Store(PROGID_EventSubscription, event_subscription)
    # - wait for a stop event (iSensLogon)
    self.isenslogon_thread_id=win32api.GetCurrentThreadId()
    pythoncom.PumpMessages()
        # - wait for a stop event (for HandlerEx)
    # win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
        # Write a stop message.
        servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,servicemanager.PYS_SERVICE_STOPPED,(self._svc_name_,'')) # stopped
    self.ReportServiceStatus(win32service.SERVICE_STOPPED)

    # HandleEx callback hooks - this call is currently deprecated in favour of SENS
    def SvcOtherEx(self, control, event_type, data):
        if control == win32service.SERVICE_CONTROL_SESSIONCHANGE:
            if event_type == 7:
                # workstation locked
                # Create a socket (SOCK_STREAM means a TCP socket) (may be done at the start of the service too, but it is more reliable here)
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                # Connect to server and send data
                sock.connect((HOST, PORT))
                sock.send("knock-lock$\n")
                # Receive data from the server and shut down
                received = sock.recv(1024)
                sock.close()
                #servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,0xF000,('Workstation locked', received))
            elif event_type == 8:
                # workstation unlocked
                # Create a socket (SOCK_STREAM means a TCP socket) (may be done at the start of the service too, but it is more reliable here)
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                # Connect to server and send data
                sock.connect((HOST, PORT))
                sock.send("knock-unlock#\n")
                # Receive data from the server and shut down
                received = sock.recv(1024)
                sock.close()
                #servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,0xF000,('Workstation unlocked', received))
            else:
                # otherwise just ignore
                return

    # reboot/halt event issues a different call - shortcircuit it to the SvcStop
    SvcShutdown = SvcStop

if __name__ == '__main__':
    win32serviceutil.HandleCommandLine(StateSyncSvc)

How to disable automatic screen blanking and locking on Ubuntu

It sets a background process that you'll have to kill once you are no longer using synergy.

# Tests for X server blanking / Monitor blanking
export DISPLAY=:0
export XAUTHORITY=/home/'''user'''/.Xauthority
xblanktest=$(xset -q | grep timeout | awk '{printf $2}')
dpmstest=$(xset -q | grep "  DPMS is Enabled")

if [[ "$xblanktest" -gt 0 ]] || [[ -n "$dpmstest" ]]; then
  echo Disabling screen blanking, powersaving, and screensaver...
  # Turn off X blanking, Monitor blanking
  xset s off; xset -dpms
  # Supend screensaver
  echo '#!/bin/bash'  >  /tmp/suspend-dbus-screensaver
  echo 'while :'      >> /tmp/suspend-dbus-screensaver
  echo 'do'           >> /tmp/suspend-dbus-screensaver
  echo 'qdbus org.freedesktop.ScreenSaver /ScreenSaver SimulateUserActivity' >> /tmp/suspend-dbus-screensaver
  echo 'sleep 119'    >> /tmp/suspend-dbus-screensaver
  echo 'done'         >> /tmp/suspend-dbus-screensaver
  chmod u+x /tmp/suspend-dbus-screensaver
  nohup "/tmp/suspend-dbus-screensaver" &> /dev/null &
fi

To re-enable screen blanking and DPMS run

 xset s on; xset +dpms

How to Lock/Unlock Linux remotely

This is the final piece of the lock synchronization procedure outlined in the beginning of this document. Make sure to replace both /home/user/ instances in subprocess.Popen with your actual user name.

#!/usr/bin/python
'''
Server portion of a lock/unlock synchronization mechanism.
Watches a message on a 'sync' port and locks/unlocks a screen.
To use this tool first establish an ssh tunnel from the client using -L 24809:localhost:24809 or from the server using the -R 24809:localhost:24809 switch
'''
import subprocess, SocketServer, time
import sys

locker = None # kscreenlocker process

class TCPLocksmith(SocketServer.BaseRequestHandler):
    def handle(self):
        global locker
        if self.client_address[0] != '127.0.0.1': # security precaution
            print "Disallowed a non-local connection from %s. Use an SSH tunnel" % self.client_address[0]
            return
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        # just send back the same data, but upper-cased
        if self.data == 'knock-lock': # security by obscurity, lame but true
            # check first if already locked
            checklock=subprocess.call(["pgrep","-f","kscreenlocker --forcelock"])
            if checklock == 0:
                print "Already locked"
                self.request.send('negative')
                return
            # use display in case the script is running from a non-x environment. running with shell=True without the display variable set results in a screen saver, not screen locker
            locker = subprocess.Popen('export DISPLAY=:0;export XAUTHORITY=/home/user/.Xauthority;/usr/lib/kde4/libexec/kscreenlocker --forcelock',shell=True)#, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            print "Remote screen locked PID(%i)." % (locker.pid)#, stdoutdata, stderrdata)
            sys.stdout.flush()
            self.request.send('affirmative')
        elif self.data == 'knock-unlock':
            if locker is None or locker.poll() is not None: # not locked with this server or locked but the locker of this server is no longer running (i.e it was unlocked and relocked) - security precaution
                print "Not locked with this server. Refusing to unlock"
                self.request.send('negative')
                return
            # piping msgs to log if python is run with a redirect to a file
            unlocker = subprocess.Popen('export DISPLAY=:0;export XAUTHORITY=/home/user/.Xauthority;/usr/bin/qdbus org.kde.screenlocker /MainApplication quit',shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            (stdoutdata,stderrdata)=unlocker.communicate()
            print "Remote screen unlock command submitted PID(%i):\n%s\n%s" % (unlocker.pid, stdoutdata, stderrdata)
            # Check/kill the service from the above lock section if it is defined.
            time.sleep(1)
            if locker.poll() is None:
                print "Screen is still locked. Attempting terminate PID(%i)" % locker.pid
                locker.terminate()
                time.sleep(1)
                if locker.poll() is None:
                    print "Screen is still locked. Killing PID(%i)" % locker.pid
                    #subprocess.call(["pkill","-9","-f","kscreenlocker"])
                    locker.kill()
            sys.stdout.flush()
            self.request.send('affirmative')

if __name__ == "__main__":
    HOST, PORT = "localhost", 24809
    server = SocketServer.TCPServer((HOST, PORT), TCPLocksmith)
    print "Activating the locksmith server on %s(%s); this will keep running until you interrupt the program with Ctrl-C" % (HOST, PORT)
    try:
        server.serve_forever()
    except:
        print "done"

How to synchronize screen lock from Linux to Windows aka how to lock Windows remotely

The lock command itself is trivial:

rundll32.exe user32.dll,LockWorkStation

To run it when linux locks do the following:

  1. Set up an SSH server on windows using cygwin's sshd or similar. Alternatively you can use an ssh client (not ssh server) on windows. To do that set up a tunnel to the Linux box with a remote port forwarding (-R option) on Windows
  2. Run a service on the Linux box that listens for screen lock events and sends a windows lock command.

Here is the service, done in perl for GNOME and KDE. It also showcases the use of multiplexed SSH tunnels and uses SSH identity key for passwordless logon.


#!/usr/bin/perl
# watches dbus for session idle status from the linux screensaver and sends commands to the windows box to act accordingly

# set proper values here:
my $USER=user
my $HOST=windowshost
my $PORT=22

if (`pidof ksmserver`) {
   print "KDE running.";
   $screensaver = 'org.freedesktop.ScreenSaver';
   $busmember = 'ActiveChanged';
} elsif (`pidof gnome-session`) {
   print "GNOME running.";
   $screensaver = 'org.gnome.ScreenSaver';
   #$busmember = 'SessionIdleChanged';
   $busmember = 'ActiveChanged';
} else {
  print "Unknown desktop environment";
  exit 2;
}
my $cmd = "dbus-monitor --session \"type='signal',interface='$screensaver',member='$busmember'\"";
#print $cmd;
#exit 1;

open (IN, "$cmd |");
print "Monitoring dbus\n";
while (<IN>) {
    if (m/^\s+boolean true/) {
        #print "*** Session is idle ***\n";
        #check for the ssh connection first
        if (-e "/home/$USER/.ssh/control_socket") {
            # send the lock screen command
            #system('ssh -p $PORT -i /home/$USER/.ssh/remote_identity USER@HOST \'rundll32.exe user32.dll,LockWorkStation\'');
            # use the muliplexed ssh channel instead of creating a new one. The host name is not really required but the current version of ssh
            # is buggy - it needs something before the remote command
            system('ssh -S /home/$USER/.ssh/ssh_control_socket $HOST \'rundll32.exe user32.dll,LockWorkStation\'');
        }
    } elsif (m/^\s+boolean false/) {
        print "*** Session is no longer idle ***\n";
    }
}

@Microsoft @LinuxandUnix @Tools @HowTo