windowsgo

How to gracefully terminate a process on Windows, similar to SIGTERM?


I want to exit the target process gracefully instead of terminating it directly. Process A send a command to Process B, and after receiving it, B will complete its cleanup tasks and then exit.

  1. The target process is not a console program, so I can't use windows.GenerateConsoleCtrlEvent.
  2. The target process is not a gui, so i can't use WM_CLOSE.

The Kill method in the Golang os package actually uses TerminateProcess, which is not what I want.

Update:

  1. I tried to make the target process listen to stdin. The subprocess started and then ended immediately. It seems that this doesn't work in non-interactive mode.

    reader := bufio.NewReader(os.Stdin)
    if _, err := reader.ReadString('\n'); err != nil {
        stop()
    }
    

    Sorry, that's a stupid error. This method is feasible. It's just that I used stdin incorrectly.

  2. I tried using taskkill /t, but it told me that the target process is a child process of another process. So I adjusted the parent-child relationship and had the parent process call taskkill /t, but it still returned the error Exit status 128. That's very strange.

Finally:

In the end, I solved the problem using named pipes. The downside is that it requires support in the target process's code. But that's sufficient for me because the target process is not a third-party one.


Solution

  • In the end, I solved the problem using pipes.

    1. use named pipe

    // target process main.go
    func main() {
        // ... some logic 
    
        sig := make(chan os.Signal, 1)
        signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    
        go func() {
            pipeName := fmt.Sprintf("\\\\.\\pipe\\%d", os.Getpid())
            listener, err := winio.ListenPipe(pipeName, nil)
            if err != nil {
                log.Errorf("Failed to create pipe: %v", err)
                return
            }
            defer listener.Close()
    
            conn, err := listener.Accept()
            if err != nil {
                log.Errorf("Pipe accept error: %v", err)
                return
            }
            defer conn.Close()
    
            buf := make([]byte, 1024)
            n, err := conn.Read(buf)
            if err != nil {
                log.Errorf("Pipe read error: %v", err)
            }
            msg := string(buf[:n])
            if msg == "shutdown" {
                sig <- syscall.SIGTERM
            }
        }()
        
        <-sig
        stop()
    }
    
    // control process
    func killProcessGraceful(targetPID int) {
        pipeName := fmt.Sprintf(`\\.\pipe\%d`, targetPID)
        conn, err := winio.DialPipe(pipeName, nil)
        if err != nil {
            log.Errorf("Failed to connect to pipe (PID=%d): %v", targetPID, err)
            return killProcessForcefully(targetPID)
        }
        defer conn.Close()
        if _, err := conn.Write([]byte("shutdown")); err != nil {
            log.Errorf("Failed to send shutdown command: %v", err)
            return killProcessForcefully(targetPID)
        }
    
        return nil
    }
    

    2. use stdin

    // target process main.go
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    go func() {
        buf := make([]byte, 1)
        if _, err := os.Stdin.Read(buf); err == io.EOF {
            sig <- syscall.SIGTERM
        }       
    }()
    
    // control process
    func StartProcess(path string, arg ...string) error {
        cmd := exec.Command(path, arg...)
        cmd.Dir = filepath.Dir(path)
        stdinPipe, err := cmd.StdinPipe()
        if err != nil {
            return errors.Errorf("get stdin pipe failed: %v", err)
        }
        pipeMap.Store(filepath.Base(path), stdinPipe)
        if err := cmd.Start(); err != nil {
            return errors.Errorf("start process %s failed: %v", filepath.Base(path), err)
        }
    
        return nil
    }
    
    func kill(process string) {
        if pipe, ok := pipeMap.Load(processName); ok {
            pipe.(io.WriteCloser).Close()
        }
        // ...force kill
    }