Skip to content

Commit c421aaa

Browse files
author
moc
committed
fix(checks): stop the detached service before tmpdir cleanup in cross-reuse MCP check
Round two of the same failure. Fork preview run 35506404071 (with 14a7fd5) failed the same check #9 with the same code: EBUSY: resource busy or locked, rmdir '...\wf-cross-mcp-3VJDll' The test ran 7945ms -- the full ~6.2s retry budget burned while the directory stayed locked. Not a transient lock, so retrying cannot fix it. Real root cause (src/main.mjs, stdio branch): when no existing service is found, --stdio spawns a detached daemon -- spawn(process.execPath, args, {cwd:workspace, detached:true, ...}); child.unref() -- with cwd set to the workspace (the mkdtemp tmp dir), service.log and the database inside dataDir (the same tmp dir). By design "the service, workers and dashboard outlive" the chat transport, so client.close()/transport.close() only tear down the stdio face and the daemon keeps running, holding its cwd handle plus service.log/DB handles inside the tree. win32 refuses to rmdir a directory tree a live process is sitting in; Linux/darwin unlink open files, which is why local runs stay green. Layer two, from run 35506090983: transport.close() resolving does not mean a child has fully exited, so handle release can lag an instant even after the daemon is gone. The codebase's own convention already stops the service first (checks/fail-loud.check.mjs:80, checks/lifecycle.check.mjs:19): run dist/main.mjs --stop-service --workspace <dir> --data-dir <dir> after closing the client. Identity matches this test because its --settings file sets workspace=dataDir=dir, the same endpoint.json the daemon registered under. - finally: after client/transport close (still deferred-rethrow), run --stop-service wrapped in try/catch so a stop failure cannot block rm - keep rmWithRetry as the release-lag fallback, still failing loud when exhausted - rewrite the comment to the two-layer root cause with both run ids Only checks/cross-reuse-mcp.check.mjs is touched; no src/, no dist/, no new dependencies. Local verification: npm test exit=0, 126/126; ps confirms the daemon spawned by the check is actually terminated by the stop-service step (no stray wf-cross-mcp-* processes remain).
1 parent 22def56 commit c421aaa

1 file changed

Lines changed: 4 additions & 3 deletions

File tree

‎plugins/hetaoBackend/mcode-dynamic-workflows/checks/cross-reuse-mcp.check.mjs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import test from 'node:test';import assert from 'node:assert/strict';import {Client} from '@modelcontextprotocol/sdk/client/index.js';import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';import {mkdtemp,rm,writeFile} from 'node:fs/promises';import {tmpdir} from 'node:os';import {join,resolve} from 'node:path';
2-
// win32 EBUSY race in tmpdir cleanup: StdioClientTransport.close() resolving does not mean the spawned server process has exited, so the child can still hold the dataDir handle when rm() runs and win32 refuses to rmdir a busy directory (fork preview run 35506090983, first full windows-latest pass; Linux/darwin unlink open files, so this only bites on Windows). Retry the transient lock with exponential backoff, then fail loud — a cleanup failure stays visible, we only give the OS time to release handles.
1+
import test from 'node:test';import assert from 'node:assert/strict';import {Client} from '@modelcontextprotocol/sdk/client/index.js';import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';import {mkdtemp,rm,writeFile} from 'node:fs/promises';import {tmpdir} from 'node:os';import {join,resolve} from 'node:path';import {execFile} from 'node:child_process';import {promisify} from 'node:util';
2+
// win32 EBUSY on tmpdir cleanup has two layers. (1) --stdio spawns a detached daemon (src/main.mjs: spawn with cwd:workspace, service.log and the DB in dataDir, child.unref(); "the service, workers and dashboard outlive" the chat transport by design), so closing client/transport leaves that daemon alive with its cwd and handles inside the mkdtemp dir — win32 refuses to rmdir a tree a live process sits in, and retrying alone cannot fix it: run 35506404071 burned the full ~6s retry budget and still failed EBUSY. Stop the service first, same as checks/fail-loud.check.mjs (--stop-service matches this test's --settings identity because workspace=dataDir=dir in both). (2) Even after the daemon is gone, StdioClientTransport.close() resolving does not mean the child has fully exited, so handle release can lag an instant: run 35506090983. Hence stop-service, then rmWithRetry as a release-lag fallback, then fail loud — cleanup failures stay visible. Linux/darwin unlink open files, so none of this bites locally.
3+
const exec=promisify(execFile);
34
const rmWithRetry=async(dir)=>{for(let i=0;;i++){try{return await rm(dir,{recursive:true,force:true})}catch(e){if(!['EBUSY','ENOTEMPTY','EPERM'].includes(e?.code)||i>=5)throw e;await new Promise(res=>setTimeout(res,200*2**i))}}};
45
test('packaged MCP advertises reuseAcrossRuns and accepts it through the public tool surface',async()=>{
56
const dir=await mkdtemp(join(tmpdir(),'wf-cross-mcp-'));await writeFile(join(dir,'settings.json'),JSON.stringify({workspace:dir,dataDir:dir}));
@@ -12,5 +13,5 @@ test('packaged MCP advertises reuseAcrossRuns and accepts it through the public
1213
assert.equal(run.reuseAcrossRuns,true,'the flag must survive the public tool surface');
1314
const rejected=await client.callTool({name:'workflow_start',arguments:{requestId:'cross-mcp-bad',name:'Bad flag type',executor:'demo',reuseAcrossRuns:'yes',script:'return 1;'}});
1415
assert.ok(rejected.isError,'a non-boolean flag must be rejected by the public surface');
15-
}finally{let closeErr;try{await client.close();await transport.close();}catch(e){closeErr=e}await rmWithRetry(dir);if(closeErr)throw closeErr;}
16+
}finally{let closeErr;try{await client.close();await transport.close();}catch(e){closeErr=e}try{await exec(process.execPath,[resolve('dist/main.mjs'),'--stop-service','--workspace',dir,'--data-dir',dir]);}catch{}await rmWithRetry(dir);if(closeErr)throw closeErr;}
1617
});

0 commit comments

Comments
 (0)