stop Ctrl + C from propagating upwards

144 views Asked by At

I have a program running (let's call it A), and inside this, I'm calling child_process from NodeJs to run another application (let's call it C). Now I can't change the structure of this, because we already have lots of features implemented in this pattern. So tweaking anything may cause to broken pipeline.

Now I want to terminate that C application, using CTRL+C. As that is a binary, I can't modify the source, so I need to send Ctrl + C to that program. Which I'm doing and it's happily terminating.

Now the issue is, it's killing C along with A. So that's the main problem as of now. I've tried creating another intermediate program, as B. But then it terminates C, then B, and finally A. So the Ctrl + C is propagating upwards. How can I prevent Ctrl + C from propagating up?

Here goes the pseudo-code:

FUNCTION SigintWindows(args):

        INITIALIZE isolate from args
        Create new handle scope with isolate

        GET processId from args[0]

        OPEN process with processId If fails:
            THROW Error("Failed to open process Error code: " + error code)
            EXIT

        TRY attaching to console If fails:
            THROW Error("Failed to attach to console Error code: " + error code)
            TRY sending Ctrl-C event directly If fails:
                THROW Error("Failed to send Ctrl-C event Error code: " + error code)
                CLOSE process handle
                EXIT
            ELSE:
                SET return value to true
                EXIT
        ELSE:
            DISABLE Ctrl-C handling for our program If fails:
                THROW Error("Failed to disable Ctrl-C handling Error code: " + error code)
                CLOSE process handle
                EXIT

            SEND Ctrl-C event If fails:
                THROW Error("Failed to send Ctrl-C event Error code: " + error code)

                RE-ENABLE Ctrl-C handling If fails:
                    THROW Error("Failed to re-enable Ctrl-C handling Error code: " + error code)

                FREE the console
                CLOSE process handle
                EXIT

            ELSE:
                WAIT for the process to exit (max 2 seconds) If process doesn't exit:
                    THROW Error("Process did not exit within 2 seconds")

                RE-ENABLE Ctrl-C handling If fails:
                    THROW Error("Failed to re-enable Ctrl-C handling Error code: " + error code)

                FREE the console
                CLOSE process handle
                EXIT

        END IF

        RE-ENABLE Ctrl-C handling If fails:
            THROW Error("Failed to re-enable Ctrl-C handling")
            CLOSE process handle
            EXIT

        FREE the console
        CLOSE process handle
        SET return value to true

And here goes the whole C++ implementation:

#include <node.h>
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
  #include <windows.h>
#endif

namespace ctrlc {

  using v8::FunctionCallbackInfo;
  using v8::Isolate;
  using v8::Local;
  using v8::Object;
  using v8::String;
  using v8::Value;

  void SigintWindows(const v8::FunctionCallbackInfo < v8::Value > & args) {
    #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
      v8::Isolate * isolate = args.GetIsolate();
      v8::HandleScope scope(isolate);

      // Check the number of arguments passed
      if (args.Length() != 1) {
        v8::Local < v8::String > v8String = v8::String::NewFromUtf8(isolate, "Invalid arguments").ToLocalChecked();
        isolate -> ThrowException(v8::Exception::TypeError(v8String));

        return;
      }

      // Check the argument types
      if (!args[0] -> IsUint32()) {
        v8::Local < v8::String > v8String = v8::String::NewFromUtf8(isolate, "Argument must be a number").ToLocalChecked();
        isolate -> ThrowException(v8::Exception::TypeError(v8String));

        return;
      }

      DWORD processId = args[0] -> Uint32Value(isolate -> GetCurrentContext()).ToChecked();

      HANDLE hProcess = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, FALSE, processId);
      if (hProcess == NULL) {
        v8::Local<v8::String> v8String = v8::String::NewFromUtf8(isolate, ("Failed to open process. Error code: " + std::to_string(GetLastError())).c_str()).ToLocalChecked();
        isolate->ThrowException(v8::Exception::Error(v8String));

        return;
      }

      // Try to attach to console
      if (!AttachConsole(processId)) {
        v8::Local<v8::String> v8String = v8::String::NewFromUtf8(isolate, ("Failed to attach to console. Error code: " + std::to_string(GetLastError())).c_str()).ToLocalChecked();
        isolate->ThrowException(v8::Exception::Error(v8String));

        // If attaching to console fails, try sending Ctrl-C event directly
        if (!GenerateConsoleCtrlEvent(CTRL_C_EVENT, processId)) {
          v8::Local<v8::String> v8String = v8::String::NewFromUtf8(isolate, ("Failed to send Ctrl-C event. Error code: " + std::to_string(GetLastError())).c_str()).ToLocalChecked();
          isolate->ThrowException(v8::Exception::Error(v8String));

          CloseHandle(hProcess);

          return;
        } else {
          args.GetReturnValue().Set(true);
          return;
        }
      } else {
        // Disable Ctrl-C handling for our program
        if (!SetConsoleCtrlHandler(NULL, TRUE)) {
          v8::Local<v8::String> v8String = v8::String::NewFromUtf8(isolate, ("Failed to disable Ctrl-C handling. Error code: " + std::to_string(GetLastError())).c_str()).ToLocalChecked();
          isolate->ThrowException(v8::Exception::Error(v8String));

          CloseHandle(hProcess);

          return;
        }

        // Send Ctrl-C event
        if (!GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0)) {
          v8::Local<v8::String> v8String = v8::String::NewFromUtf8(isolate, ("Failed to send Ctrl-C event. Error code: " + std::to_string(GetLastError())).c_str()).ToLocalChecked();
          isolate->ThrowException(v8::Exception::Error(v8String));

          // Re-enable Ctrl-C handling
          if (!SetConsoleCtrlHandler(NULL, FALSE)) {
            v8::Local<v8::String> v8String = v8::String::NewFromUtf8(isolate, ("Failed to re-enable Ctrl-C handling. Error code: " + std::to_string(GetLastError())).c_str()).ToLocalChecked();
            isolate->ThrowException(v8::Exception::Error(v8String));
          }

          FreeConsole();
          CloseHandle(hProcess);
          return;
        } else {
          // Wait for process to exit
          if (WaitForSingleObject(hProcess, 2000) != WAIT_OBJECT_0) {
            v8::Local<v8::String> v8String = v8::String::NewFromUtf8(isolate, "Process did not exit within 2 seconds.").ToLocalChecked();
            isolate->ThrowException(v8::Exception::Error(v8String));
          }

          // Re-enable Ctrl-C handling
          if (!SetConsoleCtrlHandler(NULL, FALSE)) {
            v8::Local<v8::String> v8String = v8::String::NewFromUtf8(isolate, ("Failed to re-enable Ctrl-C handling. Error code: " + std::to_string(GetLastError())).c_str()).ToLocalChecked();
            isolate->ThrowException(v8::Exception::Error(v8String));
          }

          FreeConsole();
          CloseHandle(hProcess);
          return;
        }
      }

      // Re-enable Ctrl-C handling
      if (!SetConsoleCtrlHandler(NULL, FALSE)) {
        v8::Local<v8::String> v8String = v8::String::NewFromUtf8(isolate, "Failed to re-enable Ctrl-C handling").ToLocalChecked();
        isolate->ThrowException(v8::Exception::Error(v8String));

        CloseHandle(hProcess);
        
        return;
      }

      FreeConsole();
      CloseHandle(hProcess);
      args.GetReturnValue().Set(True(isolate));
    #endif
  }

  void Init(Local < Object > exports) {
    NODE_SET_METHOD(exports, "sigintWindows", SigintWindows);
  }

  NODE_MODULE(ctrlc, Init)

} // namespace ctrlc

Feel free to provide any solutions or suggestions.

NB: I've tried assigning an handler, instead of a none, but that doesn't work either.

2

There are 2 answers

0
Ahmed AEK On

CTRL+C signal is sent by the terminal, and is received by all the processes A B C and D connected to that terminal at the exact same time.

you should kill the child process C by its handle using TerminateProcess, and not by sending a CTRL+C event, this way your process A is not affected, needless to say this will prevent C from doing any resources cleanup, so be careful of that.

another option is to launch B and C in a separate new terminal, this way CTRL+C is sent to the terminal containing only B and C, therefore A will be unaffected.

0
YangXiaoPo-MSFT On

As HandlerRoutine said,

Because the system creates a new thread in the process to execute the handler function,...

Each console process has its own list of HandlerRoutine functions. Initially, this list contains only a default handler function that calls ExitProcess. A console process adds or removes additional handler functions by calling the SetConsoleCtrlHandler function, which does not affect the list of handler functions for other processes.

As a general way, you need the target process to execute a specific piece of code which is ExitProcess in this case. In the view of HandlerRoutine, @RbMm indicated a way to call CreateRemoteThread in target process with entry point at CtrlRoutine and CTRL_C_EVENT as parameter.