返回 DeepSeek-Reasonix
bash_background.go
根目录 / internal / tool / builtin / bash_background.go
1 package builtin
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "os/exec"
9 "strings"
10 "time"
11
12 "reasonix/internal/jobs"
13 "reasonix/internal/proc"
14 "reasonix/internal/sandbox"
15 "reasonix/internal/sessiontemp"
16 "reasonix/internal/shellrun"
17 "reasonix/internal/tool"
18 )
19
20 func (b bash) startBackground(ctx context.Context, p bashParams, sh sandbox.Shell, argv []string, wrapped bool, cmdEnv []string, lease *sessiontemp.Lease, releaseLease *bool, start time.Time, ex *tool.ShellExecution) (tool.DetailedResult, error) {
21 jm, ok := jobs.FromContext(ctx)
22 if !ok {
23 ex.State = tool.ShellStateNotRun
24 ex.FailurePhase = tool.ShellPhaseDependency
25 ex.MutationRisk = tool.ShellMutationNotStarted
26 ex.DurationMs = time.Since(start).Milliseconds()
27 return tool.DetailedResult{Execution: ex}, fmt.Errorf("background execution is not available in this context")
28 }
29
30 // The job closure owns the lease after StartForSession. It releases the
31 // generation after either process completion or a start failure.
32 jobLease := lease
33 *releaseLease = false
34 jobLabel := strings.TrimSpace(p.Description)
35 if jobLabel == "" {
36 jobLabel = commandPreview(p.Command)
37 }
38 permissionPreset := string(sandbox.PermissionPresetFrom(ctx))
39 jobSpec := b.specForCall(ctx)
40 job := jm.StartForSession(jobs.SessionFromContext(ctx), b.Name(), jobLabel, func(jobCtx context.Context, out io.Writer) (string, error) {
41 if jobLease != nil {
42 defer jobLease.Release()
43 }
44 cmd := proc.CommandContext(jobCtx, argv[0], argv[1:]...)
45 cmd.Dir = b.workDir
46 cmd.Env = cmdEnv
47 cmd.WaitDelay = bashWaitDelay
48 capture := &boundedDiagnosticWriter{limit: 64 << 10}
49 combined := io.MultiWriter(out, capture)
50 cmd.Stdout = combined
51 cmd.Stderr = combined
52 started := time.Now()
53 tracked, runErr := runShellProcess(jobCtx, cmd, sh, p.Command, shouldTrackShellProcess(wrapped, sh, p.Command, p.PreserveBackgroundProcesses))
54 if shouldReapAfterRun(jobCtx, sh, p.Command, p.PreserveBackgroundProcesses) {
55 reapShellProcess(cmd, tracked)
56 }
57 runErr = normalizeBashRunError(jobCtx, runErr, p.PreserveBackgroundProcesses)
58 execution, classifiedErr := classifyBackgroundShellExecution(jobCtx, sh, argv, capture.String(), runErr, started)
59 if classifiedErr != nil && execution.FailurePhase == tool.ShellPhaseExecution && wrapped {
60 writeBackgroundSandboxHint(out, capture.String(), classifiedErr, p, jobSpec, permissionPreset)
61 }
62 jobs.SetExecution(jobCtx, execution)
63 return "", classifiedErr
64 })
65
66 msg := fmt.Sprintf("Started background job %q. It keeps running across turns; read it with job_output(job_id=%q) or stop it with job_kill(job_id=%q).", job.ID, job.ID, job.ID)
67 ex.State = tool.ShellStateBackgroundStarted
68 ex.MutationRisk = tool.ShellMutationUnknown
69 ex.DurationMs = time.Since(start).Milliseconds()
70 return tool.DetailedResult{
71 Output: appendSessionDataHint(msg, b.guard.CommandHint(b.workDir, p.Command)),
72 Execution: ex,
73 }, nil
74 }
75
76 func writeBackgroundSandboxHint(out io.Writer, captured string, runErr error, p bashParams, spec sandbox.Spec, permissionPreset string) {
77 hinted := appendSandboxWriteHint(captured, runErr, p, spec, permissionPreset)
78 if hint := strings.TrimPrefix(hinted, captured); hint != "" {
79 _, _ = io.WriteString(out, hint+"\n")
80 }
81 }
82
83 type boundedDiagnosticWriter struct {
84 buf []byte
85 limit int
86 }
87
88 func (w *boundedDiagnosticWriter) Write(p []byte) (int, error) {
89 n := len(p)
90 if w.limit <= 0 {
91 return n, nil
92 }
93 if len(p) >= w.limit {
94 w.buf = append(w.buf[:0], p[len(p)-w.limit:]...)
95 return n, nil
96 }
97 w.buf = append(w.buf, p...)
98 if overflow := len(w.buf) - w.limit; overflow > 0 {
99 copy(w.buf, w.buf[overflow:])
100 w.buf = w.buf[:w.limit]
101 }
102 return n, nil
103 }
104
105 func (w *boundedDiagnosticWriter) String() string { return string(w.buf) }
106
107 func classifyBackgroundShellExecution(ctx context.Context, sh sandbox.Shell, argv []string, output string, runErr error, started time.Time) (*tool.ShellExecution, error) {
108 ex := shellrun.DescriptorFromShell(sh)
109 ex.DurationMs = time.Since(started).Milliseconds()
110 switch {
111 case ctx.Err() != nil:
112 ex.State = tool.ShellStateCancelled
113 ex.FailurePhase = tool.ShellPhaseCancellation
114 ex.MutationRisk = tool.ShellMutationMayBePartial
115 return ex, runErr
116 case runErr == nil:
117 ex.State = tool.ShellStateCompleted
118 ex.ExitCode = tool.IntPtr(0)
119 ex.MutationRisk = tool.ShellMutationMayHaveCompleted
120 return ex, nil
121 }
122 if exitErr := (*exec.ExitError)(nil); errors.As(runErr, &exitErr) {
123 ex.State = tool.ShellStateFailed
124 ex.FailurePhase = tool.ShellPhaseExecution
125 ex.ExitCode = tool.IntPtr(exitErr.ExitCode())
126 ex.MutationRisk = tool.ShellMutationMayBePartial
127 return ex, runErr
128 }
129 ex.State = tool.ShellStateNotRun
130 ex.FailurePhase = tool.ShellPhaseLaunch
131 ex.MutationRisk = tool.ShellMutationNotStarted
132 return ex, runErr
133 }
134
134 lines GO