@@ -1,6 +1,7 @@
1package shell
1package shell
@@ -67,16 +68,15 @@ func newPersistentShell(cwd string) *PersistentShell {
67 shellArgs = cfg.Shell.Args
68 shellArgs = cfg.Shell.Args
71− shellPath = os.Getenv("SHELL")
71+ shellPath = cmp.Or(shellPath, os.Getenv("SHELL"), "/bin/bash")
72− if shellPath == "" {
72+ if !strings.HasSuffix(shellPath, "bash") && !strings.HasSuffix(shellPath, "zsh") {
73+ logging.Warn("only bash and zsh are supported at this time", "shell", shellPath)
73− shellPath = "/bin/bash"
74+ shellPath = "/bin/bash"
77 // Default shell args
77 // Default shell args
78 if len(shellArgs) == 0 {
78 if len(shellArgs) == 0 {
79− shellArgs = []string{"-l"}
79+ shellArgs = []string{"--login"}
82 cmd := exec.Command(shellPath, shellArgs...)
82 cmd := exec.Command(shellPath, shellArgs...)
@@ -127,12 +127,15 @@ func newPersistentShell(cwd string) *PersistentShell {
128func (s *PersistentShell) processCommands() {
128func (s *PersistentShell) processCommands() {
129 for cmd := range s.commandQueue {
129 for cmd := range s.commandQueue {
130− result := s.execCommand(cmd.command, cmd.timeout, cmd.ctx)
130+ cmd.resultChan <- s.execCommand(cmd.ctx, cmd.command, cmd.timeout)
131− cmd.resultChan <- result
134+const runBashCommandFormat = `%s </dev/null >%q 2>%q
135−func (s *PersistentShell) execCommand(command string, timeout time.Duration, ctx context.Context) commandResult {
138+func (s *PersistentShell) execCommand(ctx context.Context, command string, timeout time.Duration) commandResult {
136 s.mu.Lock()
139 s.mu.Lock()
137 defer s.mu.Unlock()
140 defer s.mu.Unlock()
@@ -144,34 +147,22 @@ func (s *PersistentShell) execCommand(command string, timeout time.Duration, ctx
147− tempDir := os.TempDir()
150+ tmp := os.TempDir()
151+ now := time.Now().UnixNano()
148− stdoutFile := filepath.Join(tempDir, fmt.Sprintf("crush-stdout-%d", time.Now().UnixNano()))
152+ stdoutFile := filepath.Join(tmp, fmt.Sprintf("crush-stdout-%d", now))
149− stderrFile := filepath.Join(tempDir, fmt.Sprintf("crush-stderr-%d", time.Now().UnixNano()))
153+ stderrFile := filepath.Join(tmp, fmt.Sprintf("crush-stderr-%d", now))
150− statusFile := filepath.Join(tempDir, fmt.Sprintf("crush-status-%d", time.Now().UnixNano()))
154+ statusFile := filepath.Join(tmp, fmt.Sprintf("crush-status-%d", now))
151− cwdFile := filepath.Join(tempDir, fmt.Sprintf("crush-cwd-%d", time.Now().UnixNano()))
155+ cwdFile := filepath.Join(tmp, fmt.Sprintf("crush-cwd-%d", now))
153 defer func() {
157 defer func() {
154− os.Remove(stdoutFile)
158+ _ = os.Remove(stdoutFile)
155− os.Remove(stderrFile)
159+ _ = os.Remove(stderrFile)
156− os.Remove(statusFile)
160+ _ = os.Remove(statusFile)
157− os.Remove(cwdFile)
161+ _ = os.Remove(cwdFile)
160− fullCommand := fmt.Sprintf(`
164+ script := fmt.Sprintf(runBashCommandFormat, command, stdoutFile, stderrFile, statusFile, cwdFile)
161−eval %s < /dev/null > %s 2> %s
164−echo $EXEC_EXIT_CODE > %s
166− shellQuote(command),
167− shellQuote(stdoutFile),
168− shellQuote(stderrFile),
169− shellQuote(cwdFile),
170− shellQuote(statusFile),
173− _, err := s.stdin.Write([]byte(fullCommand + "\n"))
165+ if _, err := s.stdin.Write([]byte(script + "\n")); err != nil {
175 return commandResult{
166 return commandResult{
176 stderr: fmt.Sprintf("Failed to write command to shell: %v", err),
167 stderr: fmt.Sprintf("Failed to write command to shell: %v", err),
177 exitCode: 1,
168 exitCode: 1,
@@ -180,18 +171,18 @@ echo $EXEC_EXIT_CODE > %s
182 interrupted := false
173 interrupted := false
184− startTime := time.Now()
186 done := make(chan bool)
174 done := make(chan bool)
187 go func() {
175 go func() {
188 // Use exponential backoff polling
176 // Use exponential backoff polling
189− pollInterval := 1 * time.Millisecond
177+ pollInterval := 10 * time.Millisecond
190− maxPollInterval := 100 * time.Millisecond
178+ maxPollInterval := time.Second
192 ticker := time.NewTicker(pollInterval)
180 ticker := time.NewTicker(pollInterval)
193 defer ticker.Stop()
181 defer ticker.Stop()
183+ timeoutTicker := time.NewTicker(cmp.Or(timeout, time.Hour*99999))
184+ defer timeoutTicker.Stop()
197 case <-ctx.Done():
188 case <-ctx.Done():
@@ -200,22 +191,18 @@ echo $EXEC_EXIT_CODE > %s
200 done <- true
191 done <- true
194+ case <-timeoutTicker.C:
203 case <-ticker.C:
200 case <-ticker.C:
204− if fileExists(statusFile) && fileSize(statusFile) > 0 {
201+ if fileSize(statusFile) > 0 {
205 done <- true
202 done <- true
210− elapsed := time.Since(startTime)
211− if elapsed > timeout {
219 // Exponential backoff to reduce CPU usage for longer-running commands
206 // Exponential backoff to reduce CPU usage for longer-running commands
220 if pollInterval < maxPollInterval {
207 if pollInterval < maxPollInterval {
221 pollInterval = min(time.Duration(float64(pollInterval)*1.5), maxPollInterval)
208 pollInterval = min(time.Duration(float64(pollInterval)*1.5), maxPollInterval)
@@ -280,12 +267,10 @@ func (s *PersistentShell) Exec(ctx context.Context, command string, timeoutMs in
280 return "", "Shell is not alive", 1, false, errors.New("shell is not alive")
267 return "", "Shell is not alive", 1, false, errors.New("shell is not alive")
283− timeout := time.Duration(timeoutMs) * time.Millisecond
285 resultChan := make(chan commandResult)
270 resultChan := make(chan commandResult)
286 s.commandQueue <- &commandExecution{
271 s.commandQueue <- &commandExecution{
287 command: command,
272 command: command,
288− timeout: timeout,
273+ timeout: time.Duration(timeoutMs) * time.Millisecond,
289 resultChan: resultChan,
274 resultChan: resultChan,
290 ctx: ctx,
275 ctx: ctx,
@@ -310,10 +295,6 @@ func (s *PersistentShell) Close() {
310 s.isAlive = false
295 s.isAlive = false
313−func shellQuote(s string) string {
314− return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
317func readFileOrEmpty(path string) string {
298func readFileOrEmpty(path string) string {
318 content, err := os.ReadFile(path)
299 content, err := os.ReadFile(path)
319 if err != nil {
300 if err != nil {
@@ -322,11 +303,6 @@ func readFileOrEmpty(path string) string {
322 return string(content)
303 return string(content)
325−func fileExists(path string) bool {
326− _, err := os.Stat(path)
330func fileSize(path string) int64 {
306func fileSize(path string) int64 {
331 info, err := os.Stat(path)
307 info, err := os.Stat(path)
332 if err != nil {
308 if err != nil {