fix(ci): stabilize windows mcp preflight cleanup
This commit is contained in:
parent
cd4e9ccba8
commit
d766d174e3
2 changed files with 77 additions and 11 deletions
|
|
@ -205,6 +205,9 @@ const TEAM_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,127}$/;
|
||||||
const RUN_TIMEOUT_MS = 300_000;
|
const RUN_TIMEOUT_MS = 300_000;
|
||||||
const VERIFY_TIMEOUT_MS = 15_000;
|
const VERIFY_TIMEOUT_MS = 15_000;
|
||||||
const VERIFY_POLL_MS = 500;
|
const VERIFY_POLL_MS = 500;
|
||||||
|
const MCP_PREFLIGHT_SHUTDOWN_GRACE_MS = 250;
|
||||||
|
const MCP_PREFLIGHT_SHUTDOWN_TIMEOUT_MS = 2_000;
|
||||||
|
const MCP_PREFLIGHT_SHUTDOWN_POLL_MS = 50;
|
||||||
const STDERR_RING_LIMIT = 64 * 1024;
|
const STDERR_RING_LIMIT = 64 * 1024;
|
||||||
const STDOUT_RING_LIMIT = 64 * 1024;
|
const STDOUT_RING_LIMIT = 64 * 1024;
|
||||||
// Progress emissions fan out the latest CLI tail + assistant output to the
|
// Progress emissions fan out the latest CLI tail + assistant output to the
|
||||||
|
|
@ -1004,6 +1007,39 @@ async function waitForPidsToExit(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForChildProcessToExit(
|
||||||
|
child: ChildProcess | null | undefined,
|
||||||
|
timeoutMs: number
|
||||||
|
): Promise<void> {
|
||||||
|
if (!child?.pid || !isProcessAlive(child.pid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const finish = (): void => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
if (timeoutHandle) {
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
}
|
||||||
|
child.off('close', finish);
|
||||||
|
child.off('exit', finish);
|
||||||
|
child.off('error', finish);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
timeoutHandle = setTimeout(finish, timeoutMs);
|
||||||
|
child.once('close', finish);
|
||||||
|
child.once('exit', finish);
|
||||||
|
child.once('error', finish);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function tryReadRegularFileUtf8(
|
async function tryReadRegularFileUtf8(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
opts: { timeoutMs: number; maxBytes: number }
|
opts: { timeoutMs: number; maxBytes: number }
|
||||||
|
|
@ -13588,11 +13624,26 @@ export class TeamProvisioningService {
|
||||||
throw new Error(this.buildAgentTeamsMcpValidationError(errorText));
|
throw new Error(this.buildAgentTeamsMcpValidationError(errorText));
|
||||||
} finally {
|
} finally {
|
||||||
rejectAll(new Error('agent-teams MCP preflight session closed'));
|
rejectAll(new Error('agent-teams MCP preflight session closed'));
|
||||||
if (child?.stdin && !child.stdin.destroyed) {
|
if (child?.stdin && !child.stdin.destroyed && !child.stdin.writableEnded) {
|
||||||
child.stdin.end();
|
const stdin = child.stdin;
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
try {
|
||||||
|
stdin.end(() => resolve());
|
||||||
|
} catch {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (child) {
|
if (child?.pid) {
|
||||||
killProcessTree(child);
|
await waitForChildProcessToExit(child, MCP_PREFLIGHT_SHUTDOWN_GRACE_MS);
|
||||||
|
if (isProcessAlive(child.pid)) {
|
||||||
|
killProcessTree(child);
|
||||||
|
await waitForPidsToExit([child.pid], {
|
||||||
|
timeoutMs: MCP_PREFLIGHT_SHUTDOWN_TIMEOUT_MS,
|
||||||
|
pollMs: MCP_PREFLIGHT_SHUTDOWN_POLL_MS,
|
||||||
|
});
|
||||||
|
await waitForChildProcessToExit(child, MCP_PREFLIGHT_SHUTDOWN_GRACE_MS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await fs.promises.rm(fixture.claudeDir, { recursive: true, force: true }).catch(() => {});
|
await fs.promises.rm(fixture.claudeDir, { recursive: true, force: true }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,26 @@ process.stdin.on('data', (chunk) => {
|
||||||
return scriptPath;
|
return scriptPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeTempRoot(dirPath: string): Promise<void> {
|
||||||
|
if (!dirPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxAttempts = process.platform === 'win32' ? 20 : 1;
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||||
|
try {
|
||||||
|
await fs.promises.rm(dirPath, { recursive: true, force: true });
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
const code = (error as NodeJS.ErrnoException).code;
|
||||||
|
if ((code !== 'EBUSY' && code !== 'EPERM') || attempt === maxAttempts) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('TeamProvisioningService prepare/auth behavior', () => {
|
describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
let tempRoot = '';
|
let tempRoot = '';
|
||||||
|
|
||||||
|
|
@ -166,13 +186,8 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
|
||||||
delete process.env.ANTHROPIC_AUTH_TOKEN;
|
delete process.env.ANTHROPIC_AUTH_TOKEN;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(async () => {
|
||||||
fs.rmSync(tempRoot, {
|
await removeTempRoot(tempRoot);
|
||||||
recursive: true,
|
|
||||||
force: true,
|
|
||||||
maxRetries: 5,
|
|
||||||
retryDelay: 200,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not create missing directories during prepareForProvisioning', async () => {
|
it('does not create missing directories during prepareForProvisioning', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue