Search code examples
c++windowsconsole

How to do stuff when the console is closed by user (C++)?


I have a program run on a console, and I need to save progress when the user closes the console. How to do it?

Example:

void func()
{
    while (true)
        if (UserClosesTheConsole()) // Dunno how to detect
            SaveProgess();
}

int main()
{
    std::thread t(func);
}

Any solution (doesn't need to be cross-platform) is welcome.


Solution

  • I would do something like this, where I put all the business logic inside a class. This class is then also responsible for managing the lifetime of the mainloop (thread). I also use condition variables to let the mainloop cooperatively shut down in a responisve manner.

    #include <cassert>
    #include <chrono>
    #include <thread>
    #include <mutex>
    #include <condition_variable>
    #include <iostream>
    
    #define WIN32_LEAN_AND_MEAN
    #include <windows.h>
    
    // It is always a good idea to encapsulate the business logic in a class
    // this way you can control the lifetime of the business logic
    // and the runtime state of it. This is especially important when
    // you have background threads running that you need to stop gracefully.
    class BusinessLogic
    {
    public:
        BusinessLogic() = default;
    
        ~BusinessLogic()
        {
            Stop();
        }
    
        // There is a chance for a small race condition where starting a thread
        // will return before the background thread is actually scheduled.
        // So wait for the mainloop to have really started (thread scheduled) before continuing.
        void Run()
        {
            m_thread = std::thread{ [this] { Mainloop(); } };
            WaitForState(State::Running); // Wait for mainloop to have really started
        }
    
        // Stop the business logic
        // this can happen because main asks it to stop or the signal handler asks it to stop
        void Stop()
        {
            // if already stopped do nothing
            {
                std::unique_lock<std::mutex> lock{ m_mutex };
                if(m_state == State::Stopped)
                    return;
            }
            SetState(State::Stopping);
            WaitForState(State::Stopped); // Wait for mainloop to have really stopped
        }
    
        void SaveProgress()
        {
            std::cout << "Saving progress\n";
        }
    
    private:
        // state of the business logic/mainloop
        enum class State
        {
            Initial,
            Starting,
            Running,
            Stopping,
            Stopped
        };
    
        // use a condition variable to signal state changes
        void SetState(State state)
        {
            std::unique_lock<std::mutex> lock{ m_mutex };
            m_state = state;
            m_cv.notify_all();
        }
    
        // use the condition varibale to wait for state changes
        void WaitForState(State state)
        {
            std::unique_lock<std::mutex> lock{ m_mutex };
            m_cv.wait(lock, [this, state] { return m_state == state; });
        }
    
        void Mainloop()
        {
            SetState(State::Running);
            while(true)
            {
                std::cout << "." << std::flush; // show mainloop is still running
    
                // Using a condition variable with a timeout allows for a clean and responsive shutdown
                std::unique_lock<std::mutex> lock{ m_mutex };
                m_cv.wait_for(lock, std::chrono::seconds(1), [this] { return m_state == State::Stopping; });
    
                // Check if we should stop (cooperative shutdown of the thread)
                if(m_state == State::Stopping)
                {
                    SaveProgress();
                    break;
                }
            }
            SetState(State::Stopped);
        }
    
        std::mutex m_mutex;
        std::condition_variable m_cv;
        State m_state{ State::Initial };
        std::thread m_thread;
    };
    
    // Meyer's singleton
    // Allows console handler to access the BusinessLogic instance
    BusinessLogic& GetBusinessLogic()
    {
        static BusinessLogic businessLogic;
        return businessLogic;
    }
    
    BOOL WINAPI SetConsoleHandler(DWORD signal)
    {
        if((signal == CTRL_C_EVENT) || (signal == CTRL_BREAK_EVENT) || (signal == CTRL_CLOSE_EVENT))
        {
            std::cout << "Control flow abort received\n";
            GetBusinessLogic().Stop();
            std::this_thread::sleep_for(std::chrono::seconds(3)); // Allow user to see the output before finally shutting down
        }
    
        return TRUE;
    }
    
    int main()
    {
        auto& businessLogic = GetBusinessLogic();
        auto retval = ::SetConsoleCtrlHandler(SetConsoleHandler, TRUE);
    
        businessLogic.Run();
    
        // Allow businessLogic to run for 10 seconds
        std::this_thread::sleep_for(std::chrono::seconds(10));
    
        businessLogic.Stop();
        return 0;
    }