package detach import ( "fmt" "os" "sync" "syscall" "time" ) const RingBufSize = 64 * 1024 // 64KB ring buffer per detached process // Process represents a process that the agent has detached from // but which continues running. The user can view its output and signal it. type Process struct { PID int Command string Started time.Time Exited bool ExitCode int Ring *RingBuffer Cmd *os.Process Done chan struct{} Mu sync.Mutex } // Info returns a plain-data snapshot of this process for external consumers. type InfoData struct { PID int Command string Started int64 // unix timestamp Exited bool ExitCode int } func (p *Process) Info() InfoData { p.Mu.Lock() defer p.Mu.Unlock() return InfoData{ PID: p.PID, Command: p.Command, Started: p.Started.Unix(), Exited: p.Exited, ExitCode: p.ExitCode, } } // Output returns the current contents of the ring buffer. func (p *Process) Output() string { p.Mu.Lock() defer p.Mu.Unlock() return p.Ring.String() } // Signal sends a signal to the detached process. func (p *Process) Signal(sig syscall.Signal) error { p.Mu.Lock() defer p.Mu.Unlock() if p.Exited { return fmt.Errorf("process %d already exited", p.PID) } if p.Cmd == nil { return fmt.Errorf("process %d has no handle", p.PID) } // Send to process group return syscall.Kill(-p.PID, sig) } // ringBuffer is a fixed-size circular byte buffer. type RingBuffer struct { buf []byte size int pos int full bool } func NewRingBuffer(size int) *RingBuffer { return &RingBuffer{buf: make([]byte, size), size: size} } // Write implements io.Writer. func (r *RingBuffer) Write(p []byte) (int, error) { n := len(p) if n >= r.size { // Data larger than buffer: just keep the tail copy(r.buf, p[n-r.size:]) r.pos = 0 r.full = true return n, nil } if r.pos+n <= r.size { copy(r.buf[r.pos:], p) } else { first := r.size - r.pos copy(r.buf[r.pos:], p[:first]) copy(r.buf, p[first:]) r.full = true } r.pos = (r.pos + n) % r.size if r.pos == 0 && n > 0 { r.full = true } return n, nil } // String returns the buffer contents in order. func (r *RingBuffer) String() string { if !r.full { return string(r.buf[:r.pos]) } // Buffer has wrapped: data from pos..end + 0..pos out := make([]byte, r.size) copy(out, r.buf[r.pos:]) copy(out[r.size-r.pos:], r.buf[:r.pos]) return string(out) }