How to synchronize screen lock between Windows and Linux
So, you want to lock/unlock only one of the two connected systems, Windows or Linux, but have both systems respond appropriately. Quite possible, if you do it the right way.
There is a catch though. Locking a windows box from linux is not hard. Unlocking a windows box remotely, on the other hand, would require some hacking into undocumented API's and would set your windows IDS/Firewalls 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 Ubuntu box locked/unlocked automatically via some scripting. Here is what you'd need to do:
- Install a service on the windows box that monitors it's locked/unlocked state and sends it to the Linux box
- Set up an SSH tunnel or reuse one if you have set up synergy. You are not sending all your keystrokes in clear text over the wire, are you?.
- Set your Linux box to never blank the screen and simulate user activity, so it does not lock by itself.
- 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.
Contents |
[edit] 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)
[edit] 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
[edit] 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"
[edit] How to lock Windows remotely
- Set up an SSH server on windows using cygwin's sshd or similar. Alternatively you can use an ssh client on windows to set up a tunnel to the Linux box with a remote port forwarding (-R option)
- Run a service on the Linux box that listens for lock events and sends a windows lock command. The lock command itself is trivial:
rundll32.exe user32.dll,LockWorkStation
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. Download
#!/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/pge_control_socket") { # send the lock screen command #system('ssh -p $PORT -i /home/alex/.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"; } }