9

perhaps I'm misinterpreting this part of Windows' Task Scheduler UI, but the following options suggest (to me) that a program is first asked nicely to stop, and then forcefully quit when that fails:

if the running task does not end when requested...

from the deepest corners of my mind, I remembered that Windows applications can respond to requests to quit; with that in mind, I was able to google up AppDomain.CurrentDomain.ProcessExit. however, it appears that Task Scheduler's "stop the task..." and AppDomain.CurrentDomain.ProcessExit do not work together as I had hoped; here is an example program I threw together that does not work:

using System;
using System.Threading;
using System.Windows.Forms;

namespace GimmeJustASec
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain.CurrentDomain.ProcessExit += new EventHandler(SuddenCleanup);

            while(true)
            {
                Thread.Sleep(1000);
            }
        }

        static void SuddenCleanup(object sender, EventArgs e)
        {
            MessageBox.Show("Hello!");
        }
    }
}

tl;dr my question is:

  1. can a program be written to respond to Task Scheduler stopping it? (or does Task Scheduler force-quit tasks in the rudest way possible?)
  2. if a program can be written in this way, how is the correct way to do so? (the way I tried is clearly not correct.)
  3. am I over-thinking all of this, and it would just be better to have the program time itself in a parallel thread? (perhaps with a max time set in app.config, or whatever.)

[edit] tried this variant, at Andrew Morton's request, with similar results:

using System;
using System.Threading;
using System.Windows.Forms;
using System.IO;

namespace GimmeJustASec
{
    class Program
    {
        private static StreamWriter _log;

        static void Main(string[] args)
        {
            _log = File.CreateText("GimmeJustASec.log");
            _log.AutoFlush = true;

            _log.WriteLine("Hello!");

            AppDomain.CurrentDomain.ProcessExit += new EventHandler(SuddenCleanup);

            while(true)
            {
                Thread.Sleep(1000);
            }
        }

        static void SuddenCleanup(object sender, EventArgs e)
        {
            _log.WriteLine("Goodbye!");
        }
    }
}

after Task Scheduler stops the task, the .log file contains "Hello!" but not "Goodbye!"

Ben
  • 719
  • 6
  • 25
  • This seems like the XY problem. Please tell us what are you trying to achieve instead of the problems you're having with the solution you think of – Pablo Recalde Aug 24 '17 at 17:49
  • fair! the problem is that we have several tasks that run overnight; some of them depend on others, and we have a dashboard to keep track of things, bla-bla-bla, and each task adds a record to a table indicating when it's started, updates that row when it's finished, etc. RARELY a task will take a very, very, very long time; in these cases, it's almost certainly a problem somewhere that we need to fix. ideally, when task scheduler ends a task, that task could perform some final cleanup (like updating its DB record with a stack trace and correct run status (failed), and/or e-mailing us.) – Ben Aug 24 '17 at 17:54
  • Do you have your scheduled task configured so it can [interact with the desktop](https://superuser.com/q/549752/272824)? If not, you won't see the message in your test. Writing to a file in a location to which the account the task runs under has write access to is usually easier. – Andrew Morton Aug 24 '17 at 17:56
  • for the example task I wrote, I scheduled it locally, with my account. I could see the CLI window open when the task started. I set task scheduler to end the task after 5 seconds, and after 5 seconds the CLI window closed; the MessageBox was never shown – Ben Aug 24 '17 at 17:56
  • There are two ways in Windows to *nicely* ask a program to stop running: send a `WM_CLOSE` message to its main (or all) windows (if a GUI) or send `CTRL_CLOSE_EVENT` to console applications. Does hooking up `Console.CancelKeyPress` show anything? (Conversely, there's only one non-nice way that's *guaranteed* to end things: `TerminateProcess()`. Do not run finalizers, do not pass go.) Apropos *finalizers*: if by some chance Task Scheduler does not stop the process rudely, finalizers should run. Does `try { while (true) { ... } } finally { _log.WriteLine(); }` write? – Jeroen Mostert Aug 24 '17 at 18:24
  • 1
    All that said, the *proper* way to handle this is to assume your program can die suddenly and without any chance to run code whatsoever (because, you know, it *can* -- let's say the .NET runtime has a bug that triggers a crash) and detect this *outside* the program, so you can do the mailing/logging from there. If you can brook a delay, you can still do this from the program itself: detect an unclean shutdown when the task next runs and *then* notify. This is tons more reliable than attempting to do cleanup/signaling while the process is going down (for whatever reason). – Jeroen Mostert Aug 24 '17 at 18:27
  • @JeroenMostert your point about other task-ending scenarios is a good one, and since you mention using an outside monitoring program, we *had* previously considered creating something like that, for other reasons (to tighten up scheduling - since any Task Scheduler time/interval will always leave some downtime - and to reduce the number of things we need in the Task Scheduler from 20+ to 1 (or even 0, if run as a service)). I'm thinking your ideas are better than mine; what's the SO-appropriate way to mark "my question was based on bad premises, and Jeroen Mostert showed this"? – Ben Aug 24 '17 at 18:56
  • @Ben Just ask Jeroen Mostert to convert those useful comments into an answer. – Andrew Morton Aug 25 '17 at 08:31
  • @JeroenMostert, you can also enumerate the threads in a process and try posting `WM_QUIT` to each thread's message queue via `PostThreadMessage`. This works even if the thread just calls `GetMessage` without creating any windows. It fails with `ERROR_INVALID_THREAD_ID` if the thread isn't it on the same desktop or doesn't have a message queue. Of course, just like `WM_CLOSE`, it's ultimately up to the application to decide whether or not it will actually exit when it gets this message. – Eryk Sun Aug 25 '17 at 12:22
  • As to console apps, there's no clean way to kill them unless you created the process with the flag `CREATE_NEW_PROCESS_GROUP`. If you simply send `WM_CLOSE` to the console window (like taskkill.exe rudely does), then the console kills *every process* that's attached to it, not just the single process that you want to kill. If you know it's the lead of a process group, you can narrow it down to just that group. Attach to the console via `AttachConsole` and send Ctrl+Break to the group via `GenerateConsoleCtrlEvent`. The C runtime handles this event and every event except Ctrl+C as `SIGBREAK`. – Eryk Sun Aug 25 '17 at 12:28
  • 1
    @eryksun: in .NET, things are actually considerably worse since by default, exiting a console application doesn't even run any finalizers or `finally` blocks, unless you jump through hoops. And that's when the event actually ends up with the application, which doesn't happen by default if you close the window... and when you get around *that*, there's still no event on shutdown. All in all, this is evidence much in favor of not trying to go for a clean exit at all, but just prepare for the rude exit when it comes. – Jeroen Mostert Aug 25 '17 at 12:48

1 Answers1

1

The proper way to handle this is to assume your program can die suddenly and without any chance to run code whatsoever (because, you know, it can -- let's say the .NET runtime has a bug that triggers a crash) and detect this outside the program, so you can do the mailing/logging from there. If you can brook a delay, you can still do this from the program itself: detect an unclean shutdown when the task next runs and then notify. This is tons more reliable than attempting to do cleanup/signaling while the process is going down (for whatever reason). This is especially true for console applications (if you're using those) because exiting those normally doesn't even run any finalizers, unless you write code for it (AppDomain.ProcessExit and Console.CancelKeyPress aren't enough, you have to go all the way to SetConsoleCtrlHandler). All in all, this does not make me have much hope for a clean exit that isn't performed by the application itself.

This does not answer the original question of whether you can detect a request to stop issued by Task Scheduler, and if so, how. I've tried to establish how that works, but I failed: if I run a console application under Task Scheduler that refuses to exit, it will merrily keep on running, even if I've configured it to be terminated after 10 seconds or 1 minute. (You can't set timeouts this short from the interface, but you can from the command line.) I did not test if the minimum time supported by Task Scheduler, 1 hour, works. I've also not tested if things fare differently when using an actual schedule, rather than a manually triggered task. If you manually end a task, though, it defiinitely just calls TerminateProcess and doesn't give you any chance for a clean exit -- that alone should be some motivation to not put your code for signaling failure in the task itself.

Jeroen Mostert
  • 27,176
  • 2
  • 52
  • 85