diff --git a/pkg/tools/execute/execute_test.go b/pkg/tools/execute/execute_test.go index f8d3d22..2b4a7a1 100644 --- a/pkg/tools/execute/execute_test.go +++ b/pkg/tools/execute/execute_test.go @@ -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) { diff --git a/pkg/tools/execute/server.go b/pkg/tools/execute/server.go index f4d2f79..5645197 100644 --- a/pkg/tools/execute/server.go +++ b/pkg/tools/execute/server.go @@ -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.