TL;DR
- I wanted a tray app that catches "did you clock out?" the instant you shut down.
- The by-the-book approach — a service handling
SERVICE_CONTROL_PRESHUTDOWN— silently fails when Fast Startup (HiberbootEnabled) is on, because "shutdown" is really a hybrid hibernate. - Hijacking
shutdown.exevia IFEO works, but it's unshippable (admin rights, AV reputation, OS-update fragility, incomplete coverage). - Conclusion: modern Windows shutdown can't be reliably blocked — and shouldn't be.
- So I flipped it: instead of blocking shutdown, interrupt the moment the user reaches for it. UI Automation watches the Start-menu power flyout and nudges just before the click.
1. The itch: "Did you clock in?" at the exact moment you leave
It started with forgetting to clock out. The moment you power off at the end of the day, a one-line "did you clock in?" would prevent a real mistake. My first idea was simple: detect shutdown, pause it, show a confirm dialog, proceed on OK. That was the start of a thorny road.
2. Attempt 1: a service that receives PRESHUTDOWN
The by-the-book way to intercept shutdown on Windows is a service receiving SERVICE_CONTROL_PRESHUTDOWN. Unlike WM_QUERYENDSESSION, it arrives before other processes are killed, and you can extend the grace period.
SERVICE_ACCEPT_PRESHUTDOWN = 0x00000100
SERVICE_CONTROL_PRESHUTDOWN = 0x0000000F
class ShutdownBlockerService(win32serviceutil.ServiceFramework):
def GetAcceptedControls(self):
return (win32service.SERVICE_ACCEPT_STOP |
win32service.SERVICE_ACCEPT_SHUTDOWN |
SERVICE_ACCEPT_PRESHUTDOWN)
It worked locally. Then on most real machines, the event never fired.
Why: Fast Startup eats PRESHUTDOWN
Windows 10/11 default to Fast Startup, where "shutdown" is actually logoff + hybrid hibernate — not a true shutdown — so PRESHUTDOWN may never be delivered. The workaround is to disable Fast Startup via the registry:
# HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Power
# HiberbootEnabled = 0
But that means silently rewriting the user's power settings, plus a system service, admin rights, and a boot-time registry change. For an app you hand to ordinary users, that's absurd.
Lesson: On Windows, "shutdown" under Fast Startup is closer to hibernate. If you rely on
PRESHUTDOWN, suspectHiberbootEnabledfirst.
3. Attempt 2: hijack shutdown.exe with IFEO
"If a service won't work, take over the entrance — shutdown.exe." Image File Execution Options lets you set a Debugger value so your program launches instead of the target.
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\
shutdown.exe\Debugger = "C:\...\ShutdownInterceptor.exe"
It works. But:
- It only covers one of many shutdown paths (Start menu power button,
Alt+F4, directExitWindowsEx…). - Needs admin (HKLM).
- AV flags it — hijacking a system EXE via IFEO is indistinguishable from malware.
- Breaks on OS updates; uninstall leftovers are scary.
Not shippable. I rethought the whole thing.
4. The pivot: stop blocking, start interrupting
The requirement was never "block shutdown" — it was "say one thing before the PC goes off." Reliably preventing shutdown (Fast Startup, many entrances, privileges, AV) is never worth it.
So invert it: the moment the user opens the Start-menu power flyout, interrupt from my side and show the reminder. Don't stop it — just make them notice.
Windows 11's Start menu is a separate process (StartMenuExperienceHost.exe), observable via UI Automation. I poll lightly for the "Shut down" item becoming visible (or the cursor nearing "Power"):
import uiautomation as auto
auto.SetGlobalSearchTimeout(0) # critical: disable blocking search
desk = auto.GetRootControl()
SD_NAMES = ('Shut down', 'Shutdown', 'シャットダウン')
POWER_NAMES = ('Power', '電源')
while True:
found = False
for name in SD_NAMES:
sd = desk.Control(Name=name, searchDepth=6)
if sd.Exists(maxSearchSeconds=0) and not sd.IsOffscreen:
r = sd.BoundingRectangle
if r.width() > 0 and r.height() > 0:
found = True
break
if found:
# show a small card in the lower-right
...
time.sleep(0.10) # 100ms poll
Without SetGlobalSearchTimeout(0), every miss waits out the default timeout and the poll stalls — a real gotcha I hit in practice.
The result: "just before you click the power button, a quiet card appears in the lower-right." Working with the OS flow instead of fighting it — and far more stable.
5. Constraints decided the design: no service, no admin, clean uninstall
The pivot was also reverse-engineered from distribution requirements. To ship broadly (and on the Microsoft Store), three things matter:
- No system service (kills attempt 1)
- No admin / no registry hacks (kills attempts 1 & 2)
- Clean uninstall (AV reputation, Store policy)
"Interrupt" satisfies all three naturally. I also generalized the triggers beyond shutdown: startup, set times (daily/weekly/monthly/Nth weekday), and shutdown/sign-out (where I use WM_QUERYENDSESSION only as a display trigger, never to block). The code shed its service and IFEO and collapsed into a single tkinter tray process, shipped as one PyInstaller EXE writing only to %APPDATA%.
6. Takeaways
- Modern Windows shutdown can't be reliably blocked. Fast Startup, many entrances, privileges, AV — all against you.
- Using
PRESHUTDOWN? SuspectHiberbootEnabledfirst. - IFEO system-EXE hijacking "works but can't ship" (AV / admin / update fragility / uninstall).
- Re-reading the requirement from "block" to "interrupt (make them notice)" lands on a gentle UI-Automation solution that also fits distribution constraints (no service, no admin, clean).
Fighting the OS head-on is brittle. Riding its flow and gently tapping the user on the shoulder turned out to be both more robust and far easier to ship.
I build LastCall solo — a Windows tray reminder (free; optional paid Microsoft 365 add-on).
Try it (free):
- Microsoft Store: https://apps.microsoft.com/detail/9NZW2F210FX7
- Site: https://lastcall.ryusudo.com/en
Happy to answer anything about the UI Automation polling or packaging a Python tray app for the Store.
Top comments (0)