How to synchronize screen lock between Windows and Linux

From KnowWiki
Jump to: navigation, search

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:

  1. Install a service on the windows box that monitors it's locked/unlocked state and sends it to the Linux box
  2. 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?.
  3. Set your Linux box to never blank the screen and simulate user activity, so it does not lock by itself.
  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.

Contents

[edit] Script to monitor windows lock/unlock status

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.

Download

''' 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.

Download

# 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.

Download

#!/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

  1. 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)
  2. 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";
    }
}
Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox