3

I am working on a data acquisition application and I want to ensure that it exits gracefully. That is, it processes all the already collected data, flushes all the (file) buffers to "disk" (persistent memory) and might even uploads the data to the cloud.

So, I wrote (based on this answer) the code below to catch every close event. (It is just a test code.)

Problem: If I use the X in the top-right corner of the console, the program gets terminated after a short delay, even though the termination sequence is still running. (The handler does get called, and it does start to wait for the threads to join but then it gets killed after a while.) If I terminate with Crt+C or Ctr+Break it works as intended; The termination sequence finishes and exits the process.

Question: How can I make the OS wait for my application to terminate instead of killing it off after a short grace period?

#region Trap application termination
[DllImport("Kernel32")]
private static extern bool SetConsoleCtrlHandler(EventHandler handler, bool add);

private delegate bool EventHandler(CtrlType sig);
static EventHandler _handler;

enum CtrlType
{
    CTRL_C_EVENT = 0,
    CTRL_BREAK_EVENT = 1,
    CTRL_CLOSE_EVENT = 2,
    CTRL_LOGOFF_EVENT = 5,
    CTRL_SHUTDOWN_EVENT = 6
}

private static bool Handler(CtrlType sig, List<Thread> threads, List<Task> tasks, CancellationTokenSource cancellationRequest)
{
    //starts new foregeound thread, so the process doesn't terminate when all the cancelled threads end
    Thread closer = new Thread(() => terminationSequence(threads, tasks, cancellationRequest));
    closer.IsBackground = false;
    closer.Start();

    closer.Join();  //wait for the termination sequence to finish

    return true; //just to be pretty; this never runs (obviously)
}
private static void terminationSequence(List<Thread> threads, List<Task> tasks, CancellationTokenSource cancellationRequest)
{
    cancellationRequest.Cancel(); //sends cancellation requests to all threads and tasks

    //wait for all the tasks to meet the cancellation request
    foreach (Task task in tasks)
    {
        task.Wait();
    }

    //wait for all the treads to meet the cancellation request
    foreach (Thread thread in threads)
    {
        thread.Join();
    }
    /*maybe do some additional work*/
    //simulate work being done
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Console.WriteLine("Spinning");
    while (stopwatch.Elapsed.Seconds < 30)
    {
        if (stopwatch.Elapsed.Seconds % 2 == 0)
        {
            Console.Clear();
            Console.WriteLine("Elapsed Time: {0}m {1}s", stopwatch.Elapsed.Minutes, stopwatch.Elapsed.Seconds);
        }
        Thread.SpinWait(10000);
    }

    Environment.Exit(0); //exit the process
}
#endregion

static void Main(string[] args)
{
    CancellationTokenSource cancellationRequest = new CancellationTokenSource();    //cancellation signal to all threads and tasks
    List<Thread> threads = new List<Thread>(); //list of threads

    //specifys termination handler
    _handler += new EventHandler((type) => Handler(type, threads, new List<Task>(), cancellationRequest));
    SetConsoleCtrlHandler(_handler, true);

    //creating a new thread
    Thread t = new Thread(() => logic(cancellationRequest.Token));
    threads.Add(t);
    t.Start();
}
Rosdi Kasim
  • 20,195
  • 22
  • 115
  • 142
Cerike
  • 344
  • 1
  • 11
  • Possible duplicate of [How to stop C# console applications from closing automatically?](https://stackoverflow.com/questions/11512821/how-to-stop-c-sharp-console-applications-from-closing-automatically) – Rosdi Kasim Mar 11 '18 at 13:31
  • 6
    @RosdiKasim I'm sorry, but people like you who flag questions as duplicates of questions which have nothing to do with the question in question are absolutely ruining this site... – Cerike Mar 11 '18 at 13:45
  • May be this [SO post](https://stackoverflow.com/questions/4646827/on-exit-for-a-console-application) can give you some hints. Looks like the maximum time that the OS gives you is 5 secs to do all the cleanup work. – Thangadurai Mar 11 '18 at 13:55
  • 1
    @Thangadurai that particular timeout is controlled by the .NET runtime. If he were to use the win32 API console approach, the timeout is 30 seconds. Neither are very good if he decides to upload data. I was writing a suggestion to write a Windows service which should give him complete control, but the `Task` answer is much better. – McGuireV10 Mar 11 '18 at 14:04
  • Sorry for that, but apparently I am not the only one confused by the question. Also, 1 flag wont get your question closed. Perhaps you should reword the question title. – Rosdi Kasim Mar 11 '18 at 14:04
  • @McGuireV10, Though I have not absolutely tested the time limits, the SO post that I referred is using Win32 API and someone mentioned that the time limit is 3 seconds. – Thangadurai Mar 11 '18 at 14:09
  • Deadlock is a very likely outcome for code like this, the user never gives a hoot what your code might be doing when he forces termination. With very high odds that it is at the worst possible time, a basic reason he decided to put an end to it. It is very hard to debug, the OS simply does not give you enough time to find out why a thread is not responding to the cancellation request. It has to be done the hard way, use Trace.WriteLine(). – Hans Passant Mar 11 '18 at 14:16
  • @Thangadurai Thank you for the link, it was helpful in closing on on the underling issue. – Cerike Mar 11 '18 at 15:19
  • I think it's not reasonable to expect that OS will give your application infinite amount of time to shutdown if user explicitly closes it (or OS is shutting down itself). – Evk Mar 11 '18 at 15:19
  • @Evk Well, I didn't realize, that when you close the console it sends the same signal as if you try to kill the process in the task manager. I wanted to ensure that if the user had enough of the data collecting and just closes the window every last bit of info gets saved. (Because that is what the user would naturally expect.) But it now seems to me that a console application is just not the right setting for something like this. (Upon system shutdown the OS does ask the user if he/she would want to kill the program or wait for it to finish.) – Cerike Mar 11 '18 at 15:27
  • @McGuireV10 The "Task answer" doesn't work... I do now think that you are right that I need to do this in a different setting. (Not in windows Service though, because that is not really user friendly.) I think I will make a Forms Application. – Cerike Mar 11 '18 at 15:34
  • @HansPassant Thank you for your answer on the other question. Could you please tell me where is the 5 second deadline coming from. (Could you link me to some windows documentation for example.) What do you mean by that I should use Trace.WriteLine()? Isn't that just prints on the output console if you are debugging? – Cerike Mar 11 '18 at 15:40
  • The manual is not going to help you. It is far more important for you to think through what happens when your program terminates unexpectedly for *other* reasons. Like it crashing because of an unhandled critical exception, somebody tripping over the power cord and unplugging the machine, the user rebooting the OS without waiting. Mishaps that are quite beyond your control, it still needs to work correctly. And not different from the user closing the console. – Hans Passant Mar 11 '18 at 16:04
  • @HansPassant Yes, I am already dealing with that: File.Replace(), FlushFileBuffers, periodic upload of result to the cloud, CRC checks, etc. My problem with the user closing the console is, that if he/she closes it after 5 minutest he/she expects (somewhat rightful so) 5 minuets of data. However, my goal is not to save self-destructive users from themselves (ex. pulling the power cord). In that case they get the best effort on the part of my application to save as many data as it can, but they can have no expectations of having all the data. But, I would enjoy reading the doc about the 5 sec! – Cerike Mar 11 '18 at 16:38
  • @HansPassant So if you would link me to the Windows documentation where it talks about what happens when a CTRL_CLOSE_EVENT gets raised (the user kills the process) and how the process has 5 more secs to exit before it gets butchered, I would highly appreciate that. Not because of this specific problem, but because it interests me. – Cerike Mar 11 '18 at 16:43
  • @Cerike your console program wouldn't go away in a Windows service scenario, it would just offload all that IO work to the service. – McGuireV10 Mar 12 '18 at 10:47

1 Answers1

2

Since C# 7.1 you can have an async Task Main() method. Using this, you can modify your handler to create a method, and wait on it in the Main method.

Sidenote: you should use tasks instead of threads wherever you can. Tasks manage your thread better, and they run of the ThreadPool. When you create a new Thread instance, it's assuming it'll be a long running task, and the windows will treat it differently.

So with that in mind, consider wrapping the TerminateSequence method in a task, instead of a thread, and make that task a member of your class. Now you won't have to wait for it in the context of the handler, you could instead wait for it in the Main method.

With the rest of your code remaining the same, you could do the following:

private Task _finalTask;

private static bool Handler(CtrlType sig, List<Thread> threads, List<Task> tasks, CancellationTokenSource cancellationRequest)
{
    //starts new foregeound thread, so the process doesn't terminate when all the cancelled threads end
    _finalTask = Task.Run(() => terminationSequence(threads, tasks, cancellationRequest));
}

// ...

static async Task Main(string[] args)
{
    // ...

    // Wait for the termination process
    if(_finalProcess != null)
        await _finalTask
}

If you're not working with C# 7.1 you can still do this, it'll just be a little less elegant. All you need to do is wait for it:

_finalTask?.Wait();

And that should do it.

CKII
  • 1,316
  • 12
  • 26
  • 2
    I like this answer, but I would suggest the OP tests behavior in edge-cases like the user logging out or machine shutdown. I know with the old Win32 console API those were handled very differently. – McGuireV10 Mar 11 '18 at 14:05
  • 2
    You should use `_finalTask?.GetAwaiter().GetResult();` instead. See https://stackoverflow.com/questions/36426937/what-is-the-difference-between-wait-vs-getawaiter-getresult for why. – Cameron MacFarland Mar 11 '18 at 14:35
  • @CKII For the record: I did try out your solution and it doesn't work. Why would it? (Maybe I missed something.) But, you are doing the same thing that I did just in a slightly different way, and thus it has the same problem; The OS/.NET framework kills it before it would complete. – Cerike Mar 11 '18 at 14:51
  • 1
    I don't see how this could help. OS will still kill the process the same way, whether you use tasks, threads or whatever else. – Evk Mar 11 '18 at 14:53