16type PersistentShell struct {
22 commandQueue chan *commandExecution
25type commandExecution struct {
28 resultChan chan commandResult
32type commandResult struct {
41 shellInstance *PersistentShell
42 shellInstanceOnce sync.Once
45func GetPersistentShell(workingDir string) *PersistentShell {
46 shellInstanceOnce.Do(func() {
47 shellInstance = newPersistentShell(workingDir)
50 if shellInstance == nil {
51 shellInstance = newPersistentShell(workingDir)
52 } else if !shellInstance.isAlive {
53 shellInstance = newPersistentShell(shellInstance.cwd)
59func newPersistentShell(cwd string) *PersistentShell {
60 shellPath := os.Getenv("SHELL")
62 shellPath = "/bin/bash"
65 cmd := exec.Command(shellPath, "-l")
68 stdinPipe, err := cmd.StdinPipe()
73 cmd.Env = append(os.Environ(), "GIT_EDITOR=true")
80 shell := &PersistentShell{
82 stdin: stdinPipe.(*os.File),
85 commandQueue: make(chan *commandExecution, 10),
90 if r := recover(); r != nil {
91 fmt.Fprintf(os.Stderr, "Panic in shell command processor: %v\n", r)
93 close(shell.commandQueue)
96 shell.processCommands()
102 // Log the error if needed
104 shell.isAlive = false
105 close(shell.commandQueue)
111func (s *PersistentShell) processCommands() {
112 for cmd := range s.commandQueue {
113 result := s.execCommand(cmd.command, cmd.timeout, cmd.ctx)
114 cmd.resultChan <- result
118func (s *PersistentShell) execCommand(command string, timeout time.Duration, ctx context.Context) commandResult {
123 return commandResult{
124 stderr: "Shell is not alive",
126 err: errors.New("shell is not alive"),
130 tempDir := os.TempDir()
131 stdoutFile := filepath.Join(tempDir, fmt.Sprintf("opencode-stdout-%d", time.Now().UnixNano()))
132 stderrFile := filepath.Join(tempDir, fmt.Sprintf("opencode-stderr-%d", time.Now().UnixNano()))
133 statusFile := filepath.Join(tempDir, fmt.Sprintf("opencode-status-%d", time.Now().UnixNano()))
134 cwdFile := filepath.Join(tempDir, fmt.Sprintf("opencode-cwd-%d", time.Now().UnixNano()))
137 os.Remove(stdoutFile)
138 os.Remove(stderrFile)
139 os.Remove(statusFile)
143 fullCommand := fmt.Sprintf(`
144eval %s < /dev/null > %s 2> %s
147echo $EXEC_EXIT_CODE > %s
150 shellQuote(stdoutFile),
151 shellQuote(stderrFile),
153 shellQuote(statusFile),
156 _, err := s.stdin.Write([]byte(fullCommand + "\n"))
158 return commandResult{
159 stderr: fmt.Sprintf("Failed to write command to shell: %v", err),
167 startTime := time.Now()
169 done := make(chan bool)
179 case <-time.After(10 * time.Millisecond):
180 if fileExists(statusFile) && fileSize(statusFile) > 0 {
186 elapsed := time.Since(startTime)
187 if elapsed > timeout {
200 stdout := readFileOrEmpty(stdoutFile)
201 stderr := readFileOrEmpty(stderrFile)
202 exitCodeStr := readFileOrEmpty(statusFile)
203 newCwd := readFileOrEmpty(cwdFile)
206 if exitCodeStr != "" {
207 fmt.Sscanf(exitCodeStr, "%d", &exitCode)
208 } else if interrupted {
210 stderr += "\nCommand execution timed out or was interrupted"
214 s.cwd = strings.TrimSpace(newCwd)
217 return commandResult{
221 interrupted: interrupted,
225func (s *PersistentShell) killChildren() {
226 if s.cmd == nil || s.cmd.Process == nil {
230 pgrepCmd := exec.Command("pgrep", "-P", fmt.Sprintf("%d", s.cmd.Process.Pid))
231 output, err := pgrepCmd.Output()
236 for pidStr := range strings.SplitSeq(string(output), "\n") {
237 if pidStr = strings.TrimSpace(pidStr); pidStr != "" {
239 fmt.Sscanf(pidStr, "%d", &pid)
241 proc, err := os.FindProcess(pid)
243 proc.Signal(syscall.SIGTERM)
250func (s *PersistentShell) Exec(ctx context.Context, command string, timeoutMs int) (string, string, int, bool, error) {
252 return "", "Shell is not alive", 1, false, errors.New("shell is not alive")
255 timeout := time.Duration(timeoutMs) * time.Millisecond
257 resultChan := make(chan commandResult)
258 s.commandQueue <- &commandExecution{
261 resultChan: resultChan,
265 result := <-resultChan
266 return result.stdout, result.stderr, result.exitCode, result.interrupted, result.err
269func (s *PersistentShell) Close() {
277 s.stdin.Write([]byte("exit\n"))
283func shellQuote(s string) string {
284 return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
287func readFileOrEmpty(path string) string {
288 content, err := os.ReadFile(path)
292 return string(content)
295func fileExists(path string) bool {
296 _, err := os.Stat(path)
300func fileSize(path string) int64 {
301 info, err := os.Stat(path)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list