fix(daemon): preserve replacement socket ownership
This commit is contained in:
parent
782e028abe
commit
fe87142d89
@ -452,18 +452,24 @@ async function prepareSocket(socketPath: string): Promise<void> {
|
||||
}
|
||||
|
||||
async function cleanupArtifacts(options: DaemonHostOptions): Promise<void> {
|
||||
await cleanupDaemonArtifactsIfOwned(options, process.pid);
|
||||
}
|
||||
|
||||
export async function cleanupDaemonArtifactsIfOwned(
|
||||
paths: Pick<DaemonHostOptions, 'metadataPath' | 'socketPath'>,
|
||||
ownerPid: number
|
||||
): Promise<void> {
|
||||
// A superseded daemon may finish shutting down after its replacement has
|
||||
// already rebound the same paths. Never let that old process unlink the
|
||||
// replacement daemon's live socket and metadata.
|
||||
const metadata = await readJsonFile<{ pid?: number; socketPath?: string }>(paths.metadataPath).catch(() => undefined);
|
||||
if (metadata?.pid !== ownerPid || metadata.socketPath !== paths.socketPath) {
|
||||
return;
|
||||
}
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
await fs.unlink(options.socketPath);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
try {
|
||||
await fs.unlink(options.metadataPath);
|
||||
} catch {
|
||||
// ignore
|
||||
await fs.unlink(paths.socketPath).catch(() => {});
|
||||
}
|
||||
await fs.unlink(paths.metadataPath).catch(() => {});
|
||||
}
|
||||
|
||||
async function handleSocketRequest(
|
||||
|
||||
@ -5,7 +5,12 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { ServerDefinition } from '../src/config.js';
|
||||
import { __testProcessRequest, isDaemonResponding, metadataMatches } from '../src/daemon/host.js';
|
||||
import {
|
||||
__testProcessRequest,
|
||||
cleanupDaemonArtifactsIfOwned,
|
||||
isDaemonResponding,
|
||||
metadataMatches,
|
||||
} from '../src/daemon/host.js';
|
||||
import type { DaemonRequest } from '../src/daemon/protocol.js';
|
||||
import type { Runtime } from '../src/runtime.js';
|
||||
|
||||
@ -246,6 +251,41 @@ describe('metadataMatches', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('daemon artifact cleanup', () => {
|
||||
let dir: string;
|
||||
let metadataPath: string;
|
||||
let socketPath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-cleanup-'));
|
||||
metadataPath = path.join(dir, 'daemon.json');
|
||||
socketPath = path.join(dir, 'daemon.sock');
|
||||
await fs.writeFile(socketPath, 'socket', 'utf8');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('removes artifacts still owned by the stopping daemon', async () => {
|
||||
await fs.writeFile(metadataPath, JSON.stringify({ pid: 4321, socketPath }), 'utf8');
|
||||
|
||||
await cleanupDaemonArtifactsIfOwned({ metadataPath, socketPath }, 4321);
|
||||
|
||||
await expect(fs.access(metadataPath)).rejects.toThrow();
|
||||
await expect(fs.access(socketPath)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('preserves artifacts replaced by a newer daemon', async () => {
|
||||
await fs.writeFile(metadataPath, JSON.stringify({ pid: 9876, socketPath }), 'utf8');
|
||||
|
||||
await cleanupDaemonArtifactsIfOwned({ metadataPath, socketPath }, 4321);
|
||||
|
||||
await expect(fs.access(metadataPath)).resolves.toBeUndefined();
|
||||
await expect(fs.access(socketPath)).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
function createRuntimeDouble(): Pick<Runtime, 'callTool' | 'listTools'> {
|
||||
return {
|
||||
callTool: vi.fn().mockResolvedValue({ ok: true }),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user