Building a custom solution using Windows 10 and Azure to track when I’m working from home

Photo by @fredmarriage / Unsplash.com

I browsed back through my calendar, that the last time I went physically to work at the local Microsoft office was about 170 days ago. Even before this new normal, where everyone works from home, I often visited the office perhaps once a week, for a day at a time. The commute is about 45 minutes each way, and I’ve always wanted to prioritize those precious minutes for something that I actually enjoy.

Ending my vacation recently, and turning into work mode again, I realized it would be both interesting and fun to track when I’m working, and when I’m not working. Obviously I’m aiming to fill those 40-or-so hours each week, but it’s becoming more challenging to monitor. Am I working when I’m having lunch at home, and at the same time folding away my laundry? What about when I’m sitting in a comfy chair on the balcony and catching up on emails? What then, when I go out for a walk by the sea and listen to a conference call? I feel I’m clear on these, but I wanted to build a solution to monitor this further.

Coming up with the idea

In recent months I’ve built many solutions. A custom URL shortener, a command-line tool for Microsoft To-Do, my first custom native Android app, and plenty of others. Turns out, when you fix your mind on getting something done, it becomes easier the more you do it.

I needed a way to automatically measure when I’m on my main computer. Usually, if not always, this tells me that I’m working. Sometimes I fiddle around other stuff, but for that, I mostly use a laptop – or just my phone. To roughly monitor this, I figured I’d need to check when I’m logged in and when my Windows 10 is not locked. I have a built-in habit to hit <Win> + L if I leave the computer.

To track this, I need an API in the cloud. This is the easy bit. Thus, a vague and simple approach is that when I lock my Windows 10, I’ll automatically call a custom API and trigger a status change in the cloud. This status change can then do whatever I need – turn off the lights in my home office, warm up the car, or something similar. As I control the API endpoint, as long as I can reach my other devices I’m good.

Building the monitoring tool for lock/unlock states

Using C#, which is often my happy place, I set out to build the first bit – figuring out when my Windows 10 is locked or unlocked.

Side note: Why didn’t I just build a simple script and schedule that with Windows Task Scheduler? I did try that, too, but it was unreliable and often laggy. At times it wouldn’t trigger at all, and other times it triggered after several minutes.

Windows 10 runs a binary, LockApp.exe (see some details here), in the User-Space. This app is responsible for providing you with the lock screen, and showing notifications items at the same time. By default, when you are logged in (thus, Windows 10 is unlocked), LockApp.exe – the process – is disabled.

Once you lock your Windows user session (with the aforementioned <Win> + L, or Ctrl+Alt+Del > Lock, or clicking your profile in Start Menu and selecting Lock), LockApp.exe is resumed automatically.

The thread state of the LockApp.exe process changes. We’re interested when it changes to UserRequest, as that implies the LockApp (in charge of the lock screen in Windows 10) is waiting for a user request. There are numerous other states as well, and you can easily inspect these by checking the ThreadWaitReason of a given thread.

I wrote a quick C# command-line utility to check for the state change of the LockApp.exe process:

Process[] processCollection = Process.GetProcessesByName("lockapp");
foreach (Process p in processCollection)
{
	lockAppProc = p;
}

ProcessThread thread = lockAppProc.Threads[0];

if (thread.ThreadState == System.Diagnostics.ThreadState.Wait)
{
   //Suspended == User is logged in, LockApp is suspended
    if (thread.WaitReason == ThreadWaitReason.Suspended)
    // update monitoring that we are working from home
}
else
{
   // We can safely assume we're not working as the process is not suspended
   // update monitoring that we are NOT working from home
}

This worked surprisingly well. The reason to first check for ThreadState, and only then checking for the WaitReason is mentioned in the remarks here:

“The WaitReason property is valid only when the ThreadState is Wait. Therefore, check the ThreadState value before you get the WaitReason property.”

I ran my command-line tool in a never-ending while() loop. It worked, for about 5 minutes!

The issues with monitoring state of a given process indefinitely

I spent an embarrassingly long time debugging the issues. First, the command-line tool ran neatly – but at some point, it abruptly decided that I am back in work mode! Even if my Windows 10 remained locked, I had no remote connectivity running, and I wasn’t even at home. It would simply say that the LockApp had suspended (meaning, that it’s no longer waiting for a UserRequest), and that must mean I’m logged in and have an active Windows logon session. I suspected this was related to some background task or activity Windows 10 performs after a certain idle period.

The other issue was that sometimes it failed after exactly 60 seconds, and sometimes it failed after a longer period of time. Sometimes it failed after 5 minutes, other times it ran for an hour or two, and then failed.

The 60-second trigger was easy to debug. I first checked that my PC does not power down, hibernate, or switch to a sleeping state that messes with my plans. Turns out, the PC did not do an S1, S2, S3, or S4 sleeping state but rather simply turned off the monitor.

In Windows 10 registry, you can the value of the Attributes key from 1 to 2 under Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Power\PowerSettings\7516b95f-f776-4464-8c53-06167f40cc99\8EC4B3A5-6868-48c2-BE75-4F3044BE88A7. This then allows you to use the Power Options Control Panel applet and get a new option to configure:

The Console lock display off timeout is now visible and allows you to specify just when the console display is turned off. By default, I think, this is 60 seconds. I changed it to 0 to disable it. The downside to this is that now my displays rarely turn off anymore.

The other issue was peskier. At times, Windows decided to run something in the background, and this changes the LockApp process thread state to Suspended, even if briefly. There doesn’t seem to be a clear way to make the distinction, so it effectively rendered my hastily-written command-line tool pretty useless. I was also eager to get this working, so I did not delve any deeper with ProcMon or similar to figure out what was happening.

Back to the drawing board: Building the API first

I was a bit defeated with this, as initially, I felt the solution was elegant enough to ‘just work’. I then set my sights on building the API, and resume troubleshooting of the issue of trapping the events properly.

I built a fairly simple Logic Apps that act as the API. It triggers through an HTTP POST, and the payload includes a simple status message in JSON:

Next, I’ll parse the JSON to create a few handy variables – the REMOTEWORKSTATUS, implying if I’m working (or not working), and a bit of HTML snippets to render my status message for the web.

Why am I injecting HTML through a variable, you might ask? Turns out, that when you host your website (that shows my status) in a Static Website in Azure Storage, there are issues in setting a proper Content-Type for the .html file blob. It always rendered as a plain text file. By setting the HTML content in a variable, I was able to define it properly and in a reliable way.

Next, I’m checking what the status is. If the status is working, I’m obviously working from home. If it’s not working, I’m not working. A simple check:

The actions there populate index.html in the Azure Storage container $web, which is created for you once you enable the Static Website option. It looks like this in Azure Portal:

Finally, I’ll respond to the original HTTP POST request with a response of HTTP/200 OK:

Trying out the API with PowerShell using Invoke-RestMethod is a handy way to test that everything works:

Opening the website that points to the Azure Storage container shows me that index.html has been updated. I set the address to wfh.jussiroine.com. I might not keep this live for too long, but for now, it’s a fun little way to show when I’m working.

(You’ll note that it only works over HTTP – one of the limitations in Azure Storage-hosted static web sites is that you’ll need to use Azure CDN to provide HTTPS support)

Back to troubleshooting the monitoring tool for lock/unlock events

I had a short break and resumed my troubleshooting efforts for the command-line tool. I spent about an hour inspecting the different thread states, but couldn’t isolate the issues.

I then went back to the imaginary drawing board and commented out all of my code. I figured that instead of relying on a separate process and it’s thread state, perhaps I could just try and trap the actual event that Windows passes to LockApp? This way, I wouldn’t have to keep polling for LockApp process thread states constantly, but could simply sit in the background and wait for an event to occur.

I found SystemEvents.SessionSwitch event, it’s part of Microsoft.Win32 namespace. This event occurs when the currently logged-in user changes. There are other events, such as SystemEvents.SessionEnded, but as I rarely log out, the SessionSwitch is the right one for me.

Wiring up to listen to this event is very simple. In my Main() method I simply need to add a new delegate for the event:

SystemEvents.SessionSwitch += new SessionSwitchEventHandler(SystemEvents_SessionSwitch);

And then I’ll need to build the event handler:

static void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
{
	if (e.Reason == SessionSwitchReason.SessionLock)
	{
		Console.WriteLine("Locked");
		CallRemoteWorkAPI("not working");
	}
	else if (e.Reason == SessionSwitchReason.SessionUnlock)
	{
		Console.WriteLine("Unlocked");
		CallRemoteWorkAPI("working");
	}
}

When the session change reason is SessionLock, I can safely assume that I’m not working anymore. If it’s SessionUnlock, I’m back to working mode. You can see all the different reasons here.

For each event that occurs, I’m calling CallRemoteWorkAPI(), which is a simple method to call my Logic Apps API:

static async void CallRemoteWorkAPI(string message)
{
    HttpClient client = new HttpClient();

    string API_URL = "https://URL-TO-LOGIC-APP";

    string status = "{ 'status': '" + message + "' }";

    var content = new StringContent(status.ToString(), Encoding.UTF8, "application/json");

    var result = await client.PostAsync(API_URL, content);

    if (result.StatusCode != System.Net.HttpStatusCode.OK)
    {
        Console.WriteLine("Fatal error: {0}", result.ReasonPhrase);
    }

}

For clarity, the complete working implementation is as follows:

using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Text;
using System.Threading;

namespace GetLockAppStatus
{
    class Program
    {
        static void Main(string[] args)
        {
            CallRemoteWorkAPI("working"); 
            SystemEvents.SessionSwitch += new SessionSwitchEventHandler(SystemEvents_SessionSwitch);

            while (true)
            {
                Console.Write(".");
                Thread.Sleep(60000);
            }
        }

static void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
{
    if (e.Reason == SessionSwitchReason.SessionLock)
    {
        Console.WriteLine("Locked");
        CallRemoteWorkAPI("not working");
    }
    else if (e.Reason == SessionSwitchReason.SessionUnlock)
    {
        Console.WriteLine("Unlocked");
        CallRemoteWorkAPI("working");
    }
}

static async void CallRemoteWorkAPI(string message)
{
    HttpClient client = new HttpClient();

    string API_URL = "https://URL-TO-LOGIC-APP";
    string status = "{ 'status': '" + message + "' }";

    var content = new StringContent(status.ToString(), Encoding.UTF8, "application/json");

    var result = await client.PostAsync(API_URL, content);

    if (result.StatusCode != System.Net.HttpStatusCode.OK)
    {
        Console.WriteLine("Fatal error: {0}", result.ReasonPhrase);
    }

}
}
}

I’m polling for the state once per minute:

In conclusion

The solution is fairly elegant – a simple .EXE file, that consumes only about 17 MB of RAM when idling in the background. For comparison, Your Phone background services consume 1.5 GB when idle:

The Logic App takes about 500-900 milliseconds to run through, so it’s blazingly fast. Once I unlock my computer the status changes immediately in practice. As Logic Apps pricing is relatively lax, during my furious testing period over the past few days I’ve accumulated about 6 cents in total cost:

I’m happy now that I’ve found an easy way to trap events in Windows using C#, and this opens up all sorts of interesting possibilities to build out.

In addition, I can now also re-configure my displays to turn off in a timely fashion, so I’m back to the original state and only need to have one .EXE running:

At a later point I might re-purpose this to a native Windows NT Service to make it more elegant.