Home > Software engineering >  How to stop a thread reading stdin in a C Linux console application?
How to stop a thread reading stdin in a C Linux console application?

Time:03-03

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 unblock fgets.
  • 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 allow sigTerm() 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'.

  • Related