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:
- Install a service on the windows box that monitors its locked/unlocked state and sends a lock/unlock command to the Linux box
- 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?.
- 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.
- 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:
- 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
- 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