package agent import ( "errors" "fmt" "os" "os/exec" "sync" "time" "github.com/kunchenguid/no-mistakes/internal/shellenv" ) type nativeAgentCommand struct { cmd *exec.Cmd stdout *nativeAgentPipe stderr *nativeAgentPipe waitCh chan error terminateOnce sync.Once closePipesOnce sync.Once pipeMu sync.Mutex remainingPipes int pipesDone chan struct{} } type nativeAgentPipe struct { file *os.File done func() doneOnce sync.Once } func (p *nativeAgentPipe) Read(b []byte) (int, error) { n, err := p.file.Read(b) if err != nil { p.markDone() } return n, err } func (p *nativeAgentPipe) Close() error { err := p.file.Close() p.markDone() return err } func (p *nativeAgentPipe) markDone() { p.doneOnce.Do(p.done) } func startNativeAgentCommand(cmd *exec.Cmd) (*nativeAgentCommand, error) { stdoutR, stdoutW, err := os.Pipe() if err != nil { return nil, fmt.Errorf("stdout pipe: %w", err) } stderrR, stderrW, err := os.Pipe() if err != nil { _ = stdoutR.Close() _ = stdoutW.Close() return nil, fmt.Errorf("stderr pipe: %w", err) } cmd.Stdout = stdoutW cmd.Stderr = stderrW if err := shellenv.StartShellCommand(cmd); err != nil { _ = stdoutR.Close() _ = stdoutW.Close() _ = stderrR.Close() _ = stderrW.Close() return nil, err } _ = stdoutW.Close() _ = stderrW.Close() started := &nativeAgentCommand{ cmd: cmd, waitCh: make(chan error, 1), remainingPipes: 2, pipesDone: make(chan struct{}), } started.stdout = &nativeAgentPipe{file: stdoutR, done: started.markPipeDone} started.stderr = &nativeAgentPipe{file: stderrR, done: started.markPipeDone} go func() { err := cmd.Wait() started.terminate() started.waitCh <- started.waitForPipes(err) }() return started, nil } func (c *nativeAgentCommand) markPipeDone() { c.pipeMu.Lock() defer c.pipeMu.Unlock() c.remainingPipes-- if c.remainingPipes == 0 { close(c.pipesDone) } } func (c *nativeAgentCommand) pid() int { if c == nil || c.cmd == nil || c.cmd.Process == nil { return 0 } return c.cmd.Process.Pid } func (c *nativeAgentCommand) waitForPipes(waitErr error) error { if c.cmd.WaitDelay <= 0 { <-c.pipesDone return waitErr } timer := time.NewTimer(c.cmd.WaitDelay) defer timer.Stop() select { case <-c.pipesDone: return waitErr case <-timer.C: c.closePipes() if waitErr == nil { return exec.ErrWaitDelay } return waitErr } } func (c *nativeAgentCommand) terminate() { c.terminateOnce.Do(func() { shellenv.TerminateShellCommandGroup(c.cmd) }) } func (c *nativeAgentCommand) waitAfterParseError(parseErr error) error { c.terminate() c.closePipes() waitErr := c.wait() if errors.Is(waitErr, exec.ErrWaitDelay) { return waitErr } return parseErr } func (c *nativeAgentCommand) wait() error { return <-c.waitCh } func (c *nativeAgentCommand) closePipes() { c.closePipesOnce.Do(func() { _ = c.stdout.Close() _ = c.stderr.Close() }) }