diff --git a/packages/prime-sandboxes/src/prime_sandboxes/sandbox.py b/packages/prime-sandboxes/src/prime_sandboxes/sandbox.py index 8d21542d..7fc7a774 100644 --- a/packages/prime-sandboxes/src/prime_sandboxes/sandbox.py +++ b/packages/prime-sandboxes/src/prime_sandboxes/sandbox.py @@ -435,14 +435,14 @@ def start_background_job( stderr_log_file_quoted = shlex.quote(stderr_log_file) # Wrap command in subshell so 'exit' terminates the subshell, not the outer shell. # This ensures 'echo $?' always runs to capture the exit code. - sh_command = f"({command_body}); echo $? > {exit_file_quoted}" + sh_command = ( + f"({command_body}) > {stdout_log_file_quoted} 2> {stderr_log_file_quoted}; " + f"echo $? > {exit_file_quoted}" + ) quoted_sh_command = shlex.quote(sh_command) - # Start detached process with separate stdout and stderr log files - bg_cmd = ( - f"nohup sh -c {quoted_sh_command} " - f"> {stdout_log_file_quoted} 2> {stderr_log_file_quoted} &" - ) + # Outer nohup redirects to /dev/null since output goes to log files inside sh -c + bg_cmd = f"nohup sh -c {quoted_sh_command} < /dev/null > /dev/null 2>&1 &" self.execute_command(sandbox_id, bg_cmd, timeout=10) return BackgroundJob( @@ -979,14 +979,14 @@ async def start_background_job( stderr_log_file_quoted = shlex.quote(stderr_log_file) # Wrap command in subshell so 'exit' terminates the subshell, not the outer shell. # This ensures 'echo $?' always runs to capture the exit code. - sh_command = f"({command_body}); echo $? > {exit_file_quoted}" + sh_command = ( + f"({command_body}) > {stdout_log_file_quoted} 2> {stderr_log_file_quoted}; " + f"echo $? > {exit_file_quoted}" + ) quoted_sh_command = shlex.quote(sh_command) - # Start detached process with separate stdout and stderr log files - bg_cmd = ( - f"nohup sh -c {quoted_sh_command} " - f"> {stdout_log_file_quoted} 2> {stderr_log_file_quoted} &" - ) + # Outer nohup redirects to /dev/null since output goes to log files inside sh -c + bg_cmd = f"nohup sh -c {quoted_sh_command} < /dev/null > /dev/null 2>&1 &" await self.execute_command(sandbox_id, bg_cmd, timeout=10) return BackgroundJob( diff --git a/packages/prime-sandboxes/tests/test_command_execution.py b/packages/prime-sandboxes/tests/test_command_execution.py index b57aa959..aeb3424c 100644 --- a/packages/prime-sandboxes/tests/test_command_execution.py +++ b/packages/prime-sandboxes/tests/test_command_execution.py @@ -226,6 +226,28 @@ def test_start_background_job(sandbox_client, shared_sandbox): print(f"✓ Background execution completed: {status.stdout.strip()}") +def test_start_background_job_returns_immediately(sandbox_client, shared_sandbox): + """Test that start_background_job returns immediately without waiting for the job.""" + print("\nTesting start_background_job returns immediately...") + + start_time = time.time() + job = sandbox_client.start_background_job( + shared_sandbox.id, + "sleep 30 && echo done", + ) + elapsed = time.time() - start_time + + assert job.job_id is not None + # Should return in under 5 seconds, not 30 + assert elapsed < 5, f"start_background_job took {elapsed:.1f}s, expected < 5s" + print(f"✓ Job started in {elapsed:.2f}s (sleep 30 is running in background)") + + # Verify job is still running (not completed yet) + status = sandbox_client.get_background_job(shared_sandbox.id, job) + assert not status.completed, "Job should still be running" + print("✓ Job correctly running in background") + + def test_start_background_job_with_working_dir(sandbox_client, shared_sandbox): """Test start_background_job with working directory""" sandbox_client.execute_command(shared_sandbox.id, "mkdir -p /tmp/bgtest")