| 328 | } |
| 329 | |
| 330 | func (h *shellHandler) RunShellBackground(ctx context.Context, params RunShellBackgroundArgs) (*tools.ToolCallResult, error) { |
| 331 | if strings.TrimSpace(params.Cmd) == "" { |
| 332 | return tools.ResultError(`Error: missing or empty "cmd" parameter. Pass the shell command as {"cmd": "..."}.`), nil |
| 333 | } |
| 334 | |
| 335 | counter := h.jobCounter.Add(1) |
| 336 | jobID := fmt.Sprintf("job_%d_%d", time.Now().Unix(), counter) |
| 337 | |
| 338 | bgCmd, bgEnv := h.applyAskpass(ctx, params.Cmd) |
| 339 | cmd := exec.Command(h.shell, append(h.shellArgsPrefix, bgCmd)...) //nolint:noctx // RunShellBackground intentionally outlives the request context |
| 340 | cmd.Env = bgEnv |
| 341 | cmd.Dir = h.resolveWorkDir(params.Cwd) |
| 342 | cmd.SysProcAttr = platformSpecificSysProcAttr() |
| 343 | |
| 344 | job := &backgroundJob{ |
| 345 | id: jobID, |
| 346 | cmd: params.Cmd, |
| 347 | cwd: params.Cwd, |
| 348 | output: &bytes.Buffer{}, |
| 349 | startTime: time.Now(), |
| 350 | } |
| 351 | |
| 352 | // The limitedWriter shares the job's outputMu so that readers |
| 353 | // (ViewBackgroundJob, ListBackgroundJobs) and the pipe-copy |
| 354 | // goroutines spawned by exec.Cmd use the same lock. |
| 355 | lw := &limitedWriter{mu: &job.outputMu, buf: job.output, maxSize: 10 * 1024 * 1024} |
| 356 | cmd.Stdout = lw |
| 357 | cmd.Stderr = lw |
| 358 | |
| 359 | if err := cmd.Start(); err != nil { |
| 360 | return tools.ResultError(fmt.Sprintf("Error starting background command: %s", err)), nil |
| 361 | } |
| 362 | |
| 363 | pg, err := createProcessGroup(cmd.Process) |
| 364 | if err != nil { |
| 365 | // Successfully started the child but couldn't install it in its own |
| 366 | // process group: clean it up before bailing out. |
| 367 | reapSpawnedChild(cmd, pg) |
| 368 | return tools.ResultError(fmt.Sprintf("Error creating process group: %s", err)), nil |
| 369 | } |
| 370 | |
| 371 | job.process = cmd.Process |
| 372 | job.processGroup = pg |
| 373 | job.status.Store(statusRunning) |
| 374 | h.jobs.Store(jobID, job) |
| 375 | |
| 376 | go h.monitorJob(job, cmd) |
| 377 | |
| 378 | return tools.ResultSuccess(fmt.Sprintf("Background job started with ID: %s\nCommand: %s\nWorking directory: %s", |
| 379 | jobID, params.Cmd, params.Cwd)), nil |
| 380 | } |
| 381 | |
| 382 | func (h *shellHandler) monitorJob(job *backgroundJob, cmd *exec.Cmd) { |
| 383 | err := cmd.Wait() |