最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

go - How to gracefully terminate a process on Windows, similar to SIGTERM? - Stack Overflow

programmeradmin1浏览0评论

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.

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.

Share Improve this question edited Apr 1 at 9:28 Colder asked Mar 28 at 7:52 ColderColder 1805 bronze badges 7
  • 3 If you'll squint a bit at your question, you'll notice that basically you've mentioned several forms of IPC: Unix signals, Win32 window messages and Win32 console stuff. So, the answer to your question would probably be "use some other available form of IPC". Supposedly a named event object would do what you're after: the target process has to wait on it while the controlling process would signal that event. – kostix Commented Mar 28 at 9:14
  • 1 Note that if the controlling process actually starts the target process itself, you can possibly use a way simpler form of IPC: make the target process read its stdin and make controlling process signal something using the target process' stdin (closing it would be the simplest way to do that). if you cannot go that way, you can create a pipe and pass its read end to the target process. – kostix Commented Mar 28 at 9:19
  • Thank you very much. It has clarified my thoughts all of a sudden. I will give it a try and come back to update if there is any result. – Colder Commented Mar 28 at 9:28
  • Globally, closing a process on Windows implies to do, in this order: sending WM_CLOSE, sending WM_QUIT if previous failed, sending WM_DESTROY if previous failed, and all are posted to process' main thread. If all fails, you call GenerateConsoleCtrlEvent, and finally TerminateProcess if everything failed. If you put yourself in a situation where none of these could work (for example, a service, a hidden process, etc.) then you have two choices remaining: 1) Ensure that TerminateProcess won't harm your data, or 2) Implement a global semaphore or named pipe to receive the request. – Wisblade Commented Mar 28 at 11:37
  • 1 Thank you again. I have added the method using stdin in the answer. It is simpler. I have learned a lot about os and ipc. – Colder Commented Apr 1 at 10:14
 |  Show 2 more comments

2 Answers 2

Reset to default 1

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
}

When unsure, I recommend looking at well known open source applications that have the functionality that you wish to make. As an example Air is a well known process spawner that monitors for file changes and restarts applications on change. They implement the start and kill process like this:

func (e *Engine) killCmd(cmd *exec.Cmd) (pid int, err error) {
    pid = cmd.Process.Pid
    // https://stackoverflow/a/44551450
    kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(pid))

    if e.config.Build.SendInterrupt {
        if err = kill.Run(); err != nil {
            return
        }
        time.Sleep(e.config.killDelay())
    }
    err = kill.Run()
    // Wait releases any resources associated with the Process.
    _, _ = cmd.Process.Wait()
    return pid, err
}

func (e *Engine) startCmd(cmd string) (*exec.Cmd, io.ReadCloser, io.ReadCloser, error) {
    var err error

    if !strings.Contains(cmd, ".exe") {
        e.runnerLog("CMD will not recognize non .exe file for execution, path: %s", cmd)
    }
    c := exec.Command("powershell", cmd)
    stderr, err := c.StderrPipe()
    if err != nil {
        return nil, nil, nil, err
    }
    stdout, err := c.StdoutPipe()
    if err != nil {
        return nil, nil, nil, err
    }

    c.Stdout = os.Stdout
    c.Stderr = os.Stderr

    err = c.Start()
    if err != nil {
        return nil, nil, nil, err
    }
    return c, stdout, stderr, err
}

You can find the repo here

They also reference another stack overflow post as to why they use this specific method.

发布评论

评论列表(0)

  1. 暂无评论