DEV Community

LastCall Dev
LastCall Dev

Posted on

I couldn't block Windows shutdown — so I bet on "the moment before the power button"

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.exe via 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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, suspect HiberbootEnabled first.

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"
Enter fullscreen mode Exit fullscreen mode

It works. But:

  • It only covers one of many shutdown paths (Start menu power button, Alt+F4, direct ExitWindowsEx…).
  • 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
Enter fullscreen mode Exit fullscreen mode

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? Suspect HiberbootEnabled first.
  • 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):

Happy to answer anything about the UI Automation polling or packaging a Python tray app for the Store.

Top comments (0)