package engine import ( "bufio" "context" "errors" "io" "os/exec" "path/filepath" "runtime" "strings" "sync" "time" "socksrevivepc/internal/oscmd" ) type ManagedProcess struct { cmd *exec.Cmd name string logger *Logger mu sync.Mutex done chan error } func StartProcess(ctx context.Context, root, name, exe string, args []string, logger *Logger) (*ManagedProcess, error) { if strings.TrimSpace(exe) == "" { return nil, errors.New(name + " executable path is empty") } if !filepath.IsAbs(exe) { exe = filepath.Join(root, exe) } cmd := oscmd.CommandContext(ctx, exe, args...) cmd.Dir = root stdout, _ := cmd.StdoutPipe() stderr, _ := cmd.StderrPipe() p := &ManagedProcess{cmd: cmd, name: name, logger: logger, done: make(chan error, 1)} if err := cmd.Start(); err != nil { return nil, err } logger.Add("info", "%s started: %s %s", name, exe, strings.Join(args, " ")) go p.pipe(stdout, "info") go p.pipe(stderr, "warn") go func() { err := cmd.Wait() p.done <- err if err != nil { logger.Add("warn", "%s stopped: %v", name, err) } else { logger.Add("info", "%s stopped", name) } }() return p, nil } func (p *ManagedProcess) Exited() (bool, error) { if p == nil || p.done == nil { return true, nil } select { case err := <-p.done: return true, err default: return false, nil } } func (p *ManagedProcess) pipe(r io.Reader, level string) { s := bufio.NewScanner(r) for s.Scan() { line := strings.TrimSpace(s.Text()) if line != "" { p.logger.Add(level, "%s: %s", p.name, line) } } } func (p *ManagedProcess) Stop() { p.mu.Lock() defer p.mu.Unlock() if p == nil || p.cmd == nil || p.cmd.Process == nil { return } if runtime.GOOS == "windows" { _ = p.cmd.Process.Kill() } else { _ = p.cmd.Process.Signal(ioSignalInterrupt()) time.Sleep(500 * time.Millisecond) _ = p.cmd.Process.Kill() } }