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.
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;
}