Skip to content

Commit ec1cba5

Browse files
authored
Merge pull request #34 from mkXultra/fix/30
fix: 削除済み作業フォルダでのプロセス操作クラッシュを修正
2 parents 5b4fb5c + 02d765f commit ec1cba5

2 files changed

Lines changed: 127 additions & 5 deletions

File tree

src/__tests__/cli-process-service.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,111 @@ printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
334334
killSpy.mockRestore();
335335
});
336336

337+
it('lists processes without crashing when a tracked work folder has been deleted', async () => {
338+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
339+
tempDirs.push(root);
340+
const stateDir = join(root, 'state');
341+
const workFolder = join(root, 'deleted-project');
342+
mkdirSync(workFolder, { recursive: true });
343+
344+
const pid = 45678;
345+
const processDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)), String(pid));
346+
mkdirSync(processDir, { recursive: true });
347+
348+
writeFileSync(
349+
join(processDir, 'meta.json'),
350+
JSON.stringify({
351+
pid,
352+
prompt: 'deleted cwd',
353+
workFolder,
354+
toolType: 'claude',
355+
startTime: new Date().toISOString(),
356+
stdoutPath: join(processDir, 'stdout.log'),
357+
stderrPath: join(processDir, 'stderr.log'),
358+
status: 'running',
359+
})
360+
);
361+
362+
rmSync(workFolder, { recursive: true, force: true });
363+
364+
const service = new CliProcessService({
365+
stateDir,
366+
cliPaths: {
367+
claude: '/bin/sh',
368+
codex: '/bin/sh',
369+
gemini: '/bin/sh',
370+
forge: '/bin/sh',
371+
},
372+
});
373+
374+
const killSpy = vi.spyOn(globalThis.process, 'kill').mockImplementation((target: number, signal?: string | number) => {
375+
if (signal === 0 && target === pid) {
376+
throw Object.assign(new Error('not running'), { code: 'ESRCH' });
377+
}
378+
return true;
379+
});
380+
381+
const listed = await service.listProcesses();
382+
383+
expect(listed).toEqual([
384+
{
385+
pid,
386+
agent: 'claude',
387+
status: 'completed',
388+
},
389+
]);
390+
expect(JSON.parse(readFileSync(join(processDir, 'meta.json'), 'utf-8')).status).toBe('completed');
391+
killSpy.mockRestore();
392+
});
393+
394+
it('cleans up finished process directories even when their work folder has been deleted', async () => {
395+
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
396+
tempDirs.push(root);
397+
const stateDir = join(root, 'state');
398+
const workFolder = join(root, 'deleted-finished-project');
399+
mkdirSync(workFolder, { recursive: true });
400+
401+
const pid = 56789;
402+
const cwdDir = join(stateDir, 'cwds', encodeCwd(realpathSync(workFolder)));
403+
const processDir = join(cwdDir, String(pid));
404+
mkdirSync(processDir, { recursive: true });
405+
406+
writeFileSync(
407+
join(processDir, 'meta.json'),
408+
JSON.stringify({
409+
pid,
410+
prompt: 'done',
411+
workFolder,
412+
toolType: 'claude',
413+
startTime: new Date().toISOString(),
414+
stdoutPath: join(processDir, 'stdout.log'),
415+
stderrPath: join(processDir, 'stderr.log'),
416+
status: 'completed',
417+
})
418+
);
419+
420+
rmSync(workFolder, { recursive: true, force: true });
421+
422+
const service = new CliProcessService({
423+
stateDir,
424+
cliPaths: {
425+
claude: '/bin/sh',
426+
codex: '/bin/sh',
427+
gemini: '/bin/sh',
428+
forge: '/bin/sh',
429+
},
430+
});
431+
432+
const result = await service.cleanupProcesses();
433+
434+
expect(result).toEqual({
435+
removed: 1,
436+
message: 'Removed 1 processes',
437+
});
438+
expect(existsSync(processDir)).toBe(false);
439+
expect(existsSync(cwdDir)).toBe(false);
440+
});
441+
337442
it('cleans up completed and failed process directories but preserves running ones', async () => {
338443
const root = mkdtempSync(join(tmpdir(), 'ai-cli-cli-service-'));
339444
tempDirs.push(root);

src/cli-process-service.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
unlinkSync,
1313
writeFileSync,
1414
} from 'node:fs';
15-
import { join } from 'node:path';
15+
import { join, basename, dirname } from 'node:path';
1616
import { homedir } from 'node:os';
1717
import { buildCliCommand, type BuildCliCommandOptions } from './cli-builder.js';
1818
import { findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
@@ -24,6 +24,7 @@ interface StoredProcess {
2424
pid: number;
2525
prompt: string;
2626
workFolder: string;
27+
cwdKey?: string;
2728
model?: string;
2829
toolType: AgentType;
2930
startTime: string;
@@ -128,6 +129,7 @@ export class CliProcessService {
128129
pid,
129130
prompt: cmd.prompt,
130131
workFolder: cmd.cwd,
132+
cwdKey: this.resolveCwdKey(cmd.cwd),
131133
model: options.model,
132134
toolType: cmd.agent,
133135
startTime: new Date().toISOString(),
@@ -260,7 +262,7 @@ export class CliProcessService {
260262
continue;
261263
}
262264

263-
const processDir = this.resolveProcessDir(refreshed.workFolder, refreshed.pid);
265+
const processDir = this.resolveStoredProcessDir(refreshed);
264266
if (existsSync(processDir)) {
265267
rmSync(processDir, { recursive: true, force: true });
266268
removed++;
@@ -304,11 +306,15 @@ export class CliProcessService {
304306
}
305307

306308
private parseProcessFile(metaPath: string): StoredProcess {
307-
return JSON.parse(readFileSync(metaPath, 'utf-8')) as StoredProcess;
309+
const process = JSON.parse(readFileSync(metaPath, 'utf-8')) as StoredProcess;
310+
if (!process.cwdKey) {
311+
process.cwdKey = basename(dirname(dirname(metaPath)));
312+
}
313+
return process;
308314
}
309315

310316
private writeProcess(process: StoredProcess): void {
311-
const processDir = this.resolveProcessDir(process.workFolder, process.pid);
317+
const processDir = this.resolveStoredProcessDir(process);
312318
mkdirSync(processDir, { recursive: true });
313319
writeFileSync(this.resolveMetaPath(processDir), JSON.stringify(process, null, 2));
314320
}
@@ -333,7 +339,18 @@ export class CliProcessService {
333339
}
334340

335341
private resolveProcessDir(cwd: string, pid: number): string {
336-
return join(this.resolveCwdsDir(), normalizeCwdForStorage(realpathSync(cwd)), String(pid));
342+
return join(this.resolveCwdsDir(), this.resolveCwdKey(cwd), String(pid));
343+
}
344+
345+
private resolveStoredProcessDir(process: StoredProcess): string {
346+
if (!process.cwdKey) {
347+
process.cwdKey = this.resolveCwdKey(process.workFolder);
348+
}
349+
return join(this.resolveCwdsDir(), process.cwdKey, String(process.pid));
350+
}
351+
352+
private resolveCwdKey(cwd: string): string {
353+
return normalizeCwdForStorage(realpathSync(cwd));
337354
}
338355

339356
private resolveMetaPath(processDir: string): string {

0 commit comments

Comments
 (0)