MCPcopy Create free account
hub / github.com/codehamr/codehamr / Bash

Function Bash

internal/tools/bash.go:38–96  ·  view source on GitHub ↗

Bash runs one shell command through /bin/sh -c and returns combined stdout+stderr. Non-zero exit is not an error; the model sees the failure and reacts. A pre-cancelled parent (Ctrl+C raced the dispatch) returns "(cancelled)" before the blank-command check, so a trivially-cancelled call isn't mista

(parent context.Context, command string, timeout time.Duration)

Source from the content-addressed store, hash-verified

36//
37// A pre-cancelled parent (Ctrl+C raced the dispatch) returns "(cancelled)"
38// before the blank-command check, so a trivially-cancelled call isn't
39// mistaken for a valid empty-args invocation.
40func Bash(parent context.Context, command string, timeout time.Duration) string {
41 if parent.Err() != nil {
42 return "(cancelled)"
43 }
44 if strings.TrimSpace(command) == "" {
45 return "(empty command)"
46 }
47 ctxT, cancel := context.WithTimeout(parent, timeout)
48 defer cancel()
49
50 cmd := exec.CommandContext(ctxT, "/bin/sh", "-c", command)
51 // Shell gets its own process group + a Cancel that kills the whole group
52 // on cancel/timeout (Unix; no-op on Windows). Without it, backgrounded
53 // children (`cmd &`) outlive the parent shell and leak.
54 setProcessGroup(cmd)
55 // Cap the wait for stdout/stderr pipes to close after /bin/sh exits.
56 // Backgrounded children inherit those pipe fds, so without this Run's
57 // pipe-copy goroutine blocks for the full timeout even though the shell
58 // is gone.
59 cmd.WaitDelay = 100 * time.Millisecond
60 // Bounded combined output instead of CombinedOutput's unbounded buffer: a
61 // high-throughput command (`cat big.iso`, `grep -r "" /`) can emit hundreds
62 // of MB/s and OOM-kill the whole TUI well before the timeout or Ctrl+C
63 // react. ctx.Truncate keeps only head+tail anyway, so nothing the model
64 // would see is lost. Stdout and Stderr get the SAME writer value, which
65 // os/exec detects and funnels through one pipe: no locking needed.
66 buf := &headTailBuffer{}
67 cmd.Stdout = buf
68 cmd.Stderr = buf
69 err := cmd.Run()
70 s := buf.String()
71 // Name the capture drop at the END of the output, where ctx.Truncate's
72 // tail keep guarantees the model sees it: the in-band seam marker sits at
73 // the ~1MB offset, always inside Truncate's dropped middle, and Truncate's
74 // own "total" would count the collapsed string, under-reporting the real
75 // size by orders of magnitude.
76 if d := buf.droppedBytes(); d > 0 {
77 s += fmt.Sprintf("\n(output capped at capture: %d bytes total, %d bytes dropped mid-stream)", buf.totalBytes(), d)
78 }
79 if err != nil {
80 switch {
81 case ctxT.Err() == context.DeadlineExceeded:
82 // Name the recovery in the result string rather than the system
83 // prompt: a foreground server is the common cause and it never
84 // exits on its own, so re-firing it with a bigger timeout just
85 // burns the turn again.
86 return s + fmt.Sprintf("\n(timeout after %s - if this command never exits on its own it is a server or watcher: re-run it backgrounded, `cmd >/tmp/x.log 2>&1 & echo $! >/tmp/x.pid`, then poll the log. If it was legitimately slow, re-issue it once with a larger timeout_seconds.)", timeout)
87 case parent.Err() == context.Canceled || ctxT.Err() == context.Canceled:
88 // User Ctrl+C; name it rather than leak "signal: killed" noise.
89 return s + "\n(cancelled)"
90 case errors.Is(err, exec.ErrWaitDelay):
91 // Shell exited 0; err is non-nil only because a backgrounded child
92 // held the pipes past WaitDelay, not a failure. Return output as-is
93 // so it isn't mislabeled with a spurious (exit: ...). After the
94 // cancel/timeout cases so those signals win over a coincident delay.
95 return s

Calls 4

StringMethod · 0.95
droppedBytesMethod · 0.95
totalBytesMethod · 0.95
setProcessGroupFunction · 0.70