DEV Community

loading...

Layer1

jeikabu profile image jeikabu Originally published at rendered-obsolete.github.io on ・4 min read

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:

  1. Detecting that the user session is coming to an end
  2. 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.

Discussion

pic
Editor guide