I am writing a console application that accepts input (one-line commands) from stdin
. This application reads input in a dedicated thread, all input is stored in a queue and later processed by the main thread in a safe way. When the exit command is entered by the user, it is intercepted by the input thread which stops listening for new input, the thread is joined into the main one, and the application stops as requested.
Now I am containerizing this application, but I still want to be able to attach to the container and input commands from stdin
, so I specified tty
and stdin_open
to be true
in my docker compose service file, and that did the trick.
But I also want docker compose to be able to gracefully stop the application, so I decided to implement sigTerm()
in my application so that it can receive the signal from docker compose and gracefully stop, however I'm stuck on that part, because the input thread is blocking while waiting for input on stdin
. I can properly receive the signal, that's not at all the point here, but I'm looking for a way to be able to properly stop my containerized application while still being able to input commands from the keyboard.
My application could be simplified like that :
void gracefulStop() {
while (getThreadCount() > 1) { // this function exists somewhere else.
if (userInputThread.joinable()) {
userInputThread.join();
removeFromThreadCount(); // this function exists somewhere else.
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
exit(SUCCESS);
}
void sigTerm(int s) {
// Maybe do some stuff here, but what...
gracefulStop();
}
void userInputLoopThreadFunc() {
addToThreadCount(); // this function exists somewhere else.
while (keepGoing) {
char buf[4096];
if (!fgets(buf, sizeof(buf), stdin)) {
break; // we couldn't read from stdin, stop trying.
}
std::string input = std::string(buf); // we received a command
// Intercept exit command
if (input.starts_with("exit")) {
keepGoing = false;
}
// IRL there's thread safety
userInputQueue.push(input); // this will be processed by mainLoop() later
}
}
int main(int argc, char **argv) {
// Register the signal
signal(SIGTERM, sigTerm);
// Begin listening to user input
userInputThread = std::thread(&userInputLoopThreadFunc, this);
// this mainLoop function contains the core of the application
// as well as the processing code of the user input
mainLoop();
// if mainLoop function returned, we received the 'exit' command
gracefulStop();
}
I've read multiple question/answers like this one about non-blocking user input (the accepted answer advises to use a dedicated thread for input, which is what I am doing), or this other one about how to stop reading stdin, and the accepted answer seems promising but :
- using ncurses for what I'm trying to do seems really overkill
- If using
select()
and the timeout mechanism described, what would happen if the timeout occurs while typing a command?
Also I've read about the c 20 jthread
here :
The class jthread represents a single thread of execution. It has the same general behavior as std::thread, except that jthread automatically rejoins on destruction, and can be cancelled/stopped in certain situations.
But I'm not sure that would help me here.
I'm thinking about multiple possibilities to solve my issue :
- Find a way to send a newline character to the
stdin
of my application without user interaction, would be hackish if at all possible but would probably unblockfgets
. - Kill the thread, I understand killing a thread is considered a bad practice, but since the only thing I'm doing here is stopping the application, maybe I can live with that, would there be any side effect? How would I do that?
- Rewriting user input in another way (unknown to me yet,
jthread
, something else?) that would allowsigTerm()
to stop the application. - Maybe use ncurses (would that really help me to stop the application by receiving a signal?)
- Go with
select()
and the timeout mechanism and live with the risk of an interrupted input - Give up on user input and have some vacation time.
CodePudding user response:
You can close stdin
in your signal handler. fgets
will then return immediately (and presumably, return NULL
).
The good news is that close
is on the list of functions that are safe to call from a signal handler (it's a pretty restrictive list). Happy days.
There's an alternative based around EINTR
, but it looks messy since you don't know for certain that fgets
will actually return when it gets it.
Also, closing stdin
should still work should you switch to using cin
and getline
, which would definitely improve your code (*). That probably returns and sets badbit
when you close stdin
, although the code can be made more robust than by checking for that alone. Perhaps just set a (volatile
) flag in your signal handler and test that.
(*) Because getline
can read into a std::string
, which means it can read arbitrary long lines without worrying about allocating a fixed-size buffer that is 'big enough'.