execute: timeout=0 means no timeout

Allows long-running daemons to be started without a deadline.
timeout < 0 still defaults to 30s. Schema descriptions updated.
This commit is contained in:
Levi Neely 2026-07-17 17:06:12 +02:00
parent 5df603a9e3
commit c0a52a8031
2 changed files with 30 additions and 5 deletions

View File

@ -517,6 +517,25 @@ func TestRunReadBatch_WithLockDir(t *testing.T) {
}
}
func TestTimeoutZeroNoDeadline(t *testing.T) {
s := newServer(t)
ctx := context.Background()
// timeout=0 should not kill the process after 30s default.
// We verify by running a 2s sleep with timeout=0 and detaching it.
// If timeout were applied, it would use default 30s which is fine,
// but we confirm it doesn't fail with timeout=0.
done := make(chan string, 1)
go func() {
out, _ := s.Execute(ctx, "echo alive; sleep 1; echo done", "bash", 0, "default", true)
done <- out
}()
out := <-done
if !strings.Contains(out, "done") {
t.Errorf("expected 'done' in output, got: %q", out)
}
}
// ---- detach ----
func TestDetach(t *testing.T) {

View File

@ -191,7 +191,7 @@ Examples:
}
}
},
"timeout": {"type": "integer", "description": "Timeout in seconds per step (default: 30)."},
"timeout": {"type": "integer", "description": "Timeout in seconds per step (default: 30). Use 0 for no timeout."},
"sandbox": {"type": "string", "description": "Sandbox name (default: default)."}
}
}`),
@ -244,7 +244,7 @@ Examples:
}
}
},
"timeout": {"type": "integer", "description": "Timeout in seconds per call (default: 30)."},
"timeout": {"type": "integer", "description": "Timeout in seconds per call (default: 30). Use 0 for no timeout."},
"sandbox": {"type": "string", "description": "Sandbox name (default: default)."}
}
}`),
@ -301,7 +301,7 @@ Examples:
}
}
},
"timeout": {"type": "integer", "description": "Timeout in seconds per stage (default: 30)."},
"timeout": {"type": "integer", "description": "Timeout in seconds per stage (default: 30). Use 0 for no timeout."},
"sandbox": {"type": "string", "description": "Sandbox name (default: default)."}
}
}`),
@ -484,7 +484,7 @@ func (e *Server) Execute(ctx context.Context, code, language string, timeout int
// executeWithStdin is like Execute but feeds stdinData to the command's stdin.
// For languages where code is itself passed via stdin (ed, expect, bc), stdinData is ignored.
func (e *Server) executeWithStdin(ctx context.Context, code, language string, timeout int, sandboxName string, trusted bool, stdinData string) (string, error) {
if timeout <= 0 {
if timeout < 0 {
timeout = 30
}
@ -512,7 +512,13 @@ func (e *Server) executeWithStdin(ctx context.Context, code, language string, ti
workDir, _ = os.Getwd()
}
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
var cancel context.CancelFunc
if timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
} else {
// timeout=0 means no timeout (run indefinitely, e.g. for daemons)
ctx, cancel = context.WithCancel(ctx)
}
// NOTE: do NOT defer cancel() here — the detach path must avoid cancelling
// the context (which would kill the detached process via cmd.Cancel).
// Each exit path calls cancel() explicitly except detach.