Stop runtime services during workspace cleanup
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
normalizeAdapterManagedRuntimeServices,
|
normalizeAdapterManagedRuntimeServices,
|
||||||
realizeExecutionWorkspace,
|
realizeExecutionWorkspace,
|
||||||
releaseRuntimeServicesForRun,
|
releaseRuntimeServicesForRun,
|
||||||
|
stopRuntimeServicesForExecutionWorkspace,
|
||||||
type RealizedExecutionWorkspace,
|
type RealizedExecutionWorkspace,
|
||||||
} from "../services/workspace-runtime.ts";
|
} from "../services/workspace-runtime.ts";
|
||||||
|
|
||||||
@@ -457,6 +458,60 @@ describe("ensureRuntimeServicesForRun", () => {
|
|||||||
expect(captured.port).toMatch(/^\d+$/);
|
expect(captured.port).toMatch(/^\d+$/);
|
||||||
expect(services[0]?.executionWorkspaceId).toBe("execution-workspace-1");
|
expect(services[0]?.executionWorkspaceId).toBe("execution-workspace-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("stops execution workspace runtime services by executionWorkspaceId", async () => {
|
||||||
|
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-stop-"));
|
||||||
|
const workspace = buildWorkspace(workspaceRoot);
|
||||||
|
const runId = "run-stop";
|
||||||
|
leasedRunIds.add(runId);
|
||||||
|
|
||||||
|
const services = await ensureRuntimeServicesForRun({
|
||||||
|
runId,
|
||||||
|
agent: {
|
||||||
|
id: "agent-1",
|
||||||
|
name: "Codex Coder",
|
||||||
|
companyId: "company-1",
|
||||||
|
},
|
||||||
|
issue: null,
|
||||||
|
workspace,
|
||||||
|
executionWorkspaceId: "execution-workspace-stop",
|
||||||
|
config: {
|
||||||
|
workspaceRuntime: {
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
name: "web",
|
||||||
|
command:
|
||||||
|
"node -e \"require('node:http').createServer((req,res)=>res.end('ok')).listen(Number(process.env.PORT), '127.0.0.1')\"",
|
||||||
|
port: { type: "auto" },
|
||||||
|
readiness: {
|
||||||
|
type: "http",
|
||||||
|
urlTemplate: "http://127.0.0.1:{{port}}",
|
||||||
|
timeoutSec: 10,
|
||||||
|
intervalMs: 100,
|
||||||
|
},
|
||||||
|
lifecycle: "shared",
|
||||||
|
reuseScope: "execution_workspace",
|
||||||
|
stopPolicy: {
|
||||||
|
type: "manual",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
adapterEnv: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(services[0]?.url).toBeTruthy();
|
||||||
|
await stopRuntimeServicesForExecutionWorkspace({
|
||||||
|
executionWorkspaceId: "execution-workspace-stop",
|
||||||
|
workspaceCwd: workspace.cwd,
|
||||||
|
});
|
||||||
|
await releaseRuntimeServicesForRun(runId);
|
||||||
|
leasedRunIds.delete(runId);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
|
||||||
|
await expect(fetch(services[0]!.url!)).rejects.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("normalizeAdapterManagedRuntimeServices", () => {
|
describe("normalizeAdapterManagedRuntimeServices", () => {
|
||||||
|
|||||||
@@ -249,6 +249,21 @@ async function directoryExists(value: string) {
|
|||||||
return fs.stat(value).then((stats) => stats.isDirectory()).catch(() => false);
|
return fs.stat(value).then((stats) => stats.isDirectory()).catch(() => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function terminateChildProcess(child: ChildProcess) {
|
||||||
|
if (!child.pid) return;
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
try {
|
||||||
|
process.kill(-child.pid, "SIGTERM");
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// Fall through to the direct child kill.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!child.killed) {
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildWorkspaceCommandEnv(input: {
|
function buildWorkspaceCommandEnv(input: {
|
||||||
base: ExecutionWorkspaceInput;
|
base: ExecutionWorkspaceInput;
|
||||||
repoRoot: string;
|
repoRoot: string;
|
||||||
@@ -528,12 +543,12 @@ export async function cleanupExecutionWorkspaceArtifacts(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input.workspace.providerType === "git_worktree" && workspacePath) {
|
if (input.workspace.providerType === "git_worktree" && workspacePath) {
|
||||||
|
const repoRoot = await resolveGitRepoRootForWorkspaceCleanup(
|
||||||
|
workspacePath,
|
||||||
|
input.projectWorkspace?.cwd ?? null,
|
||||||
|
);
|
||||||
const worktreeExists = await directoryExists(workspacePath);
|
const worktreeExists = await directoryExists(workspacePath);
|
||||||
if (worktreeExists) {
|
if (worktreeExists) {
|
||||||
const repoRoot = await resolveGitRepoRootForWorkspaceCleanup(
|
|
||||||
workspacePath,
|
|
||||||
input.projectWorkspace?.cwd ?? null,
|
|
||||||
);
|
|
||||||
if (!repoRoot) {
|
if (!repoRoot) {
|
||||||
warnings.push(`Could not resolve git repo root for "${workspacePath}".`);
|
warnings.push(`Could not resolve git repo root for "${workspacePath}".`);
|
||||||
} else {
|
} else {
|
||||||
@@ -542,12 +557,16 @@ export async function cleanupExecutionWorkspaceArtifacts(input: {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
warnings.push(err instanceof Error ? err.message : String(err));
|
warnings.push(err instanceof Error ? err.message : String(err));
|
||||||
}
|
}
|
||||||
if (createdByRuntime && input.workspace.branchName) {
|
}
|
||||||
try {
|
}
|
||||||
await runGit(["branch", "-D", input.workspace.branchName], repoRoot);
|
if (createdByRuntime && input.workspace.branchName) {
|
||||||
} catch (err) {
|
if (!repoRoot) {
|
||||||
warnings.push(err instanceof Error ? err.message : String(err));
|
warnings.push(`Could not resolve git repo root to delete branch "${input.workspace.branchName}".`);
|
||||||
}
|
} else {
|
||||||
|
try {
|
||||||
|
await runGit(["branch", "-D", input.workspace.branchName], repoRoot);
|
||||||
|
} catch (err) {
|
||||||
|
warnings.push(err instanceof Error ? err.message : String(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -859,7 +878,7 @@ async function startLocalRuntimeService(input: {
|
|||||||
const child = spawn(shell, ["-lc", command], {
|
const child = spawn(shell, ["-lc", command], {
|
||||||
cwd: serviceCwd,
|
cwd: serviceCwd,
|
||||||
env,
|
env,
|
||||||
detached: false,
|
detached: process.platform !== "win32",
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
let stderrExcerpt = "";
|
let stderrExcerpt = "";
|
||||||
@@ -885,7 +904,7 @@ async function startLocalRuntimeService(input: {
|
|||||||
try {
|
try {
|
||||||
await waitForReadiness({ service: input.service, url });
|
await waitForReadiness({ service: input.service, url });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
child.kill("SIGTERM");
|
terminateChildProcess(child);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to start runtime service "${serviceName}": ${err instanceof Error ? err.message : String(err)}${stderrExcerpt ? ` | stderr: ${stderrExcerpt.trim()}` : ""}`,
|
`Failed to start runtime service "${serviceName}": ${err instanceof Error ? err.message : String(err)}${stderrExcerpt ? ` | stderr: ${stderrExcerpt.trim()}` : ""}`,
|
||||||
);
|
);
|
||||||
@@ -944,8 +963,8 @@ async function stopRuntimeService(serviceId: string) {
|
|||||||
record.status = "stopped";
|
record.status = "stopped";
|
||||||
record.lastUsedAt = new Date().toISOString();
|
record.lastUsedAt = new Date().toISOString();
|
||||||
record.stoppedAt = new Date().toISOString();
|
record.stoppedAt = new Date().toISOString();
|
||||||
if (record.child && !record.child.killed) {
|
if (record.child && record.child.pid) {
|
||||||
record.child.kill("SIGTERM");
|
terminateChildProcess(record.child);
|
||||||
}
|
}
|
||||||
runtimeServicesById.delete(serviceId);
|
runtimeServicesById.delete(serviceId);
|
||||||
if (record.reuseKey) {
|
if (record.reuseKey) {
|
||||||
|
|||||||
Reference in New Issue
Block a user