Skip to content

Commit ee6d558

Browse files
committed
fix: restart stale daemon after source changes
1 parent 48fa16c commit ee6d558

4 files changed

Lines changed: 92 additions & 24 deletions

File tree

src/daemon-client.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { runCmdDetached, runCmdSync } from './utils/exec.ts';
1313
import { findProjectRoot, readVersion } from './utils/version.ts';
1414
import { createRequestId, emitDiagnostic, withDiagnosticTimer } from './utils/diagnostics.ts';
1515
import { isAgentDeviceDaemonProcess, stopProcessForTakeover } from './utils/process-identity.ts';
16+
import { computeDaemonCodeSignature as computeSharedDaemonCodeSignature } from './daemon/code-signature.ts';
1617
import {
1718
resolveDaemonPaths,
1819
resolveDaemonServerMode,
@@ -738,13 +739,7 @@ export function computeDaemonCodeSignature(
738739
entryPath: string,
739740
root: string = findProjectRoot(),
740741
): string {
741-
try {
742-
const stat = fs.statSync(entryPath);
743-
const relativePath = path.relative(root, entryPath) || entryPath;
744-
return `${relativePath}:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
745-
} catch {
746-
return 'unknown';
747-
}
742+
return computeSharedDaemonCodeSignature(entryPath, root);
748743
}
749744

750745
async function sendRequest(

src/daemon/code-signature.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import crypto from 'node:crypto';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { findProjectRoot } from '../utils/version.ts';
5+
6+
export function resolveDaemonCodeSignature(
7+
entryPath: string | undefined = process.argv[1],
8+
root: string = findProjectRoot(),
9+
): string {
10+
if (!entryPath) return 'unknown';
11+
return computeDaemonCodeSignature(entryPath, root);
12+
}
13+
14+
export function computeDaemonCodeSignature(
15+
entryPath: string,
16+
root: string = findProjectRoot(),
17+
): string {
18+
const targetPath = resolveDaemonCodeSignatureTarget(entryPath, root);
19+
try {
20+
const stat = fs.statSync(targetPath);
21+
if (!stat.isDirectory()) {
22+
return formatSingleFileSignature(targetPath, stat, root);
23+
}
24+
25+
const hash = crypto.createHash('sha256');
26+
let fileCount = 0;
27+
for (const filePath of walkSignatureFiles(targetPath)) {
28+
const fileStat = fs.statSync(filePath);
29+
const relativePath = path.relative(root, filePath) || path.basename(filePath);
30+
hash.update(
31+
`${relativePath}:${fileStat.size}:${Math.trunc(fileStat.mtimeMs)}:${fileStat.mode}\n`,
32+
);
33+
fileCount += 1;
34+
}
35+
36+
const relativeTarget = path.relative(root, targetPath) || path.basename(targetPath);
37+
return `${relativeTarget}:${fileCount}:${hash.digest('hex').slice(0, 16)}`;
38+
} catch {
39+
return 'unknown';
40+
}
41+
}
42+
43+
function resolveDaemonCodeSignatureTarget(entryPath: string, root: string): string {
44+
const resolvedEntryPath = path.resolve(entryPath);
45+
const sourceDaemonEntry = path.join(root, 'src', 'daemon.ts');
46+
const distDaemonEntry = path.join(root, 'dist', 'src', 'daemon.js');
47+
if (resolvedEntryPath === sourceDaemonEntry) {
48+
return path.join(root, 'src');
49+
}
50+
if (resolvedEntryPath === distDaemonEntry) {
51+
return path.join(root, 'dist', 'src');
52+
}
53+
return resolvedEntryPath;
54+
}
55+
56+
function formatSingleFileSignature(filePath: string, stat: fs.Stats, root: string): string {
57+
const relativePath = path.relative(root, filePath) || path.basename(filePath);
58+
return `${relativePath}:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
59+
}
60+
61+
function* walkSignatureFiles(dirPath: string): Generator<string> {
62+
const entries = fs
63+
.readdirSync(dirPath, { withFileTypes: true })
64+
.sort((left, right) => left.name.localeCompare(right.name));
65+
for (const entry of entries) {
66+
const entryPath = path.join(dirPath, entry.name);
67+
if (entry.isDirectory()) {
68+
yield* walkSignatureFiles(entryPath);
69+
continue;
70+
}
71+
if (entry.isFile()) {
72+
yield entryPath;
73+
}
74+
}
75+
}

src/daemon/server-lifecycle.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'node:fs';
2-
import path from 'node:path';
3-
import { findProjectRoot, readVersion } from '../utils/version.ts';
2+
import { readVersion } from '../utils/version.ts';
43
import { isAgentDeviceDaemonProcess, readProcessStartTime } from '../utils/process-identity.ts';
4+
import { resolveDaemonCodeSignature as resolveSharedDaemonCodeSignature } from './code-signature.ts';
55

66
export type DaemonLockInfo = {
77
pid: number;
@@ -11,16 +11,7 @@ export type DaemonLockInfo = {
1111
};
1212

1313
export function resolveDaemonCodeSignature(): string {
14-
const entryPath = process.argv[1];
15-
if (!entryPath) return 'unknown';
16-
try {
17-
const stat = fs.statSync(entryPath);
18-
const root = findProjectRoot();
19-
const relativePath = path.relative(root, entryPath) || entryPath;
20-
return `${relativePath}:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
21-
} catch {
22-
return 'unknown';
23-
}
14+
return resolveSharedDaemonCodeSignature(process.argv[1]);
2415
}
2516

2617
export function writeInfo(

src/utils/__tests__/daemon-client.test.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,14 +1032,21 @@ test('downloadRemoteArtifact times out stalled artifact responses and removes pa
10321032
}
10331033
});
10341034

1035-
test('computeDaemonCodeSignature includes relative path, size, and mtime', () => {
1035+
test('computeDaemonCodeSignature fingerprints the daemon source tree', () => {
10361036
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-daemon-signature-'));
10371037
try {
1038-
const daemonEntryPath = path.join(root, 'dist', 'src', 'daemon.js');
1039-
fs.mkdirSync(path.dirname(daemonEntryPath), { recursive: true });
1038+
const daemonEntryPath = path.join(root, 'src', 'daemon.ts');
1039+
const perfPath = path.join(root, 'src', 'platforms', 'ios', 'perf.ts');
1040+
fs.mkdirSync(path.dirname(perfPath), { recursive: true });
10401041
fs.writeFileSync(daemonEntryPath, 'console.log("daemon");\n', 'utf8');
1041-
const signature = computeDaemonCodeSignature(daemonEntryPath, root);
1042-
assert.match(signature, /^dist\/src\/daemon\.js:\d+:\d+$/);
1042+
fs.writeFileSync(perfPath, 'export const value = 1;\n', 'utf8');
1043+
1044+
const firstSignature = computeDaemonCodeSignature(daemonEntryPath, root);
1045+
assert.match(firstSignature, /^src:\d+:[a-f0-9]{16}$/);
1046+
1047+
fs.writeFileSync(perfPath, 'export const value = 2;\n', 'utf8');
1048+
const secondSignature = computeDaemonCodeSignature(daemonEntryPath, root);
1049+
assert.notEqual(secondSignature, firstSignature);
10431050
} finally {
10441051
fs.rmSync(root, { recursive: true, force: true });
10451052
}

0 commit comments

Comments
 (0)