Search code examples
c#multithreadingconsoleexit

How can I make a C# Console program wait for certain code before terminating?


I have a C# project using a Logger.

To improve performance, this Logger will add the log info to a waiting queue, and another thread will handle it, including writing it to the Console and the log file.

Unfortunately, it has a big problem: if the process is terminating so fast that the logger isn't fast enough to keep up and handle the rest of log infos in the queue, THEY WILL BE PERMANENTLY LOST.

More clearly, this is the main code of my logging handle thread:

public static void Info(string content, string? sender = null)
    => qlog.Enqueue(new LogDetail(content, LogLevel.Information, sender));

...

private static ConcurrentQueue<LogDetail> qlog = new();
public static int RefreshLogTicks { get; set; }

private static async Task BackgroundUpdate()
{
    if (!qlog.TryDequeue(out LogDetail _log))
    {
         await Task.Delay(RefreshLogTicks);
         await Task.Run(BackgroundUpdate);
         return;
    }

    List<string> logs = new(qlog.Count + 1);
    logs.Add(WriteLog(_log));
    while (qlog.TryDequeue(out LogDetail log))
    {
         logs.Add(WriteLog(log));
    }
    ConsoleWrapper.WriteLine(logs);  

    await Task.Delay(RefreshLogTicks);
    await Task.Run(BackgroundUpdate);
}

(If needed, the full code is open source on GitHub)

Then if I execute code like this:

Console.WriteLine("Hello, ");
Log.Info("Logger!", "Hello");
Console.WriteLine("World!");
Environment.Exit(0);

Then I get an output as this: (at least on my sloooow laptop with a 2-core CPU)

Hello,
World!

If I apply a simple change:

Console.WriteLine("Hello, ");
Log.Info("Logger!", "Hello");
Console.WriteLine("World!");
Console.ReadLine();
Environment.Exit(0);

Then the logger output will recover:

Hello,
World!
21:28:17 <Info:Hello> Logger!

So, I wonder if there's a way to guarantee that the process will terminate only when the queue is Empty? Or to make the process wait until the handle process ended?

(I know that I can't stop the exit process if someone kills it, so "end" below refers to a normal exit, e.g. Environment.Exit() or Application.Exit().)

Notice that I aren't expecting an implement like providing a method Stop() and demand invoking it before closing because I don't think the code users will always correctly invoke it - and as it is mentioned above, if an extra invoke is needed, Console.ReadLine() seems far more simple.

If it's impossible, please tell me as well. Thanks a lot!


Solution

  • Personally, I think the key of my question is how to prevent the unflushed logs from being lost, or it's to say, how to execute a clean-up process when the program is terminating.

    What solved my problem is the AppDomain.ProcessExit Event:

    The EventHandler for this event can perform termination activities, such as closing files, releasing storage and so on, before the process ends.

    Take my code for an example.

    public static void Initialize()
    {
        // ...
        AppDomain.CurrentDomain.ProcessExit += ClearUpLogQueue;
    }
    

    I registered an handler for this event, which flushs the unwritten content and dispose log files.

    Then run a test:

    // Sudden terminate test
    Log.Info($"The clearup succeed!");
    Environment.Exit(0);
    

    By this fix, the last log message is successfully flushed.

    22:52:10 <Info> The clearup succeed!
    

    Notice that it won't work when Environment.FailFast is invoked (actually expected):

    Log.Info($"The clearup succeed!");
    Environment.FailFast("test message, not error");
    
    Process terminated. test message, not error
       at System.Environment.FailFast(System.String)
       at Program.<Main>$(System.String[])