This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-core/pkg/tools/execute/detach.go

118 lines
2.4 KiB
Go

package execute
import (
"fmt"
"os"
"sync"
"syscall"
"time"
)
const ringBufSize = 64 * 1024 // 64KB ring buffer per detached process
// DetachedProcess represents a process that the agent has detached from
// but which continues running. The user can view its output and signal it.
type DetachedProcess 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 DetachedInfoData struct {
PID int
Command string
Started int64 // unix timestamp
Exited bool
ExitCode int
}
func (p *DetachedProcess) Info() DetachedInfoData {
p.mu.Lock()
defer p.mu.Unlock()
return DetachedInfoData{
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 *DetachedProcess) Output() string {
p.mu.Lock()
defer p.mu.Unlock()
return p.ring.String()
}
// Signal sends a signal to the detached process.
func (p *DetachedProcess) 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)
}