Files
fn_registry/functions/core/subprocess_stream.go
T
egutierrez a802f59f55 chore: auto-commit (95 archivos)
- cmd/fn/doctor.go
- cmd/fn/main.go
- cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt
- cpp/apps/primitives_gallery/playground/tables/data_table.cpp
- cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp
- cpp/apps/primitives_gallery/playground/tables/data_table_logic.h
- cpp/apps/primitives_gallery/playground/tables/self_test.cpp
- cpp/apps/primitives_gallery/playground/tables/tql.cpp
- cpp/apps/primitives_gallery/playground/tables/viz.cpp
- cpp/apps/primitives_gallery/playground/tables/viz.h
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:50:34 +02:00

156 lines
3.8 KiB
Go

package core
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"sync"
"syscall"
"time"
)
// StreamEvent es una linea capturada de stdout o stderr del subproceso.
type StreamEvent struct {
Stream string // "stdout" | "stderr"
Line string // sin trailing newline
Time time.Time // timestamp de recepcion
}
// StreamResult es el resultado final del subproceso, enviado por el canal de
// resultados cuando ambos pipes han llegado a EOF y el proceso ha terminado.
type StreamResult struct {
ExitCode int
Err error
DurationMs int64
}
// SubprocessStream lanza name con args como subproceso y retorna dos canales:
// - events: recibe StreamEvent (linea de stdout/stderr) hasta EOF de ambos pipes.
// - result: recibe exactamente un StreamResult cuando el proceso termina.
//
// env se concatena con os.Environ(). stdin puede ser nil.
//
// Cancelar ctx envia SIGTERM al proceso; si no termina en 2 segundos, SIGKILL.
// El caller DEBE consumir events hasta que se cierre o cancelar ctx para evitar
// bloquear las goroutines internas.
func SubprocessStream(
ctx context.Context,
name string,
args []string,
env []string,
stdin io.Reader,
) (<-chan StreamEvent, <-chan StreamResult) {
events := make(chan StreamEvent, 64)
results := make(chan StreamResult, 1)
go func() {
defer close(events)
defer close(results)
start := time.Now()
cmd := exec.CommandContext(ctx, name, args...)
// Entorno: base + extra
if len(env) > 0 {
cmd.Env = append(os.Environ(), env...)
}
if stdin != nil {
cmd.Stdin = stdin
}
// Process group propio para matar hijos al recibir SIGTERM/SIGKILL
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
results <- StreamResult{ExitCode: -1, Err: fmt.Errorf("stdout pipe: %w", err), DurationMs: 0}
return
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
results <- StreamResult{ExitCode: -1, Err: fmt.Errorf("stderr pipe: %w", err), DurationMs: 0}
return
}
if err := cmd.Start(); err != nil {
results <- StreamResult{ExitCode: -1, Err: fmt.Errorf("start: %w", err), DurationMs: 0}
return
}
// Goroutine de supervision de ctx: SIGTERM → grace 2s → SIGKILL
ctxDone := make(chan struct{})
go func() {
select {
case <-ctx.Done():
if cmd.Process != nil {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM)
timer := time.NewTimer(2 * time.Second)
defer timer.Stop()
select {
case <-timer.C:
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
case <-ctxDone:
}
}
case <-ctxDone:
}
}()
send := func(stream, line string) {
ev := StreamEvent{Stream: stream, Line: line, Time: time.Now()}
select {
case events <- ev:
case <-ctx.Done():
}
}
// Leer stdout y stderr concurrentemente
const bufSize = 1024 * 1024 // 1 MB para lineas largas (sd-cli progress, etc.)
var wg sync.WaitGroup
scanPipe := func(r io.Reader, stream string) {
defer wg.Done()
sc := bufio.NewScanner(r)
sc.Buffer(make([]byte, bufSize), bufSize)
for sc.Scan() {
send(stream, sc.Text())
}
}
wg.Add(2)
go scanPipe(stdoutPipe, "stdout")
go scanPipe(stderrPipe, "stderr")
wg.Wait()
close(ctxDone) // señal al supervisor de ctx para que pare
exitCode := 0
var waitErr error
if err := cmd.Wait(); err != nil {
waitErr = err
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
waitErr = nil // exit code no-cero no es un error de spawn
}
}
// Si el contexto fue cancelado, reportar como error de cancelacion
if ctx.Err() != nil && waitErr == nil {
waitErr = ctx.Err()
}
results <- StreamResult{
ExitCode: exitCode,
Err: waitErr,
DurationMs: time.Since(start).Milliseconds(),
}
}()
return events, results
}