diff --git a/server/README.md b/server/README.md index d656b1713..7865835e3 100644 --- a/server/README.md +++ b/server/README.md @@ -74,6 +74,7 @@ export OUTPUT_DIR=/tmp/recordings - **YAML Spec**: `GET /spec.yaml` - **JSON Spec**: `GET /spec.json` +- **Process execution guide**: [`docs/processes.md`](docs/processes.md) - **Browser REPL guide**: [`docs/repl.md`](docs/repl.md) ## 🔧 Development diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index a7333fbe7..bb223fc8c 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -74,8 +74,9 @@ type ApiService struct { watches map[string]*fsWatch // Process management - procMu sync.RWMutex - procs map[string]*processHandle + procMu sync.RWMutex + procs map[string]*processHandle + processOutput *processOutputBudget // Neko authenticated client nekoAuthClient *nekoclient.AuthClient @@ -195,6 +196,7 @@ func New( s2Storage S2Storage, egressPolicy *egresspolicy.State, webauthnHook *webauthnhook.Hook, + processOutputConfig ProcessOutputConfig, ) (*ApiService, error) { switch { case recordManager == nil: @@ -215,6 +217,10 @@ func New( return nil, fmt.Errorf("webauthnHook cannot be nil") } + outputBudget, err := newProcessOutputBudget(processOutputConfig) + if err != nil { + return nil, err + } screenshotEnabled := func() bool { return telemetrySession.CategoryEnabled(events.Screenshot) } mon := cdpmonitor.New(upstreamMgr, telemetrySession.Publish, displayNum, slog.Default(), screenshotEnabled) _ = mon.SetTelemetry(false) @@ -226,6 +232,7 @@ func New( defaultRecorderID: "default", watches: make(map[string]*fsWatch), procs: make(map[string]*processHandle), + processOutput: outputBudget, upstreamMgr: upstreamMgr, stz: stz, nekoAuthClient: nekoAuthClient, diff --git a/server/cmd/api/api/api_test.go b/server/cmd/api/api/api_test.go index 98278be82..cbf835a2a 100644 --- a/server/cmd/api/api/api_test.go +++ b/server/cmd/api/api/api_test.go @@ -433,7 +433,7 @@ func testWebAuthnWithMetro(t *testing.T, metroURL string) *webauthnhook.Hook { func newSvc(t *testing.T, mgr recorder.RecordManager) (*ApiService, error) { t.Helper() ts, es := newTelemetrySession(t) - return New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil, nil, testEgressPolicy(t), testWebAuthn(t)) + return New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil, nil, testEgressPolicy(t), testWebAuthn(t), ProcessOutputConfig{}) } func TestApiService_PatchChromiumFlags(t *testing.T) { diff --git a/server/cmd/api/api/display_test.go b/server/cmd/api/api/display_test.go index 80ccf8ad9..f0eb93f4c 100644 --- a/server/cmd/api/api/display_test.go +++ b/server/cmd/api/api/display_test.go @@ -39,7 +39,7 @@ func testFFmpegFactory(t *testing.T, tempDir string) recorder.FFmpegRecorderFact func newTestServiceWithFactory(t *testing.T, mgr recorder.RecordManager, factory recorder.FFmpegRecorderFactory) *ApiService { t.Helper() ts, es := newTelemetrySession(t) - svc, err := New(mgr, factory, newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil, nil, testEgressPolicy(t), testWebAuthn(t)) + svc, err := New(mgr, factory, newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil, nil, testEgressPolicy(t), testWebAuthn(t), ProcessOutputConfig{}) require.NoError(t, err) return svc } diff --git a/server/cmd/api/api/process.go b/server/cmd/api/api/process.go index 83f25a8ea..4c36bf36f 100644 --- a/server/cmd/api/api/process.go +++ b/server/cmd/api/api/process.go @@ -1,7 +1,6 @@ package api import ( - "bufio" "bytes" "context" "encoding/base64" @@ -22,7 +21,6 @@ import ( "github.com/coder/websocket" "github.com/creack/pty" - "github.com/google/uuid" "github.com/kernel/kernel-images/server/lib/logger" oapi "github.com/kernel/kernel-images/server/lib/oapi" "github.com/kernel/kernel-images/server/lib/ptyio" @@ -31,40 +29,26 @@ import ( ) type processHandle struct { - id openapi_types.UUID - pid int - cmd *exec.Cmd - started time.Time - exitCode *int - stdin io.WriteCloser - stdout io.ReadCloser - stderr io.ReadCloser - ptyFile *os.File - isTTY bool - outCh chan oapi.ProcessStreamEvent - doneCh chan struct{} - mu sync.RWMutex + id openapi_types.UUID + pid int + cmd *exec.Cmd + started time.Time + exitCode *int + stdin io.WriteCloser + stdout io.ReadCloser + stderr io.ReadCloser + ptyFile *os.File + isTTY bool + output *processOutput + reason string + truncated bool + durationMs int + doneCh chan struct{} + mu sync.RWMutex // attachActive guards PTY attach sessions; only one client may be attached at a time. attachActive bool } -func (h *processHandle) state() string { - h.mu.RLock() - defer h.mu.RUnlock() - if h.exitCode != nil { - return "exited" - } - return "running" -} - -func (h *processHandle) setExited(code int) { - h.mu.Lock() - if h.exitCode == nil { - h.exitCode = &code - } - h.mu.Unlock() -} - func isUserCmdError(err error) bool { return errors.Is(err, exec.ErrNotFound) || errors.Is(err, exec.ErrDot) || @@ -83,6 +67,9 @@ func buildCmd(body *oapi.ProcessExecRequest) (*exec.Cmd, error) { if body.Args != nil { args = append(args, (*body.Args)...) } + if body.TimeoutSec != nil && (*body.TimeoutSec < 0 || int64(*body.TimeoutSec) > int64((1<<63-1)/time.Second)) { + return nil, errors.New("timeout_sec must be a nonnegative duration in seconds") + } cmd := exec.Command(body.Command, args...) if body.Cwd != nil && *body.Cwd != "" { cmd.Dir = *body.Cwd @@ -166,106 +153,89 @@ func buildCmd(body *oapi.ProcessExecRequest) (*exec.Cmd, error) { // Execute a command synchronously // (POST /process/exec) func (s *ApiService) ProcessExec(ctx context.Context, request oapi.ProcessExecRequestObject) (oapi.ProcessExecResponseObject, error) { - log := logger.FromContext(ctx) - if request.Body == nil { - return oapi.ProcessExec400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body required"}}, nil - } - - cmd, err := buildCmd((*oapi.ProcessExecRequest)(request.Body)) + cmd, err := buildCmd(request.Body) if err != nil { return oapi.ProcessExec400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil } - - var stdoutBuf, stderrBuf bytes.Buffer - cmd.Stdout = &stdoutBuf - cmd.Stderr = &stderrBuf - - // Handle timeout if provided - start := time.Now() - var cancel context.CancelFunc - if request.Body.TimeoutSec != nil && *request.Body.TimeoutSec > 0 { - ctx, cancel = context.WithTimeout(ctx, time.Duration(*request.Body.TimeoutSec)*time.Second) - defer cancel() - } - if err := cmd.Start(); err != nil { + h, err := s.startProcess(ctx, cmd, false, false, request.Body.TimeoutSec) + if err != nil { if isUserCmdError(err) { return oapi.ProcessExec400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil } - log.Error("failed to start process", "err", err) + logger.FromContext(ctx).Error("failed to start process", "err", err) return oapi.ProcessExec500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil } - - done := make(chan error, 1) - go func() { done <- cmd.Wait() }() - select { - case <-ctx.Done(): - _ = cmd.Process.Kill() - <-done // ensure wait returns - return oapi.ProcessExec500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "process timed out"}}, nil - case err := <-done: - // proceed - _ = err - } - durationMs := int(time.Since(start) / time.Millisecond) - exitCode := 0 - if cmd.ProcessState != nil { - if status, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok { - exitCode = status.ExitStatus() + defer s.releaseProcess(h) + <-h.doneCh + var stdout, stderr bytes.Buffer + var offset int64 + for { + stream, data, next, err := h.output.next(context.Background(), offset) + if errors.Is(err, io.EOF) { + break + } + if err != nil { + h.outputFailed(err) + break } + if stream == 0 { + stdout.Write(data) + } else { + stderr.Write(data) + } + offset = next } + h.mu.RLock() + code, reason, truncated, duration := *h.exitCode, h.reason, h.truncated, h.durationMs + h.mu.RUnlock() + out, errOut := base64.StdEncoding.EncodeToString(stdout.Bytes()), base64.StdEncoding.EncodeToString(stderr.Bytes()) + if reason != "" { + return oapi.ProcessExec422JSONResponse{ + Code: oapi.ProcessTerminationReason(reason), Message: processTerminationMessage(reason), + ExitCode: &code, StdoutB64: &out, StderrB64: &errOut, DurationMs: &duration, OutputTruncated: &truncated, + }, nil + } + return oapi.ProcessExec200JSONResponse{ExitCode: &code, StdoutB64: &out, StderrB64: &errOut, DurationMs: &duration, OutputTruncated: &truncated}, nil +} - resp := oapi.ProcessExec200JSONResponse{ - ExitCode: &exitCode, - StdoutB64: ptrOf(base64.StdEncoding.EncodeToString(stdoutBuf.Bytes())), - StderrB64: ptrOf(base64.StdEncoding.EncodeToString(stderrBuf.Bytes())), - DurationMs: &durationMs, +func processTerminationMessage(reason string) string { + switch reason { + case "process_timeout": + return "process timed out" + case "output_limit": + return "process output limit exceeded" + case "process_canceled": + return "process canceled" + default: + return "failed to capture process output" } - return resp, nil } -// Execute a command asynchronously -// (POST /process/spawn) +// Execute a command asynchronously. func (s *ApiService) ProcessSpawn(ctx context.Context, request oapi.ProcessSpawnRequestObject) (oapi.ProcessSpawnResponseObject, error) { - log := logger.FromContext(ctx) if request.Body == nil { return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body required"}}, nil } - // Build from ProcessExecRequest shape + body := request.Body execReq := oapi.ProcessExecRequest{ - Command: request.Body.Command, - Args: request.Body.Args, - Cwd: request.Body.Cwd, - Env: request.Body.Env, - AsUser: request.Body.AsUser, - AsRoot: request.Body.AsRoot, - TimeoutSec: request.Body.TimeoutSec, + Command: body.Command, Args: body.Args, Cwd: body.Cwd, Env: body.Env, + AsRoot: body.AsRoot, AsUser: body.AsUser, TimeoutSec: body.TimeoutSec, } cmd, err := buildCmd(&execReq) if err != nil { return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil } - - var ( - stdout io.ReadCloser - stderr io.ReadCloser - stdin io.WriteCloser - ptyFile *os.File - isTTY bool - ) - // PTY mode when requested - if request.Body.AllocateTty != nil && *request.Body.AllocateTty { - // Validate rows/cols before starting the process - const maxUint16 = 65535 - if request.Body.Rows != nil && *request.Body.Rows > maxUint16 { + tty := body.AllocateTty != nil && *body.AllocateTty + if tty { + if body.Rows != nil && *body.Rows > 65535 { return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "rows must be <= 65535"}}, nil } - if request.Body.Cols != nil && *request.Body.Cols > maxUint16 { + if body.Cols != nil && *body.Cols > 65535 { return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "cols must be <= 65535"}}, nil } - // Ensure TERM and initial size env hasTerm := false - for _, kv := range cmd.Env { - if strings.HasPrefix(kv, "TERM=") { + for _, env := range cmd.Env { + if strings.HasPrefix(env, "TERM=") { hasTerm = true break } @@ -273,188 +243,20 @@ func (s *ApiService) ProcessSpawn(ctx context.Context, request oapi.ProcessSpawn if !hasTerm { cmd.Env = append(cmd.Env, "TERM=xterm-256color") } - // Start with PTY - var errStart error - ptyFile, errStart = pty.Start(cmd) - if errStart != nil { - if isUserCmdError(errStart) { - return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: errStart.Error()}}, nil - } - log.Error("failed to start PTY process", "err", errStart) - return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil - } - // Set initial size if provided - var rows, cols uint16 - if request.Body.Rows != nil && *request.Body.Rows > 0 { - rows = uint16(*request.Body.Rows) - } - if request.Body.Cols != nil && *request.Body.Cols > 0 { - cols = uint16(*request.Body.Cols) - } - if rows > 0 && cols > 0 { - _ = pty.Setsize(ptyFile, &pty.Winsize{Rows: rows, Cols: cols}) - } - stdout = ptyFile - stdin = ptyFile - isTTY = true - } else { - stdout, err = cmd.StdoutPipe() - if err != nil { - return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stdout"}}, nil - } - stderr, err = cmd.StderrPipe() - if err != nil { - return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stderr"}}, nil - } - stdin, err = cmd.StdinPipe() - if err != nil { - return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to open stdin"}}, nil - } - if err := cmd.Start(); err != nil { - if isUserCmdError(err) { - return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil - } - log.Error("failed to start process", "err", err) - return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil - } - } - - // Disable scale-to-zero while the process is running. - // Track success so we only re-enable if disable succeeded. - stzDisabled := false - if err := s.stz.Disable(ctx); err != nil { - log.Error("failed to disable scale-to-zero", "err", err) - } else { - stzDisabled = true - } - - id := openapi_types.UUID(uuid.New()) - h := &processHandle{ - id: id, - pid: cmd.Process.Pid, - cmd: cmd, - started: time.Now(), - stdin: stdin, - stdout: stdout, - stderr: stderr, - ptyFile: ptyFile, - isTTY: isTTY, - outCh: make(chan oapi.ProcessStreamEvent, 256), - doneCh: make(chan struct{}), - } - - // Store handle - s.procMu.Lock() - if s.procs == nil { - s.procs = make(map[string]*processHandle) - } - s.procs[id.String()] = h - s.procMu.Unlock() - - // Reader goroutines - // In PTY mode, do NOT read from the PTY here to avoid competing with the /attach endpoint. - // In non‑PTY mode, stdout and stderr are separate pipes, so we run two readers and tag chunks accordingly. - if !isTTY { - go func() { - reader := bufio.NewReader(stdout) - buf := make([]byte, 4096) - for { - n, err := reader.Read(buf) - if n > 0 { - data := base64.StdEncoding.EncodeToString(buf[:n]) - stream := oapi.ProcessStreamEventStream("stdout") - h.outCh <- oapi.ProcessStreamEvent{Stream: &stream, DataB64: &data} - } - if err != nil { - break - } - } - }() - go func() { - reader := bufio.NewReader(stderr) - buf := make([]byte, 4096) - for { - n, err := reader.Read(buf) - if n > 0 { - data := base64.StdEncoding.EncodeToString(buf[:n]) - stream := oapi.ProcessStreamEventStream("stderr") - h.outCh <- oapi.ProcessStreamEvent{Stream: &stream, DataB64: &data} - } - if err != nil { - break - } - } - }() } - - // Waiter goroutine - use context without cancel since HTTP request may complete - // before the process exits - stzCtx := context.WithoutCancel(ctx) - go func(stzWasDisabled bool) { - err := cmd.Wait() - code := 0 - if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { - code = status.ExitStatus() - } - } - } else if cmd.ProcessState != nil { - if status, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok { - code = status.ExitStatus() - } - } - h.setExited(code) - // Ensure all related FDs are closed to avoid leaking descriptors. - // In PTY mode, close the PTY master; in non-PTY mode, close individual pipes. - if h.isTTY { - if h.ptyFile != nil { - _ = h.ptyFile.Close() - } - } else { - if h.stdin != nil { - _ = h.stdin.Close() - } - if h.stdout != nil { - _ = h.stdout.Close() - } - if h.stderr != nil { - _ = h.stderr.Close() - } - } - - // Re-enable scale-to-zero now that the process has exited, - // but only if we successfully disabled it earlier - if stzWasDisabled { - if err := s.stz.Enable(stzCtx); err != nil { - log.Error("failed to enable scale-to-zero", "err", err) - } + h, err := s.startProcess(context.WithoutCancel(ctx), cmd, tty, true, body.TimeoutSec) + if err != nil { + if isUserCmdError(err) { + return oapi.ProcessSpawn400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil } - - // Send exit event - evt := oapi.ProcessStreamEventEvent("exit") - h.outCh <- oapi.ProcessStreamEvent{Event: &evt, ExitCode: &code} - close(h.doneCh) - // Retain the handle for a short period so clients can observe the - // final "exited" status via ProcessStatus before it disappears. - // This avoids races where the process exits immediately after spawn - // and status polling returns 404. - retention := 10 * time.Second - go func(procID string) { - time.Sleep(retention) - s.procMu.Lock() - delete(s.procs, procID) - s.procMu.Unlock() - }(id.String()) - }(stzDisabled) - - startedAt := h.started - pid := h.pid - return oapi.ProcessSpawn200JSONResponse{ - ProcessId: &id, - Pid: &pid, - StartedAt: &startedAt, - }, nil + logger.FromContext(ctx).Error("failed to start process", "err", err) + return oapi.ProcessSpawn500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start process"}}, nil + } + if tty && body.Rows != nil && *body.Rows > 0 && body.Cols != nil && *body.Cols > 0 { + _ = pty.Setsize(h.ptyFile, &pty.Winsize{Rows: uint16(*body.Rows), Cols: uint16(*body.Cols)}) + } + go s.retainProcess(h) + return oapi.ProcessSpawn200JSONResponse{ProcessId: &h.id, Pid: &h.pid, StartedAt: &h.started}, nil } // Send signal to process @@ -485,7 +287,9 @@ func (s *ApiService) ProcessKill(ctx context.Context, request oapi.ProcessKillRe default: return oapi.ProcessKill400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid signal"}}, nil } - if h.cmd.Process == nil { + h.mu.Lock() + defer h.mu.Unlock() + if h.exitCode != nil { return oapi.ProcessKill404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "process not running"}}, nil } if err := h.cmd.Process.Signal(sig); err != nil { @@ -505,19 +309,20 @@ func (s *ApiService) ProcessStatus(ctx context.Context, request oapi.ProcessStat if !ok { return oapi.ProcessStatus404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "process not found"}}, nil } - stateStr := h.state() - state := oapi.ProcessStatusState(stateStr) + state := oapi.ProcessStatusState("running") var exitCode *int h.mu.RLock() if h.exitCode != nil { + state = "exited" v := *h.exitCode exitCode = &v } pid := h.pid + reason, truncated := h.reason, h.truncated h.mu.RUnlock() // Best-effort memory stats via /proc var memBytes int - if stateStr == "running" && pid > 0 { + if state == "running" && pid > 0 { if b, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/status"); err == nil { // Parse VmRSS: 123 kB for _, line := range strings.Split(string(b), "\n") { @@ -535,7 +340,10 @@ func (s *ApiService) ProcessStatus(ctx context.Context, request oapi.ProcessStat } } cpuPct := float32(0) - resp := oapi.ProcessStatus200JSONResponse{State: &state, ExitCode: exitCode, CpuPct: &cpuPct} + resp := oapi.ProcessStatus200JSONResponse{State: &state, ExitCode: exitCode, CpuPct: &cpuPct, OutputTruncated: &truncated} + if reason != "" { + resp.TerminationReason = ptrOf(oapi.ProcessTerminationReason(reason)) + } if memBytes > 0 { resp.MemBytes = ptrOf(memBytes) } @@ -569,45 +377,117 @@ func (s *ApiService) ProcessStdin(ctx context.Context, request oapi.ProcessStdin // Stream process stdout/stderr (SSE) // (GET /process/{process_id}/stdout/stream) func (s *ApiService) ProcessStdoutStream(ctx context.Context, request oapi.ProcessStdoutStreamRequestObject) (oapi.ProcessStdoutStreamResponseObject, error) { - log := logger.FromContext(ctx) - id := request.ProcessId s.procMu.RLock() - h, ok := s.procs[id] + h, ok := s.procs[request.ProcessId] s.procMu.RUnlock() if !ok { return oapi.ProcessStdoutStream404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "process not found"}}, nil } + var offset int64 + var err error + if request.Params.LastEventID != nil { + offset, err = strconv.ParseInt(*request.Params.LastEventID, 10, 64) + } else if request.Params.Offset != nil { + offset = *request.Params.Offset + } + if err != nil || h.output.validateOffset(offset) != nil { + return oapi.ProcessStdoutStream400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid output offset"}}, nil + } + return processStreamResponse{ + ctx: ctx, process: h, offset: offset, + replay: request.Params.Offset != nil || request.Params.LastEventID != nil, + }, nil +} + +type processStreamResponse struct { + ctx context.Context + process *processHandle + offset int64 + replay bool +} - pr, pw := io.Pipe() +func (r processStreamResponse) VisitProcessStdoutStreamResponse(w http.ResponseWriter) error { + finished, stopped := make(chan struct{}), make(chan struct{}) + controller := http.NewResponseController(w) go func() { - defer pw.Close() - for { - select { - case evt := <-h.outCh: - // Write SSE: data: \n\n - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(evt); err != nil { - log.Error("failed to marshal event", "err", err) - return - } - line := bytes.TrimRight(buf.Bytes(), "\n") - if _, err := pw.Write([]byte("data: ")); err != nil { - return - } - if _, err := pw.Write(line); err != nil { - return - } - if _, err := pw.Write([]byte("\n\n")); err != nil { - return - } - case <-h.doneCh: - return - } + defer close(stopped) + select { + case <-r.process.output.closed: + case <-r.ctx.Done(): + case <-finished: + return } + // Interrupt a network write already in progress on expiry or disconnect. + _ = controller.SetWriteDeadline(time.Now()) }() + defer func() { + close(finished) + <-stopped + }() + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("X-SSE-Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := controller.Flush(); err != nil { + return err + } + for { + var stream byte + var data []byte + var next int64 + var err error + if r.replay { + stream, data, next, err = r.process.output.next(r.ctx, r.offset) + } else { + stream, data, next, err = r.process.output.nextUnread(r.ctx) + } + if errors.Is(err, errProcessOutputConsumed) { + return nil + } + var event oapi.ProcessStreamEvent + if errors.Is(err, io.EOF) { + h := r.process + <-h.doneCh + h.mu.RLock() + code, reason, truncated := *h.exitCode, h.reason, h.truncated + h.mu.RUnlock() + event = oapi.ProcessStreamEvent{Event: ptrOf(oapi.Exit), ExitCode: &code, OutputTruncated: &truncated} + if reason != "" { + event.TerminationReason = ptrOf(oapi.ProcessTerminationReason(reason)) + } + } else if err != nil { + return err + } else { + source := oapi.ProcessStreamEventStream("stdout") + if stream == 1 { + source = "stderr" + } + event = oapi.ProcessStreamEvent{Stream: &source, DataB64: ptrOf(base64.StdEncoding.EncodeToString(data)), Offset: &next} + } + if err := writeProcessEvent(w, event); err != nil { + return err + } + if err := controller.Flush(); err != nil { + return err + } + if event.Event != nil { + return nil + } + r.offset = next + } +} - headers := oapi.ProcessStdoutStream200ResponseHeaders{XSSEContentType: "application/json"} - return oapi.ProcessStdoutStream200TexteventStreamResponse{Body: pr, Headers: headers, ContentLength: 0}, nil +func writeProcessEvent(w io.Writer, event oapi.ProcessStreamEvent) error { + data, err := json.Marshal(event) + if err != nil { + return err + } + if event.Offset != nil { + if _, err := fmt.Fprintf(w, "id: %d\n", *event.Offset); err != nil { + return err + } + } + _, err = fmt.Fprintf(w, "data: %s\n\n", data) + return err } func ptrOf[T any](v T) *T { return &v } diff --git a/server/cmd/api/api/process_lifecycle_test.go b/server/cmd/api/api/process_lifecycle_test.go new file mode 100644 index 000000000..e42951593 --- /dev/null +++ b/server/cmd/api/api/process_lifecycle_test.go @@ -0,0 +1,615 @@ +package api + +import ( + "bufio" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + "testing" + "time" + + chiMiddleware "github.com/go-chi/chi/v5/middleware" + oapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/kernel/kernel-images/server/lib/recorder" + "github.com/stretchr/testify/require" +) + +func newProcessTestService(t *testing.T, config ProcessOutputConfig) *ApiService { + t.Helper() + budget, err := newProcessOutputBudget(config) + require.NoError(t, err) + ctx, cancel := context.WithCancel(context.Background()) + svc := &ApiService{processOutput: budget, lifecycleCtx: ctx, lifecycleCancel: cancel} + t.Cleanup(func() { + cancel() + svc.procMu.RLock() + handles := make([]*processHandle, 0, len(svc.procs)) + for _, h := range svc.procs { + handles = append(handles, h) + } + svc.procMu.RUnlock() + for _, h := range handles { + select { + case <-h.doneCh: + svc.releaseProcess(h) + case <-time.After(5 * time.Second): + t.Error("process did not stop during cleanup") + } + } + }) + return svc +} + +func spawnProcess(t *testing.T, svc *ApiService, command string, args ...string) oapi.ProcessSpawn200JSONResponse { + t.Helper() + resp, err := svc.ProcessSpawn(context.Background(), oapi.ProcessSpawnRequestObject{Body: &oapi.ProcessSpawnRequest{Command: command, Args: &args}}) + require.NoError(t, err) + spawn, ok := resp.(oapi.ProcessSpawn200JSONResponse) + require.True(t, ok, "unexpected spawn response: %T", resp) + + return spawn +} + +func processStream(t *testing.T, svc *ApiService, spawn oapi.ProcessSpawn200JSONResponse, params ...oapi.ProcessStdoutStreamParams) io.ReadCloser { + t.Helper() + server := httptest.NewServer(oapi.Handler(oapi.NewStrictHandler(svc, nil))) + t.Cleanup(server.Close) + client, err := oapi.NewClient(server.URL, oapi.WithHTTPClient(&http.Client{Timeout: 20 * time.Second})) + require.NoError(t, err) + var p oapi.ProcessStdoutStreamParams + if len(params) > 0 { + p = params[0] + } + resp, err := client.ProcessStdoutStream(context.Background(), spawn.ProcessId.String(), &p) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + t.Cleanup(func() { _ = resp.Body.Close() }) + return resp.Body +} + +func collectProcessStream(t *testing.T, body io.Reader) (string, []oapi.ProcessStreamEvent) { + t.Helper() + var output strings.Builder + events := make([]oapi.ProcessStreamEvent, 0) + scanner := bufio.NewScanner(body) + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "data: ") { + continue + } + var event oapi.ProcessStreamEvent + require.NoError(t, json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &event)) + events = append(events, event) + if event.DataB64 != nil { + data, err := base64.StdEncoding.DecodeString(*event.DataB64) + require.NoError(t, err) + output.Write(data) + } + } + require.NoError(t, scanner.Err()) + return output.String(), events +} + +func waitProcessExit(t *testing.T, svc *ApiService, spawn oapi.ProcessSpawn200JSONResponse) { + t.Helper() + require.Eventually(t, func() bool { + resp, err := svc.ProcessStatus(context.Background(), oapi.ProcessStatusRequestObject{ProcessId: spawn.ProcessId.String()}) + if err != nil { + return false + } + status, ok := resp.(oapi.ProcessStatus200JSONResponse) + return ok && status.State != nil && *status.State == "exited" + }, 5*time.Second, 10*time.Millisecond, "process did not exit without a stream reader") +} + +func requireProcessExitEvent(t *testing.T, events []oapi.ProcessStreamEvent, code int) { + t.Helper() + require.NotEmpty(t, events) + last := events[len(events)-1] + require.NotNil(t, last.Event) + require.Equal(t, "exit", string(*last.Event)) + require.Equal(t, code, *last.ExitCode) + for _, event := range events[:len(events)-1] { + require.Nil(t, event.Event) + } +} + +func TestProcessOutputWithoutReaderAndReplay(t *testing.T) { + svc := newProcessTestService(t, ProcessOutputConfig{}) + spawn := spawnProcess(t, svc, "head", "-c", "16777216", "/dev/zero") + waitProcessExit(t, svc, spawn) + for i := 0; i < 2; i++ { + output, events := collectProcessStream(t, processStream(t, svc, spawn, oapi.ProcessStdoutStreamParams{Offset: ptrOf(int64(0))})) + require.Equal(t, strings.Repeat("\x00", 16<<20), output) + requireProcessExitEvent(t, events, 0) + } +} + +func TestProcessStreamAfterExit(t *testing.T) { + svc := newProcessTestService(t, ProcessOutputConfig{}) + for i := 0; i < 20; i++ { + spawn := spawnProcess(t, svc, "sh", "-c", "printf one; printf two >&2; exit 7") + waitProcessExit(t, svc, spawn) + output, events := collectProcessStream(t, processStream(t, svc, spawn)) + require.Contains(t, output, "one") + require.Contains(t, output, "two") + requireProcessExitEvent(t, events, 7) + } +} + +func TestProcessExecTimeoutIsNotServerError(t *testing.T) { + svc := newProcessTestService(t, ProcessOutputConfig{}) + args, timeout := []string{"-c", "printf before; sleep 4; true"}, 1 + started := time.Now() + resp, err := svc.ProcessExec(context.Background(), oapi.ProcessExecRequestObject{Body: &oapi.ProcessExecRequest{Command: "sh", Args: &args, TimeoutSec: &timeout}}) + require.NoError(t, err) + require.Less(t, time.Since(started), 3*time.Second) + rec := httptest.NewRecorder() + require.NoError(t, resp.VisitProcessExecResponse(rec)) + require.Equal(t, 422, rec.Code, rec.Body.String()) + var result struct { + Code string `json:"code"` + StdoutB64 string `json:"stdout_b64"` + } + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &result)) + require.Equal(t, "process_timeout", result.Code) + require.Equal(t, base64.StdEncoding.EncodeToString([]byte("before")), result.StdoutB64) +} + +func TestProcessSlowReaderAndResume(t *testing.T) { + svc := newProcessTestService(t, ProcessOutputConfig{}) + spawn := spawnProcess(t, svc, "head", "-c", "2097152", "/dev/zero") + body := processStream(t, svc, spawn) + scanner := bufio.NewScanner(body) + var first oapi.ProcessStreamEvent + var lastID string + for scanner.Scan() { + if strings.HasPrefix(scanner.Text(), "id: ") { + lastID = strings.TrimPrefix(scanner.Text(), "id: ") + } + if strings.HasPrefix(scanner.Text(), "data: ") { + require.NoError(t, json.Unmarshal([]byte(strings.TrimPrefix(scanner.Text(), "data: ")), &first)) + break + } + } + require.NotNil(t, first.Offset) + require.Equal(t, strconv.FormatInt(*first.Offset, 10), lastID) + // Leave the first stream stalled while the producer finishes. + waitProcessExit(t, svc, spawn) + require.NoError(t, body.Close()) + firstData, err := base64.StdEncoding.DecodeString(*first.DataB64) + require.NoError(t, err) + for _, params := range []oapi.ProcessStdoutStreamParams{ + {LastEventID: &lastID}, + {Offset: first.Offset}, + {Offset: ptrOf(int64(0)), LastEventID: &lastID}, + } { + stream := processStream(t, svc, spawn, params) + output, events := collectProcessStream(t, stream) + require.NoError(t, stream.Close()) + require.Equal(t, 2<<20, len(firstData)+len(output), "resumed stream duplicated or lost output") + require.Equal(t, strings.Repeat("\x00", 2<<20), string(firstData)+output) + requireProcessExitEvent(t, events, 0) + } + for _, offset := range []int64{-1, 1, 1 << 40} { + resp, err := svc.ProcessStdoutStream(context.Background(), oapi.ProcessStdoutStreamRequestObject{ProcessId: spawn.ProcessId.String(), Params: oapi.ProcessStdoutStreamParams{Offset: &offset}}) + require.NoError(t, err) + require.IsType(t, oapi.ProcessStdoutStream400JSONResponse{}, resp) + } + resp, err := svc.ProcessStdoutStream(context.Background(), oapi.ProcessStdoutStreamRequestObject{ + ProcessId: spawn.ProcessId.String(), Params: oapi.ProcessStdoutStreamParams{Offset: ptrOf(int64(0)), LastEventID: ptrOf("invalid")}, + }) + require.NoError(t, err) + require.IsType(t, oapi.ProcessStdoutStream400JSONResponse{}, resp) +} + +func TestProcessExecCancellationStopsChildren(t *testing.T) { + svc := newProcessTestService(t, ProcessOutputConfig{}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + pidFile := filepath.Join(t.TempDir(), "child-pid") + args := []string{"-c", `sleep 30 & child=$!; printf before; printf '%s' "$child" > "$1"; wait`, "sh", pidFile} + type result struct { + response oapi.ProcessExecResponseObject + err error + } + completed := make(chan result, 1) + go func() { + response, err := svc.ProcessExec(ctx, oapi.ProcessExecRequestObject{Body: &oapi.ProcessExecRequest{Command: "sh", Args: &args}}) + completed <- result{response: response, err: err} + }() + childPID := waitProcessPIDFile(t, pidFile) + cancel() + select { + case result := <-completed: + require.NoError(t, result.err) + require.IsType(t, oapi.ProcessExec422JSONResponse{}, result.response) + failure := result.response.(oapi.ProcessExec422JSONResponse) + require.Equal(t, oapi.ProcessCanceled, failure.Code) + require.Equal(t, base64.StdEncoding.EncodeToString([]byte("before")), *failure.StdoutB64) + case <-time.After(5 * time.Second): + t.Fatal("exec did not finish after request cancellation") + } + requireProcessStopped(t, childPID) +} + +func TestProcessSpawnShutdownStopsChildren(t *testing.T) { + for _, tty := range []bool{false, true} { + t.Run(fmt.Sprintf("tty=%v", tty), func(t *testing.T) { + svc, err := newSvc(t, recorder.NewFFmpegManager()) + require.NoError(t, err) + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + require.NoError(t, svc.Shutdown(ctx)) + }) + pidFile := filepath.Join(t.TempDir(), "child-pid") + args := []string{"-c", `sleep 30 & printf '%s' "$!" > "$1"; wait`, "sh", pidFile} + response, err := svc.ProcessSpawn(context.Background(), oapi.ProcessSpawnRequestObject{Body: &oapi.ProcessSpawnRequest{ + Command: "sh", Args: &args, AllocateTty: &tty, + }}) + require.NoError(t, err) + require.IsType(t, oapi.ProcessSpawn200JSONResponse{}, response) + spawn := response.(oapi.ProcessSpawn200JSONResponse) + childPID := waitProcessPIDFile(t, pidFile) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + require.NoError(t, svc.Shutdown(ctx)) + requireProcessStopped(t, *spawn.Pid) + requireProcessStopped(t, childPID) + require.Eventually(t, func() bool { + status, err := svc.ProcessStatus(context.Background(), oapi.ProcessStatusRequestObject{ProcessId: spawn.ProcessId.String()}) + require.NoError(t, err) + _, released := status.(oapi.ProcessStatus404JSONResponse) + return released + }, time.Second, 10*time.Millisecond, "shutdown did not release the process") + }) + } +} + +func waitProcessPIDFile(t *testing.T, path string) int { + t.Helper() + var pid int + require.Eventually(t, func() bool { + data, err := os.ReadFile(path) + if err != nil { + return false + } + pid, err = strconv.Atoi(string(data)) + return err == nil && pid > 0 + }, 5*time.Second, 10*time.Millisecond, "command did not start its child") + return pid +} + +func requireProcessStopped(t *testing.T, pid int) { + t.Helper() + require.Eventually(t, func() bool { + stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) + return os.IsNotExist(err) || strings.Contains(string(stat), ") Z ") + }, time.Second, 10*time.Millisecond, "process %d is still running", pid) +} + +func TestProcessSpawnTimeoutSurvivesRequestCancellation(t *testing.T) { + for _, tty := range []bool{false, true} { + t.Run(fmt.Sprintf("tty=%v", tty), func(t *testing.T) { + t.Parallel() + svc := newProcessTestService(t, ProcessOutputConfig{}) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + args := []string{"-c", "sleep 30"} + resp, err := svc.ProcessSpawn(ctx, oapi.ProcessSpawnRequestObject{Body: &oapi.ProcessSpawnRequest{Command: "sh", Args: &args, TimeoutSec: ptrOf(1), AllocateTty: &tty}}) + require.NoError(t, err) + spawn := resp.(oapi.ProcessSpawn200JSONResponse) + cancel() + _, events := collectProcessStream(t, processStream(t, svc, spawn)) + requireProcessExitEvent(t, events, -1) + require.Equal(t, oapi.ProcessTimeout, *events[len(events)-1].TerminationReason) + status, err := svc.ProcessStatus(context.Background(), oapi.ProcessStatusRequestObject{ProcessId: spawn.ProcessId.String()}) + require.NoError(t, err) + require.Equal(t, oapi.ProcessTimeout, *status.(oapi.ProcessStatus200JSONResponse).TerminationReason) + }) + } +} + +func TestProcessTimeoutStopsChildren(t *testing.T) { + t.Parallel() + svc := newProcessTestService(t, ProcessOutputConfig{}) + args := []string{"-c", "sleep 30 & echo $!; wait"} + resp, err := svc.ProcessExec(context.Background(), oapi.ProcessExecRequestObject{Body: &oapi.ProcessExecRequest{Command: "sh", Args: &args, TimeoutSec: ptrOf(1)}}) + require.NoError(t, err) + failure := resp.(oapi.ProcessExec422JSONResponse) + require.Equal(t, oapi.ProcessTimeout, failure.Code) + data, err := base64.StdEncoding.DecodeString(*failure.StdoutB64) + require.NoError(t, err) + pid, err := strconv.Atoi(strings.TrimSpace(string(data))) + require.NoError(t, err) + require.Eventually(t, func() bool { + stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) + return os.IsNotExist(err) || strings.Contains(string(stat), ") Z ") + }, time.Second, 10*time.Millisecond, "child %d is still running", pid) +} + +func TestProcessDetachedChildDoesNotHoldExecResponse(t *testing.T) { + t.Parallel() + svc := newProcessTestService(t, ProcessOutputConfig{}) + pidFile := filepath.Join(t.TempDir(), "pid") + args := []string{"-c", `setsid sh -c 'echo $$ > "$1"; exec sleep 30' sh "$1" & wait`, "sh", pidFile} + t.Cleanup(func() { + data, err := os.ReadFile(pidFile) + if err == nil { + pid, err := strconv.Atoi(strings.TrimSpace(string(data))) + if err == nil { + _ = syscall.Kill(pid, syscall.SIGKILL) + } + } + }) + started := time.Now() + resp, err := svc.ProcessExec(context.Background(), oapi.ProcessExecRequestObject{Body: &oapi.ProcessExecRequest{Command: "sh", Args: &args, TimeoutSec: ptrOf(1)}}) + require.NoError(t, err) + require.Less(t, time.Since(started), 4*time.Second) + failure := resp.(oapi.ProcessExec422JSONResponse) + require.Equal(t, oapi.ProcessTimeout, failure.Code) + require.True(t, *failure.OutputTruncated) +} + +func TestProcessOutputQuota(t *testing.T) { + svc := newProcessTestService(t, ProcessOutputConfig{MaxBytes: 4096, TotalMaxBytes: 8192}) + for i := 0; i < 2; i++ { + spawn := spawnProcess(t, svc, "sh", "-c", "head -c 65536 /dev/zero; sleep 30") + output, events := collectProcessStream(t, processStream(t, svc, spawn)) + require.NotEmpty(t, output) + require.Less(t, len(output), 4096) + require.Equal(t, strings.Repeat("\x00", len(output)), output) + requireProcessExitEvent(t, events, -1) + require.Equal(t, oapi.OutputLimit, *events[len(events)-1].TerminationReason) + require.True(t, *events[len(events)-1].OutputTruncated) + } + + // Retained output consumes the shared budget until its retention expires. + args := []string{"-c", "printf more; sleep 30"} + resp, err := svc.ProcessExec(context.Background(), oapi.ProcessExecRequestObject{Body: &oapi.ProcessExecRequest{Command: "sh", Args: &args}}) + require.NoError(t, err) + failure := resp.(oapi.ProcessExec422JSONResponse) + require.Equal(t, oapi.OutputLimit, failure.Code) + require.Empty(t, *failure.StdoutB64) +} + +func TestProcessRetentionReclaimsOutputQuota(t *testing.T) { + t.Parallel() + svc := newProcessTestService(t, ProcessOutputConfig{MaxBytes: 4096, TotalMaxBytes: 4096}) + spawn := spawnProcess(t, svc, "head", "-c", "65536", "/dev/zero") + waitProcessExit(t, svc, spawn) + require.Eventually(t, func() bool { + resp, err := svc.ProcessStatus(context.Background(), oapi.ProcessStatusRequestObject{ProcessId: spawn.ProcessId.String()}) + _, expired := resp.(oapi.ProcessStatus404JSONResponse) + return err == nil && expired + }, 12*time.Second, 20*time.Millisecond) + args := []string{"-c", "printf reclaimed"} + resp, err := svc.ProcessExec(context.Background(), oapi.ProcessExecRequestObject{Body: &oapi.ProcessExecRequest{Command: "sh", Args: &args}}) + require.NoError(t, err) + result := resp.(oapi.ProcessExec200JSONResponse) + require.Equal(t, base64.StdEncoding.EncodeToString([]byte("reclaimed")), *result.StdoutB64) +} + +func TestProcessStorageFailureDoesNotRunCommand(t *testing.T) { + dir := t.TempDir() + svc := newProcessTestService(t, ProcessOutputConfig{Dir: filepath.Join(dir, "missing")}) + marker := filepath.Join(dir, "ran") + resp, err := svc.ProcessExec(context.Background(), oapi.ProcessExecRequestObject{Body: &oapi.ProcessExecRequest{Command: "touch", Args: &[]string{marker}}}) + require.NoError(t, err) + require.IsType(t, oapi.ProcessExec500JSONResponse{}, resp) + _, err = os.Stat(marker) + require.True(t, os.IsNotExist(err)) +} + +func TestProcessTimeoutHTTPContract(t *testing.T) { + t.Parallel() + svc := newProcessTestService(t, ProcessOutputConfig{}) + server := httptest.NewServer(oapi.Handler(oapi.NewStrictHandler(svc, nil))) + defer server.Close() + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Post(server.URL+"/process/exec", "application/json", strings.NewReader(`{"command":"sh","args":["-c","printf partial; sleep 30"],"timeout_sec":1}`)) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) + payload, err := io.ReadAll(resp.Body) + require.NoError(t, err) + var failure oapi.ProcessExecFailure + require.NoError(t, json.Unmarshal(payload, &failure)) + spec, err := oapi.GetSwagger() + require.NoError(t, err) + var document map[string]any + require.NoError(t, json.Unmarshal(payload, &document)) + schema := spec.Paths.Find("/process/exec").Post.Responses.Status(422).Value.Content["application/json"].Schema.Value + require.NoError(t, schema.VisitJSON(document)) + require.Equal(t, oapi.ProcessTimeout, failure.Code) + require.Equal(t, base64.StdEncoding.EncodeToString([]byte("partial")), *failure.StdoutB64) +} + +func TestProcessTimeoutAfterParentExitStopsChild(t *testing.T) { + t.Parallel() + svc := newProcessTestService(t, ProcessOutputConfig{}) + args := []string{"-c", "sleep 30 & echo $!"} + resp, err := svc.ProcessExec(context.Background(), oapi.ProcessExecRequestObject{Body: &oapi.ProcessExecRequest{Command: "sh", Args: &args, TimeoutSec: ptrOf(1)}}) + require.NoError(t, err) + var output string + switch r := resp.(type) { + case oapi.ProcessExec422JSONResponse: + output = *r.StdoutB64 + case oapi.ProcessExec200JSONResponse: + output = *r.StdoutB64 + } + data, err := base64.StdEncoding.DecodeString(output) + require.NoError(t, err) + pid, err := strconv.Atoi(strings.TrimSpace(string(data))) + require.NoError(t, err) + t.Cleanup(func() { _ = syscall.Kill(pid, syscall.SIGKILL) }) + require.IsType(t, oapi.ProcessExec422JSONResponse{}, resp) + require.Equal(t, oapi.ProcessTimeout, resp.(oapi.ProcessExec422JSONResponse).Code) + require.Eventually(t, func() bool { + stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) + return os.IsNotExist(err) || strings.Contains(string(stat), ") Z ") + }, time.Second, 10*time.Millisecond, "child %d is still running", pid) +} + +func TestProcessRetentionStopsBlockedHTTPStream(t *testing.T) { + t.Parallel() + svc := newProcessTestService(t, ProcessOutputConfig{}) + spawn := spawnProcess(t, svc, "head", "-c", "16777216", "/dev/zero") + finished := make(chan struct{}) + handler := chiMiddleware.Logger(oapi.Handler(oapi.NewStrictHandler(svc, nil))) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer close(finished) + handler.ServeHTTP(w, r) + })) + defer server.Close() + conn, err := net.Dial("tcp", strings.TrimPrefix(server.URL, "http://")) + require.NoError(t, err) + defer conn.Close() + require.NoError(t, conn.(*net.TCPConn).SetReadBuffer(1024)) + require.NoError(t, conn.SetReadDeadline(time.Now().Add(5*time.Second))) + _, err = fmt.Fprintf(conn, "GET /process/%s/stdout/stream HTTP/1.1\r\nHost: localhost\r\n\r\n", spawn.ProcessId.String()) + require.NoError(t, err) + status, err := bufio.NewReader(conn).ReadString('\n') + require.NoError(t, err) + require.Contains(t, status, "200") + waitProcessExit(t, svc, spawn) + select { + case <-finished: + case <-time.After(12 * time.Second): + t.Fatal("HTTP stream stayed blocked after output retention expired") + } + resp, err := svc.ProcessStatus(context.Background(), oapi.ProcessStatusRequestObject{ProcessId: spawn.ProcessId.String()}) + require.NoError(t, err) + require.IsType(t, oapi.ProcessStatus404JSONResponse{}, resp) +} + +func TestProcessCursorlessReconnectDoesNotReplay(t *testing.T) { + svc := newProcessTestService(t, ProcessOutputConfig{}) + spawn := spawnProcess(t, svc, "printf", "once") + output, events := collectProcessStream(t, processStream(t, svc, spawn)) + require.Equal(t, "once", output) + requireProcessExitEvent(t, events, 0) + output, events = collectProcessStream(t, processStream(t, svc, spawn)) + require.Empty(t, output) + require.Empty(t, events) +} + +func TestProcessTTYDoesNotRequireOutputStorage(t *testing.T) { + svc := newProcessTestService(t, ProcessOutputConfig{Dir: filepath.Join(t.TempDir(), "missing")}) + resp, err := svc.ProcessSpawn(context.Background(), oapi.ProcessSpawnRequestObject{Body: &oapi.ProcessSpawnRequest{Command: "true", AllocateTty: ptrOf(true)}}) + require.NoError(t, err) + require.IsType(t, oapi.ProcessSpawn200JSONResponse{}, resp) + spawn := resp.(oapi.ProcessSpawn200JSONResponse) + waitProcessExit(t, svc, spawn) + _, events := collectProcessStream(t, processStream(t, svc, spawn)) + requireProcessExitEvent(t, events, 0) +} + +func TestProcessExitedStatusIncludesCaptureFailure(t *testing.T) { + svc := newProcessTestService(t, ProcessOutputConfig{MaxBytes: 1024, TotalMaxBytes: 1024 * 100}) + for i := 0; i < 50; i++ { + spawn := spawnProcess(t, svc, "head", "-c", "8192", "/dev/zero") + require.Eventually(t, func() bool { + resp, err := svc.ProcessStatus(context.Background(), oapi.ProcessStatusRequestObject{ProcessId: spawn.ProcessId.String()}) + require.NoError(t, err) + status := resp.(oapi.ProcessStatus200JSONResponse) + if *status.State != "exited" { + return false + } + require.NotNil(t, status.TerminationReason) + require.Equal(t, oapi.OutputLimit, *status.TerminationReason) + require.True(t, *status.OutputTruncated) + return true + }, time.Second, time.Millisecond) + } +} + +func TestProcessCursorlessReconnectContinuesUnreadOutput(t *testing.T) { + svc := newProcessTestService(t, ProcessOutputConfig{}) + spawn := spawnProcess(t, svc, "sh", "-c", "printf first; read line; printf second") + finished := make(chan struct{}) + handler := oapi.Handler(oapi.NewStrictHandler(svc, nil)) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer close(finished) + handler.ServeHTTP(w, r) + })) + defer server.Close() + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(server.URL + "/process/" + spawn.ProcessId.String() + "/stdout/stream") + require.NoError(t, err) + body := resp.Body + defer body.Close() + scanner := bufio.NewScanner(body) + for scanner.Scan() { + if strings.HasPrefix(scanner.Text(), "data: ") { + var event oapi.ProcessStreamEvent + require.NoError(t, json.Unmarshal([]byte(strings.TrimPrefix(scanner.Text(), "data: ")), &event)) + require.Equal(t, base64.StdEncoding.EncodeToString([]byte("first")), *event.DataB64) + break + } + } + require.NoError(t, scanner.Err()) + require.NoError(t, body.Close()) + select { + case <-finished: + case <-time.After(time.Second): + t.Fatal("disconnected stream did not stop") + } + _, err = svc.ProcessStdin(context.Background(), oapi.ProcessStdinRequestObject{ProcessId: spawn.ProcessId.String(), Body: &oapi.ProcessStdinRequest{DataB64: base64.StdEncoding.EncodeToString([]byte("go\n"))}}) + require.NoError(t, err) + output, events := collectProcessStream(t, processStream(t, svc, spawn)) + require.Equal(t, "second", output) + requireProcessExitEvent(t, events, 0) + replay, replayEvents := collectProcessStream(t, processStream(t, svc, spawn, oapi.ProcessStdoutStreamParams{Offset: ptrOf(int64(0))})) + require.Equal(t, "firstsecond", replay) + requireProcessExitEvent(t, replayEvents, 0) +} + +func TestProcessManualSignalTargetsLeader(t *testing.T) { + svc := newProcessTestService(t, ProcessOutputConfig{}) + spawn := spawnProcess(t, svc, "sh", "-c", `trap 'printf parent-signal; exit 17' HUP +sh -c 'trap "printf child-signal" HUP; printf ready; while :; do sleep 1; done' & +wait`) + body := processStream(t, svc, spawn) + scanner := bufio.NewScanner(body) + for scanner.Scan() { + if strings.HasPrefix(scanner.Text(), "data: ") { + var event oapi.ProcessStreamEvent + require.NoError(t, json.Unmarshal([]byte(strings.TrimPrefix(scanner.Text(), "data: ")), &event)) + require.Equal(t, base64.StdEncoding.EncodeToString([]byte("ready")), *event.DataB64) + break + } + } + require.NoError(t, scanner.Err()) + response, err := svc.ProcessKill(context.Background(), oapi.ProcessKillRequestObject{ProcessId: spawn.ProcessId.String(), Body: &oapi.ProcessKillRequest{Signal: "HUP"}}) + require.NoError(t, err) + require.IsType(t, oapi.ProcessKill200JSONResponse{}, response) + var output strings.Builder + for scanner.Scan() { + if strings.HasPrefix(scanner.Text(), "data: ") { + var event oapi.ProcessStreamEvent + require.NoError(t, json.Unmarshal([]byte(strings.TrimPrefix(scanner.Text(), "data: ")), &event)) + if event.DataB64 != nil { + data, err := base64.StdEncoding.DecodeString(*event.DataB64) + require.NoError(t, err) + output.Write(data) + } else { + require.Equal(t, 17, *event.ExitCode) + } + } + } + require.NoError(t, scanner.Err()) + require.Equal(t, "parent-signal", output.String()) +} diff --git a/server/cmd/api/api/process_output.go b/server/cmd/api/api/process_output.go new file mode 100644 index 000000000..605f0f2ec --- /dev/null +++ b/server/cmd/api/api/process_output.go @@ -0,0 +1,231 @@ +package api + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "io" + "os" + "sync" +) + +const ( + processOutputChunkSize = 32 * 1024 + processOutputHeaderSize = 13 +) + +var errProcessOutputLimit = errors.New("process output limit exceeded") +var errProcessOutputConsumed = errors.New("process output already consumed") +var errProcessOutputOffset = errors.New("invalid process output offset") + +// ProcessOutputConfig bounds stored output, including record headers. Zero limits +// use the defaults; Dir defaults to the operating system's temporary directory. +type ProcessOutputConfig struct { + Dir string + MaxBytes int64 + TotalMaxBytes int64 +} + +type processOutputBudget struct { + mu sync.Mutex + config ProcessOutputConfig + used int64 +} + +func newProcessOutputBudget(config ProcessOutputConfig) (*processOutputBudget, error) { + if config.MaxBytes == 0 { + config.MaxBytes = 64 << 20 + } + if config.TotalMaxBytes == 0 { + config.TotalMaxBytes = 256 << 20 + } + if config.MaxBytes <= processOutputHeaderSize || config.TotalMaxBytes < config.MaxBytes { + return nil, fmt.Errorf("process output limits must exceed %d bytes and total must be at least the per-process limit", processOutputHeaderSize) + } + return &processOutputBudget{config: config}, nil +} + +func (b *processOutputBudget) reserve(n int64) int64 { + b.mu.Lock() + defer b.mu.Unlock() + n = min(n, b.config.TotalMaxBytes-b.used) + b.used += n + return n +} + +func (b *processOutputBudget) release(n int64) { + b.mu.Lock() + b.used -= n + b.mu.Unlock() +} + +type processOutput struct { + mu sync.Mutex + file *os.File + budget *processOutputBudget + size int64 + allocated int64 + complete bool + err error + changed chan struct{} + closed chan struct{} + closeOnce sync.Once + consumedOffset int64 + exitConsumed bool +} + +func (b *processOutputBudget) open() (*processOutput, error) { + f, err := os.CreateTemp(b.config.Dir, "kernel-process-output-*") + if err != nil { + return nil, err + } + // Keep only the open descriptor so a server crash cannot orphan output files. + if err := os.Remove(f.Name()); err != nil { + f.Close() + return nil, err + } + return &processOutput{file: f, budget: b, changed: make(chan struct{}), closed: make(chan struct{})}, nil +} + +func (o *processOutput) append(stream byte, data []byte) error { + o.mu.Lock() + defer o.mu.Unlock() + if o.err != nil { + return o.err + } + select { + case <-o.closed: + return os.ErrClosed + default: + } + want := int64(processOutputHeaderSize + len(data)) + available := min(want, o.budget.config.MaxBytes-o.allocated) + reserved := o.budget.reserve(available) + if reserved <= processOutputHeaderSize { + o.budget.release(reserved) + o.err = errProcessOutputLimit + return o.err + } + o.allocated += reserved + n := int(reserved) - processOutputHeaderSize + record := make([]byte, int(reserved)) + binary.BigEndian.PutUint64(record, uint64(o.size)) + binary.BigEndian.PutUint32(record[8:], uint32(n)) + record[12] = stream + copy(record[processOutputHeaderSize:], data[:n]) + if _, err := o.file.WriteAt(record, o.size); err != nil { + o.err = err + return err + } + o.size += reserved + close(o.changed) + o.changed = make(chan struct{}) + if reserved < want { + o.err = errProcessOutputLimit + } + return o.err +} + +func (o *processOutput) finish() { + o.mu.Lock() + o.complete = true + close(o.changed) + o.mu.Unlock() +} + +func (o *processOutput) close() { + o.closeOnce.Do(func() { + o.mu.Lock() + defer o.mu.Unlock() + close(o.closed) + if o.file != nil { + o.file.Close() + o.budget.release(o.allocated) + } + }) +} + +func (o *processOutput) header(offset int64) (byte, int, error) { + var header [processOutputHeaderSize]byte + if _, err := o.file.ReadAt(header[:], offset); err != nil { + return 0, 0, err + } + n := int(binary.BigEndian.Uint32(header[8:])) + if binary.BigEndian.Uint64(header[:]) != uint64(offset) || n <= 0 || n > processOutputChunkSize || header[12] > 1 { + return 0, 0, errProcessOutputOffset + } + return header[12], n, nil +} + +func (o *processOutput) validateOffset(offset int64) error { + o.mu.Lock() + defer o.mu.Unlock() + if offset < 0 || offset > o.size { + return errProcessOutputOffset + } + if offset == o.size { + return nil + } + _, n, err := o.header(offset) + if err != nil || offset+processOutputHeaderSize+int64(n) > o.size { + return errProcessOutputOffset + } + return nil +} + +func (o *processOutput) next(ctx context.Context, offset int64) (stream byte, data []byte, next int64, err error) { + for { + o.mu.Lock() + size, complete, changed := o.size, o.complete, o.changed + o.mu.Unlock() + if offset < size { + stream, n, err := o.header(offset) + if err != nil { + return 0, nil, offset, err + } + next := offset + processOutputHeaderSize + int64(n) + if next > size { + return 0, nil, offset, errProcessOutputOffset + } + data := make([]byte, n) + _, err = o.file.ReadAt(data, offset+processOutputHeaderSize) + return stream, data, next, err + } + if complete { + return 0, nil, offset, io.EOF + } + select { + case <-ctx.Done(): + return 0, nil, offset, ctx.Err() + case <-o.closed: + return 0, nil, offset, os.ErrClosed + case <-changed: + } + } +} + +// Cursorless streams share consumption, as with the original output queue. +func (o *processOutput) nextUnread(ctx context.Context) (byte, []byte, int64, error) { + for { + o.mu.Lock() + offset, consumed := o.consumedOffset, o.exitConsumed + o.mu.Unlock() + if consumed { + return 0, nil, offset, errProcessOutputConsumed + } + stream, data, next, err := o.next(ctx, offset) + if err != nil && !errors.Is(err, io.EOF) { + return 0, nil, offset, err + } + o.mu.Lock() + if o.consumedOffset != offset || o.exitConsumed { + o.mu.Unlock() + continue + } + o.consumedOffset = next + o.exitConsumed = errors.Is(err, io.EOF) + o.mu.Unlock() + return stream, data, next, err + } +} diff --git a/server/cmd/api/api/process_runner.go b/server/cmd/api/api/process_runner.go new file mode 100644 index 000000000..db8185373 --- /dev/null +++ b/server/cmd/api/api/process_runner.go @@ -0,0 +1,256 @@ +package api + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" + "syscall" + "time" + + "github.com/creack/pty" + "github.com/google/uuid" + "github.com/kernel/kernel-images/server/lib/logger" +) + +const processOutputDrainTimeout = time.Second +const processRetention = 10 * time.Second + +func (s *ApiService) startProcess(ctx context.Context, cmd *exec.Cmd, tty, stdin bool, timeoutSec *int) (*processHandle, error) { + s.procMu.Lock() + if s.processOutput == nil { + s.processOutput, _ = newProcessOutputBudget(ProcessOutputConfig{}) + } + budget := s.processOutput + s.procMu.Unlock() + output := &processOutput{changed: make(chan struct{}), closed: make(chan struct{})} + var err error + if !tty { + output, err = budget.open() + if err != nil { + return nil, fmt.Errorf("create process output: %v", err) + } + } + h := &processHandle{id: uuid.New(), cmd: cmd, started: time.Now(), isTTY: tty, output: output, doneCh: make(chan struct{})} + if tty { + h.ptyFile, err = pty.Start(cmd) + h.stdin = h.ptyFile + } else { + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + } + cmd.SysProcAttr.Setpgid = true + if stdin { + h.stdin, err = cmd.StdinPipe() + } + if err == nil { + h.stdout, h.stderr, err = startWithOutputPipes(cmd) + } + } + if err != nil { + if h.stdin != nil { + h.stdin.Close() + } + output.close() + return nil, err + } + h.pid = cmd.Process.Pid + s.procMu.Lock() + if s.procs == nil { + s.procs = make(map[string]*processHandle) + } + s.procs[h.id.String()] = h + s.procMu.Unlock() + stzDisabled := false + if s.stz != nil { + if err := s.stz.Disable(ctx); err != nil { + logger.FromContext(ctx).Error("failed to disable scale-to-zero", "err", err) + } else { + stzDisabled = true + } + } + lifecycle := s.lifecycleCtx + if lifecycle == nil { + lifecycle = context.Background() + } + go h.run(ctx, lifecycle, timeoutSec, func() { + if stzDisabled { + releaseCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) + defer cancel() + if err := s.stz.Enable(releaseCtx); err != nil { + logger.FromContext(ctx).Error("failed to enable scale-to-zero", "err", err) + } + } + }) + return h, nil +} + +func startWithOutputPipes(cmd *exec.Cmd) (io.ReadCloser, io.ReadCloser, error) { + stdoutR, stdoutW, err := os.Pipe() + if err != nil { + return nil, nil, err + } + stderrR, stderrW, err := os.Pipe() + if err != nil { + stdoutR.Close() + stdoutW.Close() + return nil, nil, err + } + cmd.Stdout, cmd.Stderr = stdoutW, stderrW + err = cmd.Start() + stdoutW.Close() + stderrW.Close() + if err != nil { + stdoutR.Close() + stderrR.Close() + return nil, nil, err + } + return stdoutR, stderrR, nil +} + +func (h *processHandle) terminate(reason string, truncated bool) { + h.mu.Lock() + defer h.mu.Unlock() + if h.reason == "" { + h.reason = reason + } + h.truncated = h.truncated || truncated + if h.exitCode == nil { + _ = signalChildProcessGroup(h.cmd, killSignal) + } +} + +func (h *processHandle) outputFailed(err error) { + reason := "output_error" + if errors.Is(err, errProcessOutputLimit) { + reason = "output_limit" + } + h.terminate(reason, true) +} + +func (h *processHandle) run(ctx, lifecycle context.Context, timeoutSec *int, release func()) { + defer release() + captureResults := make(chan error, 2) + pendingCaptures := 0 + if !h.isTTY { + for stream, pipe := range []io.ReadCloser{h.stdout, h.stderr} { + pendingCaptures++ + go func(stream byte, pipe io.Reader) { + captureResults <- h.captureOutput(stream, pipe) + }(byte(stream), pipe) + } + } + waited := make(chan error, 1) + go func() { waited <- waitForProcessExit(h.cmd) }() + var deadline, drain <-chan time.Time + var deadlineAt time.Time + if timeoutSec != nil && *timeoutSec > 0 { + deadlineAt = h.started.Add(time.Duration(*timeoutSec) * time.Second) + timer := time.NewTimer(time.Until(deadlineAt)) + defer timer.Stop() + deadline = timer.C + } + canceled, shutdown := ctx.Done(), lifecycle.Done() + for waited != nil || pendingCaptures > 0 { + select { + case err := <-waited: + if err != nil { + h.terminate("process_canceled", false) + logger.FromContext(ctx).Error("failed to wait for process exit", "err", err) + } + waited = nil + timer := time.NewTimer(processOutputDrainTimeout) + defer timer.Stop() + drain = timer.C + case err := <-captureResults: + pendingCaptures-- + if err != nil { + h.outputFailed(err) + } + case <-deadline: + h.terminate("process_timeout", false) + deadline = nil + case <-canceled: + h.terminate("process_canceled", false) + canceled = nil + case <-shutdown: + h.terminate("process_canceled", false) + shutdown = nil + case <-drain: + if !deadlineAt.IsZero() && !time.Now().Before(deadlineAt) { + h.terminate("process_timeout", false) + } + h.mu.Lock() + h.truncated = true + _ = signalChildProcessGroup(h.cmd, killSignal) + h.mu.Unlock() + h.closeOutputPipes() + drain = nil + } + } + h.closeOutputPipes() + if h.stdin != nil { + h.stdin.Close() + } + // Reap while holding the same lock used by signal requests, so no signal can + // target a reused PID between Wait and publishing the final status. + h.mu.Lock() + reapProcess(h.cmd) + code := h.cmd.ProcessState.ExitCode() + h.durationMs = int(time.Since(h.started) / time.Millisecond) + h.exitCode = &code + h.mu.Unlock() + h.output.finish() + close(h.doneCh) +} + +func (h *processHandle) captureOutput(stream byte, pipe io.Reader) error { + buf := make([]byte, processOutputChunkSize) + for { + n, err := pipe.Read(buf) + if n > 0 { + if writeErr := h.output.append(stream, buf[:n]); writeErr != nil { + return writeErr + } + } + if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) { + return nil + } + if err != nil { + return err + } + } +} + +func (h *processHandle) closeOutputPipes() { + if h.isTTY { + h.ptyFile.Close() + } else { + h.stdout.Close() + h.stderr.Close() + } +} + +func (s *ApiService) releaseProcess(h *processHandle) { + s.procMu.Lock() + delete(s.procs, h.id.String()) + s.procMu.Unlock() + h.output.close() +} + +func (s *ApiService) retainProcess(h *processHandle) { + <-h.doneCh + timer := time.NewTimer(processRetention) + defer timer.Stop() + var shutdown <-chan struct{} + if s.lifecycleCtx != nil { + shutdown = s.lifecycleCtx.Done() + } + select { + case <-timer.C: + case <-shutdown: + } + s.releaseProcess(h) +} diff --git a/server/cmd/api/api/process_test.go b/server/cmd/api/api/process_test.go index ea15ae6da..4893d920f 100644 --- a/server/cmd/api/api/process_test.go +++ b/server/cmd/api/api/process_test.go @@ -62,12 +62,7 @@ func TestProcessSpawnStatusAndStream(t *testing.T) { require.True(t, ok, "unexpected status resp: %T", statusResp) // Start stream reader and collect at least two data events and one exit event - streamResp, err := svc.ProcessStdoutStream(ctx, oapi.ProcessStdoutStreamRequestObject{ProcessId: s200.ProcessId.String()}) - require.NoError(t, err, "StdoutStream error") - st200, ok := streamResp.(oapi.ProcessStdoutStream200TexteventStreamResponse) - require.True(t, ok, "unexpected stream resp: %T", streamResp) - - reader := bufio.NewReader(st200.Body) + reader := bufio.NewReader(processStream(t, svc, s200)) var gotStdout, gotStderr, gotExit bool deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) && !(gotStdout && gotStderr && gotExit) { diff --git a/server/cmd/api/api/process_wait_linux.go b/server/cmd/api/api/process_wait_linux.go new file mode 100644 index 000000000..da9b0f1fa --- /dev/null +++ b/server/cmd/api/api/process_wait_linux.go @@ -0,0 +1,24 @@ +//go:build linux + +package api + +import ( + "errors" + "os/exec" + + "golang.org/x/sys/unix" +) + +// Leave the exited leader waitable until capture ends. Its PID cannot be reused +// while we may still need to signal descendants in its process group. +func waitForProcessExit(cmd *exec.Cmd) error { + var info unix.Siginfo + for { + err := unix.Waitid(unix.P_PID, cmd.Process.Pid, &info, unix.WEXITED|unix.WNOWAIT, nil) + if !errors.Is(err, unix.EINTR) { + return err + } + } +} + +func reapProcess(cmd *exec.Cmd) { _ = cmd.Wait() } diff --git a/server/cmd/api/api/process_wait_linux_test.go b/server/cmd/api/api/process_wait_linux_test.go new file mode 100644 index 000000000..469126129 --- /dev/null +++ b/server/cmd/api/api/process_wait_linux_test.go @@ -0,0 +1,29 @@ +//go:build linux + +package api + +import ( + "fmt" + "os" + "os/exec" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestProcessWaitPreservesExitedLeader(t *testing.T) { + cmd := exec.Command("sh", "-c", "exit 37") + require.NoError(t, cmd.Start()) + defer cmd.Wait() + require.Eventually(t, func() bool { + stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", cmd.Process.Pid)) + return err == nil && strings.Contains(string(stat), ") Z ") + }, time.Second, time.Millisecond) + require.NoError(t, waitForProcessExit(cmd)) + require.NoError(t, waitForProcessExit(cmd), "observing exit must not reap the leader") + reapProcess(cmd) + require.NotNil(t, cmd.ProcessState) + require.Equal(t, 37, cmd.ProcessState.ExitCode()) +} diff --git a/server/cmd/api/api/process_wait_other.go b/server/cmd/api/api/process_wait_other.go new file mode 100644 index 000000000..0dc789941 --- /dev/null +++ b/server/cmd/api/api/process_wait_other.go @@ -0,0 +1,12 @@ +//go:build !linux + +package api + +import "os/exec" + +func waitForProcessExit(cmd *exec.Cmd) error { + _ = cmd.Wait() + return nil +} + +func reapProcess(cmd *exec.Cmd) {} diff --git a/server/cmd/api/api/telemetry_test.go b/server/cmd/api/api/telemetry_test.go index 744dcf376..bea95116c 100644 --- a/server/cmd/api/api/telemetry_test.go +++ b/server/cmd/api/api/telemetry_test.go @@ -409,7 +409,7 @@ func (m *mockRecordManager) StopAll(_ context.Context) error func newTestService(t *testing.T, mgr recorder.RecordManager) *ApiService { t.Helper() ts, es := newTelemetrySession(t) - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil, nil, testEgressPolicy(t), testWebAuthn(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil, nil, testEgressPolicy(t), testWebAuthn(t), ProcessOutputConfig{}) require.NoError(t, err) svc.cdpMonitor = &stubCdpMonitor{} return svc diff --git a/server/cmd/api/api/webauthn_test.go b/server/cmd/api/api/webauthn_test.go index 168aee0c6..1b47709a5 100644 --- a/server/cmd/api/api/webauthn_test.go +++ b/server/cmd/api/api/webauthn_test.go @@ -12,7 +12,7 @@ import ( func newWebAuthnSvc(t *testing.T, metroURL string) *ApiService { t.Helper() ts, es := newTelemetrySession(t) - svc, err := New(newMockRecordManager(), newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil, nil, testEgressPolicy(t), testWebAuthnWithMetro(t, metroURL)) + svc, err := New(newMockRecordManager(), newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil, nil, testEgressPolicy(t), testWebAuthnWithMetro(t, metroURL), ProcessOutputConfig{}) require.NoError(t, err) return svc } diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index f2d4316e2..ee3b0ce2f 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -225,6 +225,7 @@ func main() { s2Storage, egressPolicy, webauthnHook, + api.ProcessOutputConfig{Dir: config.ProcessOutputDir, MaxBytes: config.ProcessOutputMaxBytes, TotalMaxBytes: config.ProcessOutputTotalMaxBytes}, ) if err != nil { slogger.Error("failed to create api service", "err", err) diff --git a/server/cmd/config/config.go b/server/cmd/config/config.go index 068f01247..7e414b707 100644 --- a/server/cmd/config/config.go +++ b/server/cmd/config/config.go @@ -20,6 +20,10 @@ type Config struct { // external API surface. MetricsPort int `envconfig:"METRICS_PORT" default:"10002"` + ProcessOutputDir string `envconfig:"PROCESS_OUTPUT_DIR" default:""` + ProcessOutputMaxBytes int64 `envconfig:"PROCESS_OUTPUT_MAX_BYTES" default:"67108864"` + ProcessOutputTotalMaxBytes int64 `envconfig:"PROCESS_OUTPUT_TOTAL_MAX_BYTES" default:"268435456"` + // Recording configuration FrameRate int `envconfig:"FRAME_RATE" default:"10"` DisplayNum int `envconfig:"DISPLAY_NUM" default:"1"` @@ -93,6 +97,9 @@ func (c *Config) LogValue() slog.Value { return slog.GroupValue( slog.Int("port", c.Port), slog.Int("metrics_port", c.MetricsPort), + slog.String("process_output_dir", c.ProcessOutputDir), + slog.Int64("process_output_max_bytes", c.ProcessOutputMaxBytes), + slog.Int64("process_output_total_max_bytes", c.ProcessOutputTotalMaxBytes), slog.Int("frame_rate", c.FrameRate), slog.Int("display_num", c.DisplayNum), slog.Int("max_size_mb", c.MaxSizeInMB), diff --git a/server/cmd/config/config_test.go b/server/cmd/config/config_test.go index eda4e3669..a1c5598bf 100644 --- a/server/cmd/config/config_test.go +++ b/server/cmd/config/config_test.go @@ -18,64 +18,72 @@ func TestLoad(t *testing.T) { name: "defaults (no env set)", env: map[string]string{}, wantCfg: &Config{ - Port: 10001, - MetricsPort: 10002, - FrameRate: 10, - DisplayNum: 1, - MaxSizeInMB: 500, - OutputDir: ".", - AudioSource: "", - PulseServer: "", - PathToFFmpeg: "ffmpeg", - DevToolsProxyPort: 9222, - ScaleToZeroCooldown: time.Second, - ChromeDriverProxyPort: 9224, - ChromeDriverUpstreamAddr: "127.0.0.1:9225", - DevToolsProxyAddr: "127.0.0.1:9222", - OTLPPath: "/v1/logs", - OTLPServiceName: "kernel-browser", - OTLPMaxQueueSize: 2048, - OTLPExportInterval: time.Second, - OTLPExportTimeout: 30 * time.Second, + Port: 10001, + ProcessOutputMaxBytes: 67108864, + ProcessOutputTotalMaxBytes: 268435456, + MetricsPort: 10002, + FrameRate: 10, + DisplayNum: 1, + MaxSizeInMB: 500, + OutputDir: ".", + AudioSource: "", + PulseServer: "", + PathToFFmpeg: "ffmpeg", + DevToolsProxyPort: 9222, + ScaleToZeroCooldown: time.Second, + ChromeDriverProxyPort: 9224, + ChromeDriverUpstreamAddr: "127.0.0.1:9225", + DevToolsProxyAddr: "127.0.0.1:9222", + OTLPPath: "/v1/logs", + OTLPServiceName: "kernel-browser", + OTLPMaxQueueSize: 2048, + OTLPExportInterval: time.Second, + OTLPExportTimeout: 30 * time.Second, }, }, { name: "custom valid env", env: map[string]string{ - "PORT": "12345", - "METRICS_PORT": "12346", - "FRAME_RATE": "20", - "DISPLAY_NUM": "2", - "MAX_SIZE_MB": "250", - "OUTPUT_DIR": "/tmp", - "AUDIO_SOURCE": "CustomOutput.monitor", - "PULSE_SERVER": "unix:/tmp/pulse/native", - "FFMPEG_PATH": "/usr/local/bin/ffmpeg", - "DEVTOOLS_PROXY_PORT": "9876", - "SCALE_TO_ZERO_COOLDOWN": "5s", - "CHROMEDRIVER_PROXY_PORT": "5432", - "CHROMEDRIVER_UPSTREAM_ADDR": "127.0.0.1:9999", + "PORT": "12345", + "PROCESS_OUTPUT_DIR": "/tmp/process-output", + "PROCESS_OUTPUT_MAX_BYTES": "1048576", + "PROCESS_OUTPUT_TOTAL_MAX_BYTES": "4194304", + "METRICS_PORT": "12346", + "FRAME_RATE": "20", + "DISPLAY_NUM": "2", + "MAX_SIZE_MB": "250", + "OUTPUT_DIR": "/tmp", + "AUDIO_SOURCE": "CustomOutput.monitor", + "PULSE_SERVER": "unix:/tmp/pulse/native", + "FFMPEG_PATH": "/usr/local/bin/ffmpeg", + "DEVTOOLS_PROXY_PORT": "9876", + "SCALE_TO_ZERO_COOLDOWN": "5s", + "CHROMEDRIVER_PROXY_PORT": "5432", + "CHROMEDRIVER_UPSTREAM_ADDR": "127.0.0.1:9999", }, wantCfg: &Config{ - Port: 12345, - MetricsPort: 12346, - FrameRate: 20, - DisplayNum: 2, - MaxSizeInMB: 250, - OutputDir: "/tmp", - AudioSource: "CustomOutput.monitor", - PulseServer: "unix:/tmp/pulse/native", - PathToFFmpeg: "/usr/local/bin/ffmpeg", - DevToolsProxyPort: 9876, - ScaleToZeroCooldown: 5 * time.Second, - ChromeDriverProxyPort: 5432, - ChromeDriverUpstreamAddr: "127.0.0.1:9999", - DevToolsProxyAddr: "127.0.0.1:9876", - OTLPPath: "/v1/logs", - OTLPServiceName: "kernel-browser", - OTLPMaxQueueSize: 2048, - OTLPExportInterval: time.Second, - OTLPExportTimeout: 30 * time.Second, + Port: 12345, + ProcessOutputDir: "/tmp/process-output", + ProcessOutputMaxBytes: 1048576, + ProcessOutputTotalMaxBytes: 4194304, + MetricsPort: 12346, + FrameRate: 20, + DisplayNum: 2, + MaxSizeInMB: 250, + OutputDir: "/tmp", + AudioSource: "CustomOutput.monitor", + PulseServer: "unix:/tmp/pulse/native", + PathToFFmpeg: "/usr/local/bin/ffmpeg", + DevToolsProxyPort: 9876, + ScaleToZeroCooldown: 5 * time.Second, + ChromeDriverProxyPort: 5432, + ChromeDriverUpstreamAddr: "127.0.0.1:9999", + DevToolsProxyAddr: "127.0.0.1:9876", + OTLPPath: "/v1/logs", + OTLPServiceName: "kernel-browser", + OTLPMaxQueueSize: 2048, + OTLPExportInterval: time.Second, + OTLPExportTimeout: 30 * time.Second, }, }, { @@ -85,25 +93,27 @@ func TestLoad(t *testing.T) { "DEVTOOLS_PROXY_ADDR": "10.0.0.1:1234", }, wantCfg: &Config{ - Port: 10001, - MetricsPort: 10002, - FrameRate: 10, - DisplayNum: 1, - MaxSizeInMB: 500, - OutputDir: ".", - AudioSource: "", - PulseServer: "", - PathToFFmpeg: "ffmpeg", - DevToolsProxyPort: 7777, - ScaleToZeroCooldown: time.Second, - ChromeDriverProxyPort: 9224, - ChromeDriverUpstreamAddr: "127.0.0.1:9225", - DevToolsProxyAddr: "10.0.0.1:1234", - OTLPPath: "/v1/logs", - OTLPServiceName: "kernel-browser", - OTLPMaxQueueSize: 2048, - OTLPExportInterval: time.Second, - OTLPExportTimeout: 30 * time.Second, + Port: 10001, + ProcessOutputMaxBytes: 67108864, + ProcessOutputTotalMaxBytes: 268435456, + MetricsPort: 10002, + FrameRate: 10, + DisplayNum: 1, + MaxSizeInMB: 500, + OutputDir: ".", + AudioSource: "", + PulseServer: "", + PathToFFmpeg: "ffmpeg", + DevToolsProxyPort: 7777, + ScaleToZeroCooldown: time.Second, + ChromeDriverProxyPort: 9224, + ChromeDriverUpstreamAddr: "127.0.0.1:9225", + DevToolsProxyAddr: "10.0.0.1:1234", + OTLPPath: "/v1/logs", + OTLPServiceName: "kernel-browser", + OTLPMaxQueueSize: 2048, + OTLPExportInterval: time.Second, + OTLPExportTimeout: 30 * time.Second, }, }, { diff --git a/server/cmd/shell/main.go b/server/cmd/shell/main.go index 6f5a3abbe..38e220a09 100644 --- a/server/cmd/shell/main.go +++ b/server/cmd/shell/main.go @@ -16,9 +16,7 @@ import ( "syscall" "time" - "github.com/google/uuid" oapi "github.com/kernel/kernel-images/server/lib/oapi" - openapi_types "github.com/oapi-codegen/runtime/types" "golang.org/x/term" ) @@ -156,8 +154,7 @@ func main() { go func() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() - uid, _ := uuid.Parse(procID) - _, _ = client.ProcessResizeWithResponse(ctx, openapi_types.UUID(uid), oapi.ProcessResizeJSONRequestBody{ + _, _ = client.ProcessResizeWithResponse(ctx, procID, oapi.ProcessResizeJSONRequestBody{ Rows: rows, Cols: cols, }) diff --git a/server/docs/processes.md b/server/docs/processes.md new file mode 100644 index 000000000..823b60cb7 --- /dev/null +++ b/server/docs/processes.md @@ -0,0 +1,39 @@ +# Process execution and output + +`POST /process/exec` waits for completion. `POST /process/spawn` returns a process ID immediately. Both accept `timeout_sec`; null or zero disables the deadline. A negative value is rejected. + +Non-TTY commands continuously capture stdout and stderr into temporary files, whether or not a client is streaming. Output capture does not wait for stream readers. TTY output continues to use the WebSocket attach endpoint. + +## Limits and retention + +| Environment variable | Default | Meaning | +| --- | --- | --- | +| `PROCESS_OUTPUT_DIR` | OS temporary directory | Existing writable directory for output files | +| `PROCESS_OUTPUT_MAX_BYTES` | 67108864 (64 MiB) | Maximum stored output per process | +| `PROCESS_OUTPUT_TOTAL_MAX_BYTES` | 268435456 (256 MiB) | Maximum stored output across running and retained processes | + +Limits count stdout, stderr, and record headers. Zero selects the default. The per-process limit must exceed 13 bytes and the aggregate limit must be at least the per-process limit. These limits bound stored file bytes, not filesystem block allocation or the memory needed to serialize an exec response. + +Exceeding either limit stops the producing process group and reports `output_limit` with `output_truncated: true`. Already captured output remains available. A storage failure reports `output_error`. + +Spawned process status and output remain available for 10 seconds after capture completes. Expiration releases the storage budget and closes active streams, even if a reader has stalled. Exec releases its stored output when its result has been assembled. Output is temporary and does not survive a server restart. + +## Streaming and reconnecting + +`GET /process/{process_id}/stdout/stream` without a cursor consumes the process's unread output, including output captured before the first connection. Reconnects continue at the shared consumed position and do not repeat a consumed exit event. Concurrent cursorless connections divide the output, as with the original queue. Consumption happens when a record is read for delivery; a disconnect can lose an in-flight record, so use explicit cursors when delivery must be resumable. + +Supplying `?offset=0` replays all retained output, then follows new output. Data events include an opaque `offset`, also emitted as the SSE `id`. To resume after a data event, send its cursor as `?offset=...` or the `Last-Event-ID` header. These explicit cursors read independently and do not advance cursorless consumption. When both are supplied, `Last-Event-ID` takes precedence so an `EventSource` opened with `?offset=0` resumes on automatic reconnect. To replay from the beginning again, open a new connection with `?offset=0` and no `Last-Event-ID`. Invalid cursors return 400; expired processes return 404. + +An `exit` event follows all captured output and includes `exit_code`, `output_truncated`, and, when execution was stopped by the server, `termination_reason`. Clients must check the terminal event to distinguish complete capture from partial output. Reconnection does not extend retention. + +## Timeouts and cancellation + +A timeout or exec request cancellation sends SIGKILL to the command's process group. Ordinary child processes in that group are included. A descendant that deliberately creates its own session or process group can survive. After the direct child exits, capture drains for at most one additional second; pipes still held open at that point are closed, remaining processes in the original group are killed, and the result is marked `output_truncated`. + +An exec timeout returns HTTP 422 with `code: process_timeout`, `message`, `exit_code`, `duration_ms`, and partial `stdout_b64` / `stderr_b64`. Output quota, capture failure, and cancellation also use 422, with `output_limit`, `output_error`, or `process_canceled`. These execution failures must not be retried automatically because the command may already have performed side effects. An ordinary nonzero command exit remains HTTP 200 with its exit code. + +Manual `/kill` requests continue to signal only the requested process, using the specified signal. + +Spawn keeps running after its HTTP request ends, but still honors its execution timeout and server shutdown. Status and the stream's exit event report why it stopped. + +These are the image server's HTTP contracts. Callers that proxy this API must forward `timeout_sec`, preserve 422 responses and their fields, and forward replay cursors to expose the same behavior. diff --git a/server/e2e/e2e_process_test.go b/server/e2e/e2e_process_test.go new file mode 100644 index 000000000..dd056bf77 --- /dev/null +++ b/server/e2e/e2e_process_test.go @@ -0,0 +1,224 @@ +package e2e + +import ( + "bufio" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "testing" + "time" + + "github.com/coder/websocket" + oapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/samber/lo" + "github.com/stretchr/testify/require" +) + +func TestProcessLifecycle(t *testing.T) { + for _, image := range []string{headlessImage, headfulImage} { + t.Run(image, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + c := NewTestContainer(t, image) + require.NoError(t, c.Start(ctx, ContainerConfig{Env: map[string]string{ + "PROCESS_OUTPUT_MAX_BYTES": "8388608", "PROCESS_OUTPUT_TOTAL_MAX_BYTES": "16777216", + }})) + defer c.Stop(context.Background()) + require.NoError(t, c.WaitReady(ctx)) + client, err := c.APIClient() + require.NoError(t, err) + + t.Run("fast exits preserve status", func(t *testing.T) { + for i := 0; i < 20; i++ { + result, err := client.ProcessExecWithResponse(ctx, oapi.ProcessExecRequest{Command: "sh", Args: lo.ToPtr([]string{"-c", fmt.Sprintf("printf out; printf err >&2; exit %d", i)})}) + require.NoError(t, err) + require.Equal(t, http.StatusOK, result.StatusCode(), "%s", result.Body) + require.Equal(t, i, *result.JSON200.ExitCode) + require.Equal(t, "b3V0", *result.JSON200.StdoutB64) + require.Equal(t, "ZXJy", *result.JSON200.StderrB64) + } + }) + + t.Run("unread output and explicit replay", func(t *testing.T) { + spawn, err := client.ProcessSpawnWithResponse(ctx, oapi.ProcessSpawnRequest{Command: "sh", Args: lo.ToPtr([]string{"-c", "head -c 4194304 /dev/zero; printf tail; printf stderr >&2"})}) + require.NoError(t, err) + require.NotNil(t, spawn.JSON200, "%s", spawn.Body) + id := spawn.JSON200.ProcessId + status := waitImageProcessExit(t, ctx, client, *spawn.JSON200) + require.Equal(t, 0, *status.ExitCode) + require.False(t, *status.OutputTruncated) + response, err := client.ProcessStdoutStream(ctx, id.String(), &oapi.ProcessStdoutStreamParams{}) + require.NoError(t, err) + out, errOut, events := readImageProcessStream(t, response) + require.Equal(t, strings.Repeat("\x00", 4<<20)+"tail", out) + require.Equal(t, "stderr", errOut) + requireImageExitEvent(t, events, 0) + + response, err = client.ProcessStdoutStream(ctx, id.String(), &oapi.ProcessStdoutStreamParams{}) + require.NoError(t, err) + out, errOut, events = readImageProcessStream(t, response) + require.Empty(t, out) + require.Empty(t, errOut) + require.Empty(t, events) + + response, err = client.ProcessStdoutStream(ctx, id.String(), &oapi.ProcessStdoutStreamParams{Offset: lo.ToPtr(int64(0))}) + require.NoError(t, err) + out, errOut, events = readImageProcessStream(t, response) + require.Equal(t, strings.Repeat("\x00", 4<<20)+"tail", out) + require.Equal(t, "stderr", errOut) + requireImageExitEvent(t, events, 0) + // Resume just after the first event and compare the remaining records. + response, err = client.ProcessStdoutStream(ctx, id.String(), &oapi.ProcessStdoutStreamParams{ + Offset: lo.ToPtr(int64(0)), LastEventID: lo.ToPtr(strconv.FormatInt(*events[0].Offset, 10)), + }) + require.NoError(t, err) + _, _, resumed := readImageProcessStream(t, response) + require.Equal(t, events[1:], resumed) + }) + + t.Run("exec deadlines stop children", func(t *testing.T) { + for _, script := range []string{"sleep 30 & echo $!; wait", "sleep 30 & echo $!"} { + result, err := client.ProcessExecWithResponse(ctx, oapi.ProcessExecRequest{Command: "sh", Args: lo.ToPtr([]string{"-c", script}), TimeoutSec: lo.ToPtr(1)}) + require.NoError(t, err) + require.Equal(t, http.StatusUnprocessableEntity, result.StatusCode(), "%s", result.Body) + require.Equal(t, oapi.ProcessTimeout, result.JSON422.Code) + data, err := base64.StdEncoding.DecodeString(*result.JSON422.StdoutB64) + require.NoError(t, err) + pid, err := strconv.Atoi(strings.TrimSpace(string(data))) + require.NoError(t, err) + require.Eventually(t, func() bool { + code, _, err := c.Exec(ctx, []string{"sh", "-c", fmt.Sprintf("test ! -e /proc/%d/stat || grep -q ') Z ' /proc/%d/stat", pid, pid)}) + return err == nil && code == 0 + }, 2*time.Second, 20*time.Millisecond, "child %d remains alive", pid) + } + }) + + t.Run("spawn deadlines include tty", func(t *testing.T) { + for _, tty := range []bool{false, true} { + spawn, err := client.ProcessSpawnWithResponse(ctx, oapi.ProcessSpawnRequest{Command: "sleep", Args: lo.ToPtr([]string{"30"}), TimeoutSec: lo.ToPtr(1), AllocateTty: &tty}) + require.NoError(t, err) + require.NotNil(t, spawn.JSON200, "%s", spawn.Body) + status := waitImageProcessExit(t, ctx, client, *spawn.JSON200) + require.Equal(t, oapi.ProcessTimeout, *status.TerminationReason) + response, err := client.ProcessStdoutStream(ctx, spawn.JSON200.ProcessId.String(), &oapi.ProcessStdoutStreamParams{}) + require.NoError(t, err) + _, _, events := readImageProcessStream(t, response) + requireImageExitEvent(t, events, -1) + require.Equal(t, oapi.ProcessTimeout, *events[len(events)-1].TerminationReason) + } + }) + + t.Run("quota failure is present at first exited status", func(t *testing.T) { + spawn, err := client.ProcessSpawnWithResponse(ctx, oapi.ProcessSpawnRequest{Command: "head", Args: lo.ToPtr([]string{"-c", "12582912", "/dev/zero"})}) + require.NoError(t, err) + require.NotNil(t, spawn.JSON200, "%s", spawn.Body) + status := waitImageProcessExit(t, ctx, client, *spawn.JSON200) + require.NotNil(t, status.TerminationReason) + require.Equal(t, oapi.OutputLimit, *status.TerminationReason) + require.True(t, *status.OutputTruncated) + response, err := client.ProcessStdoutStream(ctx, spawn.JSON200.ProcessId.String(), &oapi.ProcessStdoutStreamParams{}) + require.NoError(t, err) + out, _, events := readImageProcessStream(t, response) + require.NotEmpty(t, out) + require.Less(t, len(out), 8<<20) + require.Equal(t, oapi.OutputLimit, *events[len(events)-1].TerminationReason) + require.True(t, *events[len(events)-1].OutputTruncated) + }) + }) + } +} + +func TestProcessTTYWithoutOutputStorage(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{Env: map[string]string{"PROCESS_OUTPUT_DIR": "/proc/unavailable"}})) + defer c.Stop(context.Background()) + require.NoError(t, c.WaitReady(ctx)) + client, err := c.APIClient() + require.NoError(t, err) + spawn, err := client.ProcessSpawnWithResponse(ctx, oapi.ProcessSpawnRequest{ + Command: "sh", Args: lo.ToPtr([]string{"-c", "read line; printf 'reply:%s\\n' \"$line\"; read line"}), + AllocateTty: lo.ToPtr(true), Rows: lo.ToPtr(24), Cols: lo.ToPtr(80), + }) + require.NoError(t, err) + require.NotNil(t, spawn.JSON200, "%s", spawn.Body) + defer client.ProcessKillWithResponse(context.Background(), spawn.JSON200.ProcessId.String(), oapi.ProcessKillRequest{Signal: "KILL"}) + url := strings.Replace(c.APIBaseURL(), "http", "ws", 1) + "/process/" + spawn.JSON200.ProcessId.String() + "/attach" + conn, _, err := websocket.Dial(ctx, url, nil) + require.NoError(t, err) + defer conn.CloseNow() + resized, err := client.ProcessResizeWithResponse(ctx, spawn.JSON200.ProcessId.String(), oapi.ProcessResizeRequest{Rows: 30, Cols: 100}) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resized.StatusCode()) + require.NoError(t, conn.Write(ctx, websocket.MessageBinary, []byte("hello\n"))) + var output strings.Builder + for !strings.Contains(output.String(), "reply:hello") { + kind, data, err := conn.Read(ctx) + require.NoError(t, err) + if kind == websocket.MessageBinary { + output.Write(data) + } + } + // Non-TTY capture still fails before running commands when storage is unavailable. + result, err := client.ProcessExecWithResponse(ctx, oapi.ProcessExecRequest{Command: "true"}) + require.NoError(t, err) + require.Equal(t, http.StatusInternalServerError, result.StatusCode()) +} + +func waitImageProcessExit(t *testing.T, ctx context.Context, client *oapi.ClientWithResponses, spawn oapi.ProcessSpawnResult) *oapi.ProcessStatus { + t.Helper() + var status *oapi.ProcessStatus + require.Eventually(t, func() bool { + response, err := client.ProcessStatusWithResponse(ctx, spawn.ProcessId.String()) + require.NoError(t, err) + require.NotNil(t, response.JSON200, "%s", response.Body) + status = response.JSON200 + return *status.State == "exited" + }, 5*time.Second, 10*time.Millisecond) + return status +} + +func readImageProcessStream(t *testing.T, response *http.Response) (string, string, []oapi.ProcessStreamEvent) { + t.Helper() + defer response.Body.Close() + require.Equal(t, http.StatusOK, response.StatusCode) + var out, errOut strings.Builder + events := make([]oapi.ProcessStreamEvent, 0) + scanner := bufio.NewScanner(response.Body) + for scanner.Scan() { + if !strings.HasPrefix(scanner.Text(), "data: ") { + continue + } + var event oapi.ProcessStreamEvent + require.NoError(t, json.Unmarshal([]byte(strings.TrimPrefix(scanner.Text(), "data: ")), &event)) + events = append(events, event) + if event.DataB64 != nil { + data, err := base64.StdEncoding.DecodeString(*event.DataB64) + require.NoError(t, err) + if *event.Stream == "stdout" { + out.Write(data) + } else { + errOut.Write(data) + } + } + } + require.NoError(t, scanner.Err()) + return out.String(), errOut.String(), events +} + +func requireImageExitEvent(t *testing.T, events []oapi.ProcessStreamEvent, code int) { + t.Helper() + require.NotEmpty(t, events) + last := events[len(events)-1] + require.NotNil(t, last.Event) + require.Equal(t, oapi.Exit, *last.Event) + require.Equal(t, code, *last.ExitCode) + for _, event := range events[:len(events)-1] { + require.Nil(t, event.Event) + } +} diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index ab3d26c60..1580789eb 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -2891,6 +2891,30 @@ func (e ProcessStreamEventStream) Valid() bool { } } +// Defines values for ProcessTerminationReason. +const ( + OutputError ProcessTerminationReason = "output_error" + OutputLimit ProcessTerminationReason = "output_limit" + ProcessCanceled ProcessTerminationReason = "process_canceled" + ProcessTimeout ProcessTerminationReason = "process_timeout" +) + +// Valid indicates whether the value is a known member of the ProcessTerminationReason enum. +func (e ProcessTerminationReason) Valid() bool { + switch e { + case OutputError: + return true + case OutputLimit: + return true + case ProcessCanceled: + return true + case ProcessTimeout: + return true + default: + return false + } +} + // Defines values for PublishEventRequestCategory. const ( PublishEventRequestCategoryCaptcha PublishEventRequestCategory = "captcha" @@ -6687,6 +6711,27 @@ type PressKeyRequest struct { Keys []string `json:"keys"` } +// ProcessExecFailure defines model for ProcessExecFailure. +type ProcessExecFailure struct { + Code ProcessTerminationReason `json:"code"` + + // DurationMs Execution duration in milliseconds. + DurationMs *int `json:"duration_ms,omitempty"` + + // ExitCode Process exit code. + ExitCode *int `json:"exit_code,omitempty"` + Message string `json:"message"` + + // OutputTruncated Output could not be fully captured due to a quota, storage error, or a descendant holding a pipe open past the drain deadline. + OutputTruncated *bool `json:"output_truncated,omitempty"` + + // StderrB64 Base64-encoded stderr buffer. + StderrB64 *string `json:"stderr_b64,omitempty"` + + // StdoutB64 Base64-encoded stdout buffer. + StdoutB64 *string `json:"stdout_b64,omitempty"` +} + // ProcessExecRequest Request to execute a command synchronously. type ProcessExecRequest struct { // Args Command arguments. @@ -6707,11 +6752,11 @@ type ProcessExecRequest struct { // Env Environment variables to set for the process. Env *map[string]string `json:"env,omitempty"` - // TimeoutSec Maximum execution time in seconds. + // TimeoutSec Maximum execution time in seconds, for exec and spawn. Null or zero disables the deadline. Cancellation stops the process group; detached descendants may survive. TimeoutSec *int `json:"timeout_sec,omitempty"` } -// ProcessExecResult Result of a synchronous command execution. +// ProcessExecResult Result of a synchronous command execution. Output is bounded by the configured process output quotas. type ProcessExecResult struct { // DurationMs Execution duration in milliseconds. DurationMs *int `json:"duration_ms,omitempty"` @@ -6719,6 +6764,9 @@ type ProcessExecResult struct { // ExitCode Process exit code. ExitCode *int `json:"exit_code,omitempty"` + // OutputTruncated Output could not be fully captured due to a quota, storage error, or a descendant holding a pipe open past the drain deadline. + OutputTruncated *bool `json:"output_truncated,omitempty"` + // StderrB64 Base64-encoded stderr buffer. StderrB64 *string `json:"stderr_b64,omitempty"` @@ -6773,7 +6821,7 @@ type ProcessSpawnRequest struct { // Rows Initial terminal rows when allocate_tty is true. Rows *int `json:"rows,omitempty"` - // TimeoutSec Maximum execution time in seconds. + // TimeoutSec Maximum execution time in seconds, for exec and spawn. Null or zero disables the deadline. Cancellation stops the process group; detached descendants may survive. TimeoutSec *int `json:"timeout_sec,omitempty"` } @@ -6800,8 +6848,12 @@ type ProcessStatus struct { // MemBytes Estimated resident memory usage in bytes. MemBytes *int `json:"mem_bytes,omitempty"` + // OutputTruncated Some output was not captured. + OutputTruncated *bool `json:"output_truncated,omitempty"` + // State Process state. - State *ProcessStatusState `json:"state,omitempty"` + State *ProcessStatusState `json:"state,omitempty"` + TerminationReason *ProcessTerminationReason `json:"termination_reason,omitempty"` } // ProcessStatusState Process state. @@ -6830,8 +6882,15 @@ type ProcessStreamEvent struct { // ExitCode Exit code when the event is "exit". ExitCode *int `json:"exit_code,omitempty"` + // Offset Opaque resume cursor after this data event. Also sent as the SSE id. + Offset *int64 `json:"offset,omitempty"` + + // OutputTruncated Some output was not captured. + OutputTruncated *bool `json:"output_truncated,omitempty"` + // Stream Source stream of the data chunk. - Stream *ProcessStreamEventStream `json:"stream,omitempty"` + Stream *ProcessStreamEventStream `json:"stream,omitempty"` + TerminationReason *ProcessTerminationReason `json:"termination_reason,omitempty"` } // ProcessStreamEventEvent Lifecycle event type. @@ -6840,6 +6899,9 @@ type ProcessStreamEventEvent string // ProcessStreamEventStream Source stream of the data chunk. type ProcessStreamEventStream string +// ProcessTerminationReason defines model for ProcessTerminationReason. +type ProcessTerminationReason string + // PublishEventRequest Request body for publishing an event into the telemetry stream. type PublishEventRequest struct { // Category Event category. Optional and advisory: for a known event `type` the server assigns the category authoritatively and ignores this field. It is only used for unknown custom types, where it is required. @@ -7322,6 +7384,15 @@ type DeletePlaywrightExecutorParams struct { CloseTab *bool `form:"close_tab,omitempty" json:"close_tab,omitempty"` } +// ProcessStdoutStreamParams defines parameters for ProcessStdoutStream. +type ProcessStdoutStreamParams struct { + // Offset Resume cursor from a prior output event, or 0 to replay all retained output. Used when Last-Event-ID is absent. + Offset *int64 `form:"offset,omitempty" json:"offset,omitempty"` + + // LastEventID Resume cursor from a prior SSE data event. Takes precedence over offset so automatic reconnects resume after the last received event. + LastEventID *string `json:"Last-Event-ID,omitempty"` +} + // DownloadRecordingParams defines parameters for DownloadRecording. type DownloadRecordingParams struct { // Id Identifier of the recording session to download, as passed to /recording/start. When omitted, the default recording session is downloaded. @@ -10120,7 +10191,7 @@ type ClientInterface interface { ProcessStdin(ctx context.Context, processId string, body ProcessStdinJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) // ProcessStdoutStream request - ProcessStdoutStream(ctx context.Context, processId string, reqEditors ...RequestEditorFn) (*http.Response, error) + ProcessStdoutStream(ctx context.Context, processId string, params *ProcessStdoutStreamParams, reqEditors ...RequestEditorFn) (*http.Response, error) // DeleteRecordingWithBody request with any body DeleteRecordingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -11117,8 +11188,8 @@ func (c *Client) ProcessStdin(ctx context.Context, processId string, body Proces return c.Client.Do(req) } -func (c *Client) ProcessStdoutStream(ctx context.Context, processId string, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewProcessStdoutStreamRequest(c.Server, processId) +func (c *Client) ProcessStdoutStream(ctx context.Context, processId string, params *ProcessStdoutStreamParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewProcessStdoutStreamRequest(c.Server, processId, params) if err != nil { return nil, err } @@ -13489,7 +13560,7 @@ func NewProcessStdinRequestWithBody(server string, processId string, contentType } // NewProcessStdoutStreamRequest generates requests for ProcessStdoutStream -func NewProcessStdoutStreamRequest(server string, processId string) (*http.Request, error) { +func NewProcessStdoutStreamRequest(server string, processId string, params *ProcessStdoutStreamParams) (*http.Request, error) { var err error var pathParam0 string @@ -13514,11 +13585,48 @@ func NewProcessStdoutStreamRequest(server string, processId string) (*http.Reque return nil, err } + if params != nil { + queryValues := queryURL.Query() + + if params.Offset != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "offset", *params.Offset, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: "int64"}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err } + if params != nil { + + if params.LastEventID != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithOptions("simple", false, "Last-Event-ID", *params.LastEventID, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + req.Header.Set("Last-Event-ID", headerParam0) + } + + } + return req, nil } @@ -14538,7 +14646,7 @@ type ClientWithResponsesInterface interface { ProcessStdinWithResponse(ctx context.Context, processId string, body ProcessStdinJSONRequestBody, reqEditors ...RequestEditorFn) (*ProcessStdinResponse, error) // ProcessStdoutStreamWithResponse request - ProcessStdoutStreamWithResponse(ctx context.Context, processId string, reqEditors ...RequestEditorFn) (*ProcessStdoutStreamResponse, error) + ProcessStdoutStreamWithResponse(ctx context.Context, processId string, params *ProcessStdoutStreamParams, reqEditors ...RequestEditorFn) (*ProcessStdoutStreamResponse, error) // DeleteRecordingWithBodyWithResponse request with any body DeleteRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DeleteRecordingResponse, error) @@ -15648,6 +15756,7 @@ type ProcessExecResponse struct { HTTPResponse *http.Response JSON200 *ProcessExecResult JSON400 *BadRequestError + JSON422 *ProcessExecFailure JSON500 *InternalError } @@ -16951,8 +17060,8 @@ func (c *ClientWithResponses) ProcessStdinWithResponse(ctx context.Context, proc } // ProcessStdoutStreamWithResponse request returning *ProcessStdoutStreamResponse -func (c *ClientWithResponses) ProcessStdoutStreamWithResponse(ctx context.Context, processId string, reqEditors ...RequestEditorFn) (*ProcessStdoutStreamResponse, error) { - rsp, err := c.ProcessStdoutStream(ctx, processId, reqEditors...) +func (c *ClientWithResponses) ProcessStdoutStreamWithResponse(ctx context.Context, processId string, params *ProcessStdoutStreamParams, reqEditors ...RequestEditorFn) (*ProcessStdoutStreamResponse, error) { + rsp, err := c.ProcessStdoutStream(ctx, processId, params, reqEditors...) if err != nil { return nil, err } @@ -18870,6 +18979,13 @@ func ParseProcessExecResponse(rsp *http.Response) (*ProcessExecResponse, error) } response.JSON400 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: + var dest ProcessExecFailure + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON422 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -20082,9 +20198,9 @@ type ServerInterface interface { // Write to process stdin // (POST /process/{process_id}/stdin) ProcessStdin(w http.ResponseWriter, r *http.Request, processId string) - // Stream process stdout over SSE + // Stream retained process stdout and stderr over SSE // (GET /process/{process_id}/stdout/stream) - ProcessStdoutStream(w http.ResponseWriter, r *http.Request, processId string) + ProcessStdoutStream(w http.ResponseWriter, r *http.Request, processId string, params ProcessStdoutStreamParams) // Delete a previously recorded video file // (POST /recording/delete) DeleteRecording(w http.ResponseWriter, r *http.Request) @@ -20445,9 +20561,9 @@ func (_ Unimplemented) ProcessStdin(w http.ResponseWriter, r *http.Request, proc w.WriteHeader(http.StatusNotImplemented) } -// Stream process stdout over SSE +// Stream retained process stdout and stderr over SSE // (GET /process/{process_id}/stdout/stream) -func (_ Unimplemented) ProcessStdoutStream(w http.ResponseWriter, r *http.Request, processId string) { +func (_ Unimplemented) ProcessStdoutStream(w http.ResponseWriter, r *http.Request, processId string, params ProcessStdoutStreamParams) { w.WriteHeader(http.StatusNotImplemented) } @@ -21561,8 +21677,40 @@ func (siw *ServerInterfaceWrapper) ProcessStdoutStream(w http.ResponseWriter, r return } + // Parameter object where we will unmarshal all parameters from the context + var params ProcessStdoutStreamParams + + // ------------- Optional query parameter "offset" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) + return + } + + headers := r.Header + + // ------------- Optional header parameter "Last-Event-ID" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Last-Event-ID")]; found { + var LastEventID string + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Last-Event-ID", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Last-Event-ID", valueList[0], &LastEventID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false, Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Last-Event-ID", Err: err}) + return + } + + params.LastEventID = &LastEventID + + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.ProcessStdoutStream(w, r, processId) + siw.Handler.ProcessStdoutStream(w, r, processId, params) })) for _, middleware := range siw.HandlerMiddlewares { @@ -24013,6 +24161,15 @@ func (response ProcessExec400JSONResponse) VisitProcessExecResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type ProcessExec422JSONResponse ProcessExecFailure + +func (response ProcessExec422JSONResponse) VisitProcessExecResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(422) + + return json.NewEncoder(w).Encode(response) +} + type ProcessExec500JSONResponse struct{ InternalErrorJSONResponse } func (response ProcessExec500JSONResponse) VisitProcessExecResponse(w http.ResponseWriter) error { @@ -24238,6 +24395,7 @@ func (response ProcessStdin500JSONResponse) VisitProcessStdinResponse(w http.Res type ProcessStdoutStreamRequestObject struct { ProcessId string `json:"process_id"` + Params ProcessStdoutStreamParams } type ProcessStdoutStreamResponseObject interface { @@ -25292,7 +25450,7 @@ type StrictServerInterface interface { // Write to process stdin // (POST /process/{process_id}/stdin) ProcessStdin(ctx context.Context, request ProcessStdinRequestObject) (ProcessStdinResponseObject, error) - // Stream process stdout over SSE + // Stream retained process stdout and stderr over SSE // (GET /process/{process_id}/stdout/stream) ProcessStdoutStream(ctx context.Context, request ProcessStdoutStreamRequestObject) (ProcessStdoutStreamResponseObject, error) // Delete a previously recorded video file @@ -26830,10 +26988,11 @@ func (sh *strictHandler) ProcessStdin(w http.ResponseWriter, r *http.Request, pr } // ProcessStdoutStream operation middleware -func (sh *strictHandler) ProcessStdoutStream(w http.ResponseWriter, r *http.Request, processId string) { +func (sh *strictHandler) ProcessStdoutStream(w http.ResponseWriter, r *http.Request, processId string, params ProcessStdoutStreamParams) { var request ProcessStdoutStreamRequestObject request.ProcessId = processId + request.Params = params handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { return sh.ssi.ProcessStdoutStream(ctx, request.(ProcessStdoutStreamRequestObject)) @@ -27435,742 +27594,758 @@ func (sh *strictHandler) GetWebMCPTools(w http.ResponseWriter, r *http.Request, // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "1MeP//8BAL76igKi8gMA", + "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", + "dO7Ll/8vAAD//2r2liqB/QMA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/lib/scaletozero/connection.go b/server/lib/scaletozero/connection.go index 2b7dbf4bd..9e529be54 100644 --- a/server/lib/scaletozero/connection.go +++ b/server/lib/scaletozero/connection.go @@ -33,6 +33,7 @@ type drainConn struct { closing bool hijackedConn bool writeTimeout time.Duration + writeDeadline time.Time setDeadline func(*net.TCPConn, time.Time) error abortConnection func(*net.TCPConn) error } @@ -77,7 +78,7 @@ func (c *drainConn) Write(p []byte) (int, error) { return total + n, err } if timeout > 0 { - if err := setDeadline(c.TCPConn, time.Now().Add(timeout)); err != nil { + if err := c.refreshWriteDeadline(timeout, setDeadline); err != nil { c.abortNowWithOutcome(abort, responseDrainWriteError, err) return total, err } @@ -113,7 +114,7 @@ func (c *drainConn) ReadFrom(r io.Reader) (int64, error) { return total + n, err } if timeout > 0 { - if err := setDeadline(c.TCPConn, time.Now().Add(timeout)); err != nil { + if err := c.refreshWriteDeadline(timeout, setDeadline); err != nil { c.abortNowWithOutcome(abort, responseDrainWriteError, err) return total, err } @@ -145,6 +146,24 @@ func (c *drainConn) ReadFrom(r io.Reader) (int64, error) { } } +func (c *drainConn) SetWriteDeadline(deadline time.Time) error { + c.mu.Lock() + defer c.mu.Unlock() + c.writeDeadline = deadline + return c.TCPConn.SetWriteDeadline(deadline) +} + +func (c *drainConn) refreshWriteDeadline(timeout time.Duration, setDeadline func(*net.TCPConn, time.Time) error) error { + c.mu.Lock() + defer c.mu.Unlock() + deadline := time.Now().Add(timeout) + // ResponseController deadlines must survive the per-write timeout refresh. + if !c.writeDeadline.IsZero() && c.writeDeadline.Before(deadline) { + deadline = c.writeDeadline + } + return setDeadline(c.TCPConn, deadline) +} + func (c *drainConn) writeConfig() (time.Duration, func(*net.TCPConn, time.Time) error, func(*net.TCPConn) error, bool, error) { c.mu.Lock() defer c.mu.Unlock() diff --git a/server/lib/scaletozero/middleware_test.go b/server/lib/scaletozero/middleware_test.go index a0304cb65..a6ca985c0 100644 --- a/server/lib/scaletozero/middleware_test.go +++ b/server/lib/scaletozero/middleware_test.go @@ -686,3 +686,34 @@ func (c *signalController) Enable(context.Context) error { c.once.Do(func() { c.enabled <- time.Now() }) return nil } + +func TestMiddlewarePreservesHandlerWriteDeadline(t *testing.T) { + handlerDone := make(chan error, 1) + handler := middleware(NewNoopController(), testDrainConfig(5*time.Minute, outboundQueue))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := http.NewResponseController(w).SetWriteDeadline(time.Now()); err != nil { + handlerDone <- err + return + } + _, err := io.Copy(w, bytes.NewReader(make([]byte, 32<<20))) + handlerDone <- err + })) + listener, serverDone, server := serveTestHandler(t, handler) + defer func() { + require.NoError(t, server.Close()) + assert.ErrorIs(t, <-serverDone, http.ErrServerClosed) + }() + conn, err := net.Dial("tcp", listener.Addr().String()) + require.NoError(t, err) + defer conn.Close() + require.NoError(t, conn.(*net.TCPConn).SetReadBuffer(1024)) + _, err = fmt.Fprint(conn, "GET /stream HTTP/1.1\r\nHost: test\r\n\r\n") + require.NoError(t, err) + select { + case err := <-handlerDone: + var timeout net.Error + require.ErrorAs(t, err, &timeout) + require.True(t, timeout.Timeout()) + case <-time.After(time.Second): + t.Fatal("connection overwrote the handler's expired write deadline") + } +} diff --git a/server/openapi.yaml b/server/openapi.yaml index 0a36a8691..a1da52d1a 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -44,6 +44,12 @@ paths: $ref: "#/components/schemas/ProcessExecResult" "400": $ref: "#/components/responses/BadRequestError" + "422": + description: Execution stopped. The response includes captured output and must not be retried automatically. + content: + application/json: + schema: + $ref: "#/components/schemas/ProcessExecFailure" "500": $ref: "#/components/responses/InternalError" /process/spawn: @@ -94,7 +100,14 @@ paths: $ref: "#/components/responses/InternalError" /process/{process_id}/stdout/stream: get: - summary: Stream process stdout over SSE + summary: Stream retained process stdout and stderr over SSE + description: | + Output is retained until 10 seconds after completion. Without a cursor, + connections share consumption: reconnects continue unread output and do not + repeat a consumed exit event. Supply offset=0 to replay from the beginning, + or an offset or Last-Event-ID from a previous data event to resume independently. + Exit follows the retained output. Active streams also end when retention + expires. PTY output is available through attach. operationId: processStdoutStream x-telemetry-category: platform parameters: @@ -103,6 +116,18 @@ paths: required: true schema: type: string + - name: offset + in: query + description: Resume cursor from a prior output event, or 0 to replay all retained output. Used when Last-Event-ID is absent. + schema: + type: integer + format: int64 + minimum: 0 + - name: Last-Event-ID + in: header + description: Resume cursor from a prior SSE data event. Takes precedence over offset so automatic reconnects resume after the last received event. + schema: + type: string responses: "200": description: SSE stream of process output and lifecycle events @@ -7595,11 +7620,13 @@ components: default: false timeout_sec: type: [integer, "null"] - description: Maximum execution time in seconds. + description: Maximum execution time in seconds, for exec and spawn. Null or zero disables the deadline. Cancellation stops the process group; detached descendants may survive. + minimum: 0 + maximum: 9223372036 additionalProperties: false ProcessExecResult: type: object - description: Result of a synchronous command execution. + description: Result of a synchronous command execution. Output is bounded by the configured process output quotas. properties: exit_code: type: integer @@ -7613,6 +7640,36 @@ components: duration_ms: type: integer description: Execution duration in milliseconds. + output_truncated: + type: boolean + description: Output could not be fully captured due to a quota, storage error, or a descendant holding a pipe open past the drain deadline. + additionalProperties: false + ProcessTerminationReason: + type: string + enum: [process_timeout, output_limit, process_canceled, output_error] + ProcessExecFailure: + type: object + required: [code, message] + properties: + code: + $ref: "#/components/schemas/ProcessTerminationReason" + message: + type: string + exit_code: + type: integer + description: Process exit code. + stdout_b64: + type: string + description: Base64-encoded stdout buffer. + stderr_b64: + type: string + description: Base64-encoded stderr buffer. + duration_ms: + type: integer + description: Execution duration in milliseconds. + output_truncated: + type: boolean + description: Output could not be fully captured due to a quota, storage error, or a descendant holding a pipe open past the drain deadline. additionalProperties: false ProcessSpawnRequest: allOf: @@ -7678,6 +7735,11 @@ components: mem_bytes: type: integer description: Estimated resident memory usage in bytes. + termination_reason: + $ref: "#/components/schemas/ProcessTerminationReason" + output_truncated: + type: boolean + description: Some output was not captured. additionalProperties: false ProcessStdinRequest: type: object @@ -7711,6 +7773,10 @@ components: type: object description: SSE payload representing process output or lifecycle events. properties: + offset: + type: integer + format: int64 + description: Opaque resume cursor after this data event. Also sent as the SSE id. stream: type: string description: Source stream of the data chunk. @@ -7725,6 +7791,11 @@ components: exit_code: type: integer description: Exit code when the event is "exit". + termination_reason: + $ref: "#/components/schemas/ProcessTerminationReason" + output_truncated: + type: boolean + description: Some output was not captured. additionalProperties: false PatchDisplayRequest: type: object