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.
- The target process is not a console program, so I can't use
windows.GenerateConsoleCtrlEvent
. - 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:
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.
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 calltaskkill /t
, but it still returned the errorExit 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.
- The target process is not a console program, so I can't use
windows.GenerateConsoleCtrlEvent
. - 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:
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.
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 calltaskkill /t
, but it still returned the errorExit 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 | Show 2 more comments2 Answers
Reset to default 1In 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.
WM_CLOSE
, sendingWM_QUIT
if previous failed, sendingWM_DESTROY
if previous failed, and all are posted to process' main thread. If all fails, you callGenerateConsoleCtrlEvent
, and finallyTerminateProcess
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 thatTerminateProcess
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