feat(cybersecurity): auto-commit con 48 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
// PTYCaptureStream launches a command inside a pseudo-terminal (PTY) and
|
||||
// streams periodic snapshots of the accumulated output through a channel.
|
||||
// Unlike PTYCaptureIdle, which returns the full output at the end,
|
||||
// PTYCaptureStream emits the ENTIRE buffer accumulated so far on every
|
||||
// snapshotInterval tick — allowing callers to observe the terminal render
|
||||
// while the process is still running.
|
||||
//
|
||||
// The returned channel is closed when capture ends (idle/maxDur/ctx cancel).
|
||||
// The last value sent before closing is always a final snapshot of the
|
||||
// complete buffer, regardless of tick alignment.
|
||||
//
|
||||
// Callers MUST drain the channel or cancel ctx to avoid blocking the
|
||||
// internal goroutine. Error is returned only if pty.Start fails.
|
||||
func PTYCaptureStream(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
args []string,
|
||||
warmup time.Duration,
|
||||
inputs []string,
|
||||
stepDelay time.Duration,
|
||||
snapshotInterval time.Duration,
|
||||
idle time.Duration,
|
||||
maxDur time.Duration,
|
||||
) (<-chan string, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
|
||||
ptmx, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pty_capture_stream: pty.Start: %w", err)
|
||||
}
|
||||
|
||||
// Set a reasonable terminal size so TUIs render without truncating.
|
||||
if szErr := pty.Setsize(ptmx, &pty.Winsize{Rows: 40, Cols: 120}); szErr != nil {
|
||||
// Non-fatal: continue even if resize fails.
|
||||
_ = szErr
|
||||
}
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
buf bytes.Buffer
|
||||
lastByte = time.Now()
|
||||
)
|
||||
|
||||
// Reader goroutine: copy PTY output into buf and track last-byte time.
|
||||
readDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(readDone)
|
||||
tmp := make([]byte, 4096)
|
||||
for {
|
||||
n, rerr := ptmx.Read(tmp)
|
||||
if n > 0 {
|
||||
mu.Lock()
|
||||
buf.Write(tmp[:n])
|
||||
lastByte = time.Now()
|
||||
mu.Unlock()
|
||||
}
|
||||
if rerr != nil {
|
||||
// EIO/EOF is normal on Linux when the PTY master is closed
|
||||
// after the child exits. Not a real error.
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ch := make(chan string, 16)
|
||||
|
||||
// snapshot returns a copy of the current buffer contents.
|
||||
snapshot := func() string {
|
||||
mu.Lock()
|
||||
s := buf.String()
|
||||
mu.Unlock()
|
||||
return s
|
||||
}
|
||||
|
||||
// send emits a snapshot to ch, respecting ctx cancellation.
|
||||
send := func(s string) bool {
|
||||
select {
|
||||
case ch <- s:
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Conducting goroutine: handles warmup, inputs, periodic snapshots,
|
||||
// idle/maxDur detection, and shutdown.
|
||||
go func() {
|
||||
defer func() {
|
||||
// Shutdown: close PTY master, SIGTERM → SIGKILL, wait reader.
|
||||
_ = ptmx.Close()
|
||||
if cmd.Process != nil {
|
||||
_ = cmd.Process.Signal(syscall.SIGTERM)
|
||||
killTimer := time.NewTimer(2 * time.Second)
|
||||
waitCh := make(chan error, 1)
|
||||
go func() { waitCh <- cmd.Wait() }()
|
||||
select {
|
||||
case <-waitCh:
|
||||
// Process exited cleanly.
|
||||
case <-killTimer.C:
|
||||
_ = cmd.Process.Kill()
|
||||
<-waitCh
|
||||
}
|
||||
killTimer.Stop()
|
||||
}
|
||||
<-readDone
|
||||
// Final snapshot — always emitted so consumers get the complete state.
|
||||
send(snapshot())
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Wait for warmup so the TUI/CLI has time to initialize.
|
||||
select {
|
||||
case <-time.After(warmup):
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
// Send inputs one by one with stepDelay between them.
|
||||
for _, in := range inputs {
|
||||
if _, werr := ptmx.Write([]byte(in)); werr != nil {
|
||||
// PTY may have closed already; stop sending.
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-time.After(stepDelay):
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Main loop: emit snapshots on ticker, cut on idle or maxDur.
|
||||
ticker := time.NewTicker(snapshotInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Emit current accumulated snapshot.
|
||||
if !send(snapshot()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check termination conditions.
|
||||
mu.Lock()
|
||||
sinceLastByte := time.Since(lastByte)
|
||||
mu.Unlock()
|
||||
|
||||
elapsed := time.Since(start)
|
||||
if sinceLastByte >= idle || elapsed >= maxDur {
|
||||
return
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
Reference in New Issue
Block a user