<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: LastCall Dev</title>
    <description>The latest articles on DEV Community by LastCall Dev (@lastcall_dev).</description>
    <link>https://dev.to/lastcall_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4176164%2Ff8c6e66a-2f80-432c-a86f-d13a53b7067d.png</url>
      <title>DEV Community: LastCall Dev</title>
      <link>https://dev.to/lastcall_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lastcall_dev"/>
    <language>en</language>
    <item>
      <title>I couldn't block Windows shutdown — so I bet on "the moment before the power button"</title>
      <dc:creator>LastCall Dev</dc:creator>
      <pubDate>Sun, 11 Oct 2026 05:42:14 +0000</pubDate>
      <link>https://dev.to/lastcall_dev/i-couldnt-block-windows-shutdown-so-i-bet-on-the-moment-before-the-power-button-1m04</link>
      <guid>https://dev.to/lastcall_dev/i-couldnt-block-windows-shutdown-so-i-bet-on-the-moment-before-the-power-button-1m04</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I wanted a tray app that catches "did you clock out?" the instant you shut down.&lt;/li&gt;
&lt;li&gt;The by-the-book approach — a service handling &lt;code&gt;SERVICE_CONTROL_PRESHUTDOWN&lt;/code&gt; — &lt;strong&gt;silently fails when Fast Startup (&lt;code&gt;HiberbootEnabled&lt;/code&gt;) is on&lt;/strong&gt;, because "shutdown" is really a hybrid hibernate.&lt;/li&gt;
&lt;li&gt;Hijacking &lt;code&gt;shutdown.exe&lt;/code&gt; via &lt;strong&gt;IFEO&lt;/strong&gt; works, but it's unshippable (admin rights, AV reputation, OS-update fragility, incomplete coverage).&lt;/li&gt;
&lt;li&gt;Conclusion: &lt;strong&gt;modern Windows shutdown can't be reliably blocked — and shouldn't be.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;So I flipped it: instead of &lt;em&gt;blocking&lt;/em&gt; shutdown, &lt;strong&gt;interrupt the moment the user reaches for it&lt;/strong&gt;. UI Automation watches the Start-menu power flyout and nudges just before the click.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. The itch: "Did you clock in?" at the exact moment you leave
&lt;/h2&gt;

&lt;p&gt;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: &lt;strong&gt;detect shutdown, pause it, show a confirm dialog, proceed on OK.&lt;/strong&gt; That was the start of a thorny road.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Attempt 1: a service that receives &lt;code&gt;PRESHUTDOWN&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The by-the-book way to intercept shutdown on Windows is a service receiving &lt;code&gt;SERVICE_CONTROL_PRESHUTDOWN&lt;/code&gt;. Unlike &lt;code&gt;WM_QUERYENDSESSION&lt;/code&gt;, it arrives &lt;em&gt;before&lt;/em&gt; other processes are killed, and you can extend the grace period.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SERVICE_ACCEPT_PRESHUTDOWN&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x00000100&lt;/span&gt;
&lt;span class="n"&gt;SERVICE_CONTROL_PRESHUTDOWN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x0000000F&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ShutdownBlockerService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;win32serviceutil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceFramework&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;GetAcceptedControls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;win32service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SERVICE_ACCEPT_STOP&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
                &lt;span class="n"&gt;win32service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SERVICE_ACCEPT_SHUTDOWN&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
                &lt;span class="n"&gt;SERVICE_ACCEPT_PRESHUTDOWN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked locally. Then on most real machines, &lt;strong&gt;the event never fired.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why: Fast Startup eats PRESHUTDOWN
&lt;/h3&gt;

&lt;p&gt;Windows 10/11 default to &lt;strong&gt;Fast Startup&lt;/strong&gt;, where "shutdown" is actually &lt;em&gt;logoff + hybrid hibernate&lt;/em&gt; — not a true shutdown — so &lt;code&gt;PRESHUTDOWN&lt;/code&gt; may never be delivered. The workaround is to disable Fast Startup via the registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Power
#   HiberbootEnabled = 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But that means &lt;strong&gt;silently rewriting the user's power settings&lt;/strong&gt;, plus a system service, admin rights, and a boot-time registry change. For an app you hand to ordinary users, that's absurd.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; On Windows, "shutdown" under Fast Startup is closer to hibernate. If you rely on &lt;code&gt;PRESHUTDOWN&lt;/code&gt;, suspect &lt;code&gt;HiberbootEnabled&lt;/code&gt; first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Attempt 2: hijack &lt;code&gt;shutdown.exe&lt;/code&gt; with IFEO
&lt;/h2&gt;

&lt;p&gt;"If a service won't work, take over the entrance — &lt;code&gt;shutdown.exe&lt;/code&gt;." Image File Execution Options lets you set a &lt;code&gt;Debugger&lt;/code&gt; value so your program launches &lt;em&gt;instead&lt;/em&gt; of the target.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;HKLM\SOFTWARE\Microsoft\Windows&lt;/span&gt; &lt;span class="err"&gt;NT\CurrentVersion\Image&lt;/span&gt; &lt;span class="err"&gt;File&lt;/span&gt; &lt;span class="err"&gt;Execution&lt;/span&gt; &lt;span class="err"&gt;Options&lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="err"&gt;shutdown.exe\&lt;/span&gt;&lt;span class="py"&gt;Debugger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"C:&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s"&gt;..&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="s"&gt;hutdownInterceptor.exe"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It only covers &lt;strong&gt;one&lt;/strong&gt; of many shutdown paths (Start menu power button, &lt;code&gt;Alt+F4&lt;/code&gt;, direct &lt;code&gt;ExitWindowsEx&lt;/code&gt;…).&lt;/li&gt;
&lt;li&gt;Needs &lt;strong&gt;admin&lt;/strong&gt; (HKLM).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AV flags it&lt;/strong&gt; — hijacking a system EXE via IFEO is indistinguishable from malware.&lt;/li&gt;
&lt;li&gt;Breaks on OS updates; uninstall leftovers are scary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not shippable. I rethought the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The pivot: stop blocking, start interrupting
&lt;/h2&gt;

&lt;p&gt;The requirement was never "block shutdown" — it was "say one thing before the PC goes off." Reliably &lt;em&gt;preventing&lt;/em&gt; shutdown (Fast Startup, many entrances, privileges, AV) is never worth it.&lt;/p&gt;

&lt;p&gt;So invert it: the moment the user &lt;em&gt;opens the Start-menu power flyout&lt;/em&gt;, interrupt from my side and show the reminder. Don't stop it — just make them notice.&lt;/p&gt;

&lt;p&gt;Windows 11's Start menu is a separate process (&lt;code&gt;StartMenuExperienceHost.exe&lt;/code&gt;), observable via &lt;strong&gt;UI Automation&lt;/strong&gt;. I poll lightly for the "Shut down" item becoming visible (or the cursor nearing "Power"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uiautomation&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;auto&lt;/span&gt;
&lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SetGlobalSearchTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# critical: disable blocking search
&lt;/span&gt;&lt;span class="n"&gt;desk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GetRootControl&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;SD_NAMES&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Shut down&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Shutdown&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;シャットダウン&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;POWER_NAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Power&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;電源&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SD_NAMES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;desk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;searchDepth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxSearchSeconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsOffscreen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BoundingRectangle&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;width&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;height&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# show a small card in the lower-right
&lt;/span&gt;        &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 100ms poll
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;SetGlobalSearchTimeout(0)&lt;/code&gt;, every miss waits out the default timeout and the poll stalls — a real gotcha I hit in practice.&lt;/p&gt;

&lt;p&gt;The result: &lt;em&gt;"just before you click the power button, a quiet card appears in the lower-right."&lt;/em&gt; Working &lt;em&gt;with&lt;/em&gt; the OS flow instead of fighting it — and far more stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Constraints decided the design: no service, no admin, clean uninstall
&lt;/h2&gt;

&lt;p&gt;The pivot was also reverse-engineered from &lt;strong&gt;distribution requirements&lt;/strong&gt;. To ship broadly (and on the Microsoft Store), three things matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No system service&lt;/strong&gt; (kills attempt 1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No admin / no registry hacks&lt;/strong&gt; (kills attempts 1 &amp;amp; 2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean uninstall&lt;/strong&gt; (AV reputation, Store policy)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Interrupt" satisfies all three naturally. I also generalized the triggers beyond shutdown: &lt;strong&gt;startup&lt;/strong&gt;, &lt;strong&gt;set times&lt;/strong&gt; (daily/weekly/monthly/Nth weekday), and shutdown/sign-out (where I use &lt;code&gt;WM_QUERYENDSESSION&lt;/code&gt; only as a &lt;em&gt;display trigger&lt;/em&gt;, never to block). The code shed its service and IFEO and collapsed into a single &lt;code&gt;tkinter&lt;/code&gt; tray process, shipped as one PyInstaller EXE writing only to &lt;code&gt;%APPDATA%&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modern Windows shutdown can't be reliably blocked.&lt;/strong&gt; Fast Startup, many entrances, privileges, AV — all against you.&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;PRESHUTDOWN&lt;/code&gt;? &lt;strong&gt;Suspect &lt;code&gt;HiberbootEnabled&lt;/code&gt; first.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IFEO system-EXE hijacking "works but can't ship"&lt;/strong&gt; (AV / admin / update fragility / uninstall).&lt;/li&gt;
&lt;li&gt;Re-reading the requirement from &lt;strong&gt;"block" to "interrupt (make them notice)"&lt;/strong&gt; lands on a gentle UI-Automation solution that also fits distribution constraints (no service, no admin, clean).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;I build LastCall solo — a Windows tray reminder (free; optional paid Microsoft 365 add-on).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it (free):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microsoft Store: &lt;a href="https://apps.microsoft.com/detail/9NZW2F210FX7" rel="noopener noreferrer"&gt;https://apps.microsoft.com/detail/9NZW2F210FX7&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://lastcall.ryusudo.com/en" rel="noopener noreferrer"&gt;https://lastcall.ryusudo.com/en&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to answer anything about the UI Automation polling or packaging a Python tray app for the Store.&lt;/p&gt;

</description>
      <category>windows</category>
      <category>python</category>
      <category>showdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
