15 "github.com/opencode-ai/opencode/internal/config"
18type PersistentShell struct {
24 commandQueue chan *commandExecution
27type commandExecution struct {
30 resultChan chan commandResult
34type commandResult struct {
43 shellInstance *PersistentShell
44 shellInstanceOnce sync.Once
47func GetPersistentShell(workingDir string) *PersistentShell {
48 shellInstanceOnce.Do(func() {
49 shellInstance = newPersistentShell(workingDir)
52 if shellInstance == nil {
53 shellInstance = newPersistentShell(workingDir)
54 } else if !shellInstance.isAlive {
55 shellInstance = newPersistentShell(shellInstance.cwd)
61func newPersistentShell(cwd string) *PersistentShell {
62 // Get shell configuration from config
65 // Default to environment variable if config is not set or nil
67 var shellArgs []string
70 shellPath = cfg.Shell.Path
71 shellArgs = cfg.Shell.Args
75 shellPath = os.Getenv("SHELL")
77 shellPath = "/bin/bash"
82 if len(shellArgs) == 0 {
83 shellArgs = []string{"-l"}
86 cmd := exec.Command(shellPath, shellArgs...)
89 stdinPipe, err := cmd.StdinPipe()
94 cmd.Env = append(os.Environ(), "GIT_EDITOR=true")
101 shell := &PersistentShell{
103 stdin: stdinPipe.(*os.File),
106 commandQueue: make(chan *commandExecution, 10),
111 if r := recover(); r != nil {
112 fmt.Fprintf(os.Stderr, "Panic in shell command processor: %v\n", r)
113 shell.isAlive = false
114 close(shell.commandQueue)
117 shell.processCommands()
123 // Log the error if needed
125 shell.isAlive = false
126 close(shell.commandQueue)
132func (s *PersistentShell) processCommands() {
133 for cmd := range s.commandQueue {
134 result := s.execCommand(cmd.command, cmd.timeout, cmd.ctx)
135 cmd.resultChan <- result
139func (s *PersistentShell) execCommand(command string, timeout time.Duration, ctx context.Context) commandResult {
144 return commandResult{
145 stderr: "Shell is not alive",
147 err: errors.New("shell is not alive"),
151 tempDir := os.TempDir()
152 stdoutFile := filepath.Join(tempDir, fmt.Sprintf("opencode-stdout-%d", time.Now().UnixNano()))
153 stderrFile := filepath.Join(tempDir, fmt.Sprintf("opencode-stderr-%d", time.Now().UnixNano()))
154 statusFile := filepath.Join(tempDir, fmt.Sprintf("opencode-status-%d", time.Now().UnixNano()))
155 cwdFile := filepath.Join(tempDir, fmt.Sprintf("opencode-cwd-%d", time.Now().UnixNano()))
158 os.Remove(stdoutFile)
159 os.Remove(stderrFile)
160 os.Remove(statusFile)
164 fullCommand := fmt.Sprintf(`
165eval %s < /dev/null > %s 2> %s
168echo $EXEC_EXIT_CODE > %s
171 shellQuote(stdoutFile),
172 shellQuote(stderrFile),
174 shellQuote(statusFile),
177 _, err := s.stdin.Write([]byte(fullCommand + "\n"))
179 return commandResult{
180 stderr: fmt.Sprintf("Failed to write command to shell: %v", err),
188 startTime := time.Now()
190 done := make(chan bool)
200 case <-time.After(10 * time.Millisecond):
201 if fileExists(statusFile) && fileSize(statusFile) > 0 {
207 elapsed := time.Since(startTime)
208 if elapsed > timeout {
221 stdout := readFileOrEmpty(stdoutFile)
222 stderr := readFileOrEmpty(stderrFile)
223 exitCodeStr := readFileOrEmpty(statusFile)
224 newCwd := readFileOrEmpty(cwdFile)
227 if exitCodeStr != "" {
228 fmt.Sscanf(exitCodeStr, "%d", &exitCode)
229 } else if interrupted {
231 stderr += "\nCommand execution timed out or was interrupted"
235 s.cwd = strings.TrimSpace(newCwd)
238 return commandResult{
242 interrupted: interrupted,
246func (s *PersistentShell) killChildren() {
247 if s.cmd == nil || s.cmd.Process == nil {
251 pgrepCmd := exec.Command("pgrep", "-P", fmt.Sprintf("%d", s.cmd.Process.Pid))
252 output, err := pgrepCmd.Output()
257 for pidStr := range strings.SplitSeq(string(output), "\n") {
258 if pidStr = strings.TrimSpace(pidStr); pidStr != "" {
260 fmt.Sscanf(pidStr, "%d", &pid)
262 proc, err := os.FindProcess(pid)
264 proc.Signal(syscall.SIGTERM)
271func (s *PersistentShell) Exec(ctx context.Context, command string, timeoutMs int) (string, string, int, bool, error) {
273 return "", "Shell is not alive", 1, false, errors.New("shell is not alive")
276 timeout := time.Duration(timeoutMs) * time.Millisecond
278 resultChan := make(chan commandResult)
279 s.commandQueue <- &commandExecution{
282 resultChan: resultChan,
286 result := <-resultChan
287 return result.stdout, result.stderr, result.exitCode, result.interrupted, result.err
290func (s *PersistentShell) Close() {
298 s.stdin.Write([]byte("exit\n"))
304func shellQuote(s string) string {
305 return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
308func readFileOrEmpty(path string) string {
309 content, err := os.ReadFile(path)
313 return string(content)
316func fileExists(path string) bool {
317 _, err := os.Stat(path)
321func fileSize(path string) int64 {
322 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