I have a Go program that starts a Docker Compose process using exec.CommandContext. I've set up a context with cancellation and a signal handler to cancel the context when an interrupt signal (Ctrl+C) is received. The goal is to terminate the Docker Compose process and all its child processes quickly when the context is canceled.
However, I've noticed that it takes a significant amount of time to close all the child processes after canceling the context. I want to improve the speed at which these processes are terminated.
Here's a simplified version of my code:
package main
import (
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"sort"
"strconv"
"strings"
"syscall"
"go.uber.org/zap"
)
var cancel context.CancelFunc
func main() {
ctx := NewCtx()
username := os.Getenv("SUDO_USER")
fmt.Println("sdgnsaj")
keployAlias := "docker-compose -f /home/shivamsouravjha.linux/dockerEvent/docker-compose.yml up"
cmd := exec.CommandContext(ctx, "sh", "-c", keployAlias+" --build")
if username != "" {
// print all environment variables
// Run the command as the user who invoked sudo to preserve the user environment variables and PATH
cmd = exec.CommandContext(ctx, "sudo", "-E", "-u", os.Getenv("SUDO_USER"), "env", "PATH="+os.Getenv("PATH"), "sh", "-c", "docker-compose up")
}
cmd.Cancel = func() error {
return InterruptProcessTree(cmd, cmd.Process.Pid, syscall.SIGTERM)
}
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
fmt.Println("errro", err.Error())
}
fmt.Println("the pid", cmd.Process.Pid)
err = cmd.Wait()
if err != nil {
fmt.Println("asdasdsa")
}
}
func NewCtx() context.Context {
// Create a context that can be canceled
ctx, cancel := context.WithCancel(context.Background())
SetCancel(cancel)
// Set up a channel to listen for signals
sigs := make(chan os.Signal, 1)
// os.Interrupt is more portable than syscall.SIGINT
// there is no equivalent for syscall.SIGTERM in os.Signal
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
// Start a goroutine that will cancel the context when a signal is received
go func() {
<-sigs
fmt.Println("Signal received, canceling context...")
cancel()
}()
return ctx
}
func SetCancel(c context.CancelFunc) {
cancel = c
}
// InterruptProcessTree interrupts an entire process tree using the given signal
func InterruptProcessTree(cmd *exec.Cmd, ppid int, sig syscall.Signal) error {
// Find all descendant PIDs of the given PID & then signal them.
// Any shell doesn't signal its children when it receives a signal.
// Children may have their own process groups, so we need to signal them separately.
children, err := FindChildPIDs(ppid)
if err != nil {
return err
}
children = append(children, ppid)
sort.Slice(children, func(i, j int) bool { return children[i] > children[j] })
for _, pid := range children {
fmt.Println("this is child pid of parent", pid)
if cmd.ProcessState == nil {
err := syscall.Kill(pid, sig)
if err != nil {
fmt.Println("failed to send signal to process", zap.Int("pid", pid), zap.Error(err))
}
// time.Sleep(250 * time.Millisecond)
}
}
return nil
}
// findChildPIDs takes a parent PID and returns a slice of all descendant PIDs.
func FindChildPIDs(parentPID int) ([]int, error) {
var childPIDs []int
// Recursive helper function to find all descendants of a given PID.
var findDescendants func(int)
findDescendants = func(pid int) {
procDirs, err := os.ReadDir("/proc")
if err != nil {
return
}
for _, procDir := range procDirs {
if !procDir.IsDir() {
continue
}
childPid, err := strconv.Atoi(procDir.Name())
if err != nil {
continue
}
statusPath := filepath.Join("/proc", procDir.Name(), "status")
statusBytes, err := os.ReadFile(statusPath)
if err != nil {
continue
}
status := string(statusBytes)
for _, line := range strings.Split(status, "\n") {
if strings.HasPrefix(line, "PPid:") {
fields := strings.Fields(line)
if len(fields) == 2 {
ppid, err := strconv.Atoi(fields[1])
if err != nil {
break
}
if ppid == pid {
childPIDs = append(childPIDs, childPid)
findDescendants(childPid)
}
}
break
}
}
}
}
// Start the recursion with the initial parent PID.
findDescendants(parentPID)
return childPIDs, nil
}
this process then calls an image goose which runs similarly:-
func main() {
username := os.Getenv("SUDO_USER")
ctx := NewCtx()
fmt.Println("sdgnsaj")
keployAlias := "docker-compose -f /home/shivamsouravjha.linux/dockerEvent/a/docker-compose.yml up"
cmd := exec.CommandContext(ctx, "sh", "-c", keployAlias+" --build")
if username != "" {
// print all environment variables
// Run the command as the user who invoked sudo to preserve the user environment variables and PATH
cmd = exec.CommandContext(ctx, "sudo", "-E", "-u", os.Getenv("SUDO_USER"), "env", "PATH="+os.Getenv("PATH"), "sh", "-c", "docker-compose up")
}
// Set the cancel function for the command
cmd.Cancel = func() error {
return InterruptProcessTree(cmd, cmd.Process.Pid, syscall.SIGTERM)
}
// wait after sending the interrupt signal, before sending the kill signal
cmd.WaitDelay = 3 * time.Second
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Pdeathsig: syscall.SIGTERM,
}
// Set the output of the command
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
fmt.Println("errro", err.Error())
}
fmt.Println("the pid", cmd.Process.Pid)
go func() {
time.Sleep(10 * time.Second)
fmt.Println("the chils pids" + fmt.Sprint(FindChildPIDs(cmd.Process.Pid)))
}()
err = cmd.Wait()
if err != nil {
fmt.Println("errro")
}
}
The problem that I face is:Image
I've to send cancel signal again to close it, even when I do so the process takes time to stop.
Is there any way to end the process then only move ahead?
Meaning once a pid is successfully killed then only I proceed to kill other PID, I tried using process but nothing worked.
for _, pid := range children {
fmt.Println("this is child pid", pid)
if cmd.ProcessState == nil {
err := syscall.Kill(pid, sig)
if err != nil {
fmt.Println("failed to send signal to process", zap.Int("pid", pid), zap.Error(err))
}
// time.Sleep(250 * time.Millisecond)
}
}
I'm not inclined on using time.sleep. The other problem I face is the log "this is child pid" never appears.