3

I'm currently triggering a reboot using the windows api call InitiateSystemShutdownW. I'm passing this a dwTimeout value (10 minutes). I want to detect if this reboot (or any other reboot) has been scheduled:

[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool InitiateSystemShutdownW([MarshalAs(UnmanagedType.LPWStr)]String lpMachineName, [MarshalAs(UnmanagedType.LPWStr)]String lpMessage, int dwTimeout, bool bForceAppsClosed, bool bRebootAfterShutdown);

I know that if I call it again I will receive an error message that tells me it's already scheduled (via GetLastWin32Error), but I need to do this without calling reboot again (because I may not want to actually trigger a reboot at this point).

I've tried calling GetSystemMetrics with the parameter SM_SHUTTINGDOWN but even though I know this is scheduled this returns false/0. I'm presuming this is an actual shutdown happening not scheduled to happen.

Is there anyway I can detect that this call is in progress or more generally whether a reboot is scheduled or not?

I'm calling these methods using C#/DllImport/interop so I need an api that is accessible from a c# process.

Liam
  • 27,717
  • 28
  • 128
  • 190
  • winlogon.exe has variables inside it recording the pending shutdown time, message, flags, etc–but there is no public API to retrieve that info from it (other than relying on the event log). Windows really should have something like a "GetPendingShutdownDetails" API. I created a feedback hub issue asking Microsoft to add one – https://aka.ms/AAcdp86 – Simon Kissane May 02 '21 at 04:04

2 Answers2

2

You can use the Windows Event Log API to access log entries on the "System" channel. That log records requests by a login to shutdown the system, and to abort a system shutdown.

This code shows how to use P/Invoke to get the number of system shutdown and shutdown abort requests within a specified timeframe before the query (in milliseconds). So if you have 2 scheduled shutdown requests in the last hour, and 1 abort request, then you have a pending shutdown in progress.

If you really want all of the details about a logged shutdown request, then you can use the EvtRender function to pull out the data in XML, and parse it. This code doesn't do that.

using System;
using System.Runtime.InteropServices;

namespace WindowsEventLogChecker
{
  // partial list from "winerror.h"
  public enum ERROR
  {
    ERROR_SUCCESS,
    ERROR_NO_MORE_ITEMS = 259,
    ERROR_EVT_CHANNEL_NOT_FOUND = 15007,
    ERROR_EVT_INVALID_QUERY = 15001
  }

  // this is our own enum
  public enum SystemEventType
  {
    Shutdown,
    Abort
  }

  // these are from "winevt.h"
  public enum EVT_QUERY_FLAGS
  {
    EvtQueryChannelPath = 0x1,
    EvtQueryFilePath = 0x2,
    EvtQueryForwardDirection = 0x100,
    EvtQueryReverseDirection = 0x200,
    EvtQueryTolerateQueryErrors = 0x1000
  }


  class Program
  {
    [DllImport("wevtapi.dll", EntryPoint = "EvtQuery", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
    public static extern IntPtr EvtQuery(IntPtr session, [MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] string query, int flags);

    [DllImport("wevtapi.dll", EntryPoint = "EvtNext", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
    public static extern bool EvtNext(IntPtr resultSet, int batchSize, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] eventBatch, int timeout, int flags, ref int nReturned);

    [DllImport("wevtapi.dll", EntryPoint = "EvtClose", CallingConvention = CallingConvention.StdCall)]
    public static extern bool EvtClose(IntPtr handle);

    [DllImport("kernel32.dll", EntryPoint = "GetLastError", CallingConvention = CallingConvention.StdCall)]
    public static extern int GetLastError();

    static void Main(string[] args)
    {
      // get the number of scheduled shutdowns in the last hour
      int nShutdowns = GetEventCount(SystemEventType.Shutdown, 3600000);

      // get the number of aborted shutdowns in the last hour
      int nAborts = GetEventCount(SystemEventType.Abort, 3600000);
    }

    private static int GetEventCount(SystemEventType evtType, int timeSpanMs)
    {
      ERROR status = ERROR.ERROR_SUCCESS;
      IntPtr hResult = IntPtr.Zero;
      IntPtr[] eventBatch = new IntPtr[10];

      // these 2 event id's, along with 'USER32' event source, denote requested
      // shutdown and abort, respectively
      string shutDownId = "1074";
      string abortId = "1075";

      // XPath query to get the event id, event source, and timespan in ms from now 
      // back to when the event was posted to the event log.
      string format = "*[System[(EventID = {0}) and Provider[@Name=\"USER32\"] and TimeCreated[timediff(@SystemTime) <= {1}]]]";

      // The "System" event channel
      string channelPath = "System";

      int nEvents = 0;
      int count = 0;
      string evtQuery;

      switch (evtType)
      {
        case SystemEventType.Shutdown:
        evtQuery = string.Format(format, shutDownId, timeSpanMs);
        break;
        case SystemEventType.Abort:
        evtQuery = string.Format(format, abortId, timeSpanMs);
        break;
        default:
        throw new InvalidOperationException();
      }
      hResult = EvtQuery(IntPtr.Zero, channelPath, evtQuery, (int)(EVT_QUERY_FLAGS.EvtQueryChannelPath | EVT_QUERY_FLAGS.EvtQueryReverseDirection));
      if (IntPtr.Zero == hResult)
      {
        status = (ERROR)GetLastError();

        if (status == ERROR.ERROR_EVT_CHANNEL_NOT_FOUND)
        {
          // log error
          return 0;
        }
        else if (status == ERROR.ERROR_EVT_INVALID_QUERY)
        {
          // log error
          return 0;
        }
        else
        {
          // log error
          return 0;
        }
      }
      while (EvtNext(hResult, 10, eventBatch, 1000, 0, ref nEvents))
      {
        for (int i = 0; i < nEvents; i++)
        {
          count++;
          if (eventBatch[i] != IntPtr.Zero)
          {
            EvtClose(eventBatch[i]);
          }
        }
      }
      status = (ERROR)GetLastError();
      if (status != ERROR.ERROR_NO_MORE_ITEMS)
      {
        // log error here and cleanup any remaining events
        for (int i = 0; i < nEvents; i++)
        {
          if (eventBatch[i] != IntPtr.Zero)
          {
            EvtClose(eventBatch[i]);
          }
        }
      }
      if (hResult != null)
      {
        EvtClose(hResult);
      }
      return count;
    }
  }
}
Mark Benningfield
  • 2,800
  • 9
  • 31
  • 31
  • Seems my workload has been "re-prioritised" so I'll have to check this another time. Seems like a reasonable solution though, thanks. – Liam Apr 26 '19 at 08:10
  • 1
    Theoretically speaking, the shutdown timeout can be up to 10 years–the maximum timeout is 3650 days minus one second–and so this method may fail to find the shutdown message in the event log because it isn't looking back far enough (as presented it only looks back 1 hour), or even the shutdown event was so long ago it has been removed from the log. (In practice, this issue will be unlikely to occur, because shutdowns are much more likely to be triggered for briefer periods such as 1 hour or 5 minutes.) – Simon Kissane May 02 '21 at 04:38
  • Why don't you use the Eventlog() class instead? – marsh-wiggle May 24 '21 at 17:03
-2

This answer here has information about checking the Windows Registry via Powershell to see if a reboot is scheduled:

Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'

Aside from that, ExitWindowsEx with a ridiculously long timeout (max is 315360000 - 10 years), then checking for the ERROR_SHUTDOWN_IS_SCHEDULED - if the command succeeds, then you'll want to (of course) AbortSystemShutdown...

Frank Alvaro
  • 468
  • 4
  • 12
  • That answer says *this will not return true if a reboot in scheduled using shutdown.exe, it will only return true if Windows says your computer need to be rebooted.* shutdown.exe is just a wrapper to `InitiateSystemShutdownW`. I'm checking but I think this is wrong. I also stated in the question that I don't want to trigger a reboot (long time or not) – Liam Apr 25 '19 at 15:32
  • Apologies, and unfortunate about the downvotes :\ But, one of your questions states "Is there anyway I can detect that this call is in progress **or more generally whether a reboot is scheduled or not**?" – Frank Alvaro Apr 25 '19 at 15:42
  • But this doesn't detect "general" scheulded reboots though does it, or else it'd work for mine, general as in, non-specific. It detects that specific scenario. That's not what general means. If you think this is a [duplicate then you also shouldn't add an answer](https://meta.stackexchange.com/a/10844/217110) – Liam Apr 26 '19 at 08:12
  • The "ridiculously long timeout" method has the downside that it pollutes the event log. – Simon Kissane May 02 '21 at 04:31