I’ve covered “layer0”, a Windows service that among other things spawns and monitors a process as the logged in user- “layer1”. It only makes sense to talk about layer1.
Session Events
We want to detect when the user is logging out so we can have layer1 exit cleanly and layer0 not try to restart it. There’s two parts to this:
- Detecting that the user session is coming to an end
- Returning an exit code for
GetExitCodeProcess()
static L1ExitCode ExitCode = L1ExitCode.RestartLayer1;
static int Main(string[] args)
{
try
{
// These events only work if we have a Windows message pump
Microsoft.Win32.SystemEvents.SessionEnding += SessionEnding;
Microsoft.Win32.SystemEvents.SessionEnded += SessionEnded;
Microsoft.Win32.SystemEvents.SessionSwitch += SessionSwitch;
RunLayer1();
}
finally
{
Microsoft.Win32.SystemEvents.SessionEnding -= SessionEnding;
Microsoft.Win32.SystemEvents.SessionEnded -= SessionEnded;
Microsoft.Win32.SystemEvents.SessionSwitch -= SessionSwitch;
}
return (int)ExitCode;
}
static void SessionEnding(object sender, Microsoft.Win32.SessionEndingEventArgs args) => sessionEvent(SessionEvent.SessionEnding);
static void SessionEnded(object sender, Microsoft.Win32.SessionEndedEventArgs args) => sessionEvent(SessionEvent.SessionEnded);
static void SessionSwitch(object sender, Microsoft.Win32.SessionSwitchEventArgs args) => sessionEvent(SessionEvent.SessionSwitch);
static void sessionEvent(SessionEvent sessionEvent)
{
switch(sessionEvent)
{
case SessionEvent.SessionEnded:
case SessionEvent.SessionEnding:
ExitCode = L1ExitCode.UserLoggingOut;
// Trigger layer1 shutdown
break;
case SessionEvent.SessionSwitch:
default:
// Do nothing
break;
}
}
When a user logs out, first SessionEvent.SessionEnding
occurs followed shortly by SessionEvent.SessionEnded
.
The docs for Microsoft.Win32.SystemEvents
state the events we’re interested in are only raised if a message pump is running. It just so happens that layer1 has to have a message loop.
Message Loop
One of the oddities of our platform is that the EC driver delivers power button presses as low-level keyboard events.
Install hook and start message loop:
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wp, IntPtr lp);
LowLevelKeyboardProc lpfn;
IntPtr hHook;
bool setHWInterface2()
{
cts = new CancellationTokenSource();
keyboardHookProcTask = Task.Factory.StartNew((_) =>
{
// Need to create delegate that won't get GC'd
lpfn = new LowLevelKeyboardProc(KeyboardHookProc);
var moduleHandle = GetModuleHandle(null);
//using (var curModule = System.Diagnostics.Process.GetCurrentProcess().MainModule)
//{
// var moduleHandle = GetModuleHandle(curModule.ModuleName);
// // SetWindowsHookEx()
//}
hHook = SetWindowsHookEx(WH_KEYBOARD_LL, lpfn, moduleHandle, 0);
if (hHook == IntPtr.Zero)
{
Log("SetWindowsHookEx failed", LogLevel.Error);
}
else
{
// SetWindowsHookEx requires a Windows message loop on the thread
winMsgLoopAppCtx = new ApplicationContext();
Application.Run(winMsgLoopAppCtx);
// Using Application.DoEvents() seems cleaner, but hook function wasn't getting called
//while (!cts.IsCancellationRequested)
//{
// Application.DoEvents();
// // Sleep long enough this thread rarely runs, but short enough the button will be responsive
// await Task.Delay(250, cts.Token);
//}
if (!UnhookWindowsHookEx(hHook))
Log("UnhookWindowsHookEx failed", LogLevel.Warn);
}
}, cts.Token, TaskCreationOptions.LongRunning);
return true;
}
lpfn = new LowLevelKeyboardProc(KeyboardHookProc)
to keep a reference to the delegate is interesting because without it you can end up with the runtime exception: A callback was made on a garbage collected delegate
.
Use SetWindowsHookEx to register a callback when WH_KEYBOARD_LL
event occurs.
We create and pass an ApplicationContext to Application.Run() to create our message loop. To later stop it:
winMsgLoopAppCtx.ExitThread();
It might also be possible to use Application.DoEvents()
, but we weren’t able to get that working.
Our hook callback is based off pinvoke.net sample:
IntPtr KeyboardHookProc(int code, IntPtr wParam, IntPtr lParam)
{
if (code >= 0)
{
var kbScan = (KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(KBDLLHOOKSTRUCT));
// Filter spurious power button events, play audio feedback via PC speaker (also in EC driver), etc.
}
// Ensure other applications that have installed hooks receive hook notifications
return CallNextHookEx(hHook, code, wParam, lParam);
}
There’s additional details in LowLevelKeyboardProc.
Big Red Button
During development both layer0 and layer1 are usually running as console applications. Because layer0 will restart layer1, we originally had to stop layer0 then stop layer1. This was pretty annoying so we changed it so both close if either windows is closed:
static OS.Pinvoke.ConsoleCtrlDelegate ConsoleCtrlDelegate;
public static void StartLayerN(ILayerN layer)
{
// If user hits ctrl-c or closes console window, shut everything down
ConsoleCtrlDelegate += ctrlTypes =>
{
switch (ctrlTypes)
{
case OS.Pinvoke.CtrlTypes.CTRL_C_EVENT:
case OS.Pinvoke.CtrlTypes.CTRL_CLOSE_EVENT:
// Tell layer0 and layer1 to exit
// Need to wait here otherwise OS terminates the process (without waiting for layer1, etc.)
layer.Wait();
// We handled the signal and thus return true as per https://docs.microsoft.com/en-us/windows/console/handlerroutine
return true;
}
return false;
};
OS.Pinvoke.SetConsoleCtrlHandler(ConsoleCtrlDelegate, true);
//...
}
Wrapping Up
This project has given us many opportunities to wade through the thicket of Windows APIs. I can’t say it’s all been enjoyable, but it’s certainly been educational.
Will probably do a few more posts in the future- there’s just so many amusing (and convoluted) things you can do on Windows. But, I think I’ve done enough for the time being.
Top comments (0)