mirror of
https://github.com/Routstr/routstrd.git
synced 2026-10-05 20:38:22 +00:00
fix(wallet): stop legacy cocod before direct daemon startup and ignore migration lock PID
routstrd start already stopped the real legacy cocod daemon, but wallet migration claimed ~/.cocod/cocod.pid using routstrd's own PID. The post-claim recheck in assertLegacyCocodNotRunning then saw that live PID and falsely reported it as a running legacy cocod, so every start attempt failed with a different 'Legacy cocod daemon is still running with PID <routstrd>' error. - assertLegacyCocodNotRunning now accepts ignorePid so the migration's own lock owner is not mistaken for cocod. - The direct daemon entry point (src/daemon/index.ts) calls stopLegacyCocod before migration, so startup is safe even when launched directly or by an older/global CLI, instead of relying solely on the parent command. - initDaemon() stops legacy cocod before migrating as well. - Added a regression test for the self-owned migration lock PID.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "routstrd",
|
||||
|
||||
+12
-1
@@ -240,18 +240,29 @@ async function initDaemon(): Promise<void> {
|
||||
}
|
||||
|
||||
console.log(`Database will be stored at: ${DB_PATH}`);
|
||||
await stopLegacyCocod();
|
||||
|
||||
let migrationLockOwner: number | undefined;
|
||||
const migration = await migrateLegacyWallet({
|
||||
assertLegacyStopped: () =>
|
||||
assertLegacyCocodNotRunning({
|
||||
socketPath: legacyCocodSocketPath(),
|
||||
pidFilePath: legacyCocodPidPath(),
|
||||
ignorePid: migrationLockOwner,
|
||||
}),
|
||||
acquireLegacyLock: () => {
|
||||
mkdirSync(dirname(legacyCocodPidPath()), {
|
||||
recursive: true,
|
||||
mode: 0o700,
|
||||
});
|
||||
return claimLegacyCocodPidFile({ pidFilePath: legacyCocodPidPath() });
|
||||
const release = claimLegacyCocodPidFile({
|
||||
pidFilePath: legacyCocodPidPath(),
|
||||
});
|
||||
migrationLockOwner = process.pid;
|
||||
return () => {
|
||||
migrationLockOwner = undefined;
|
||||
release();
|
||||
};
|
||||
},
|
||||
});
|
||||
if (migration.status === "migrated") {
|
||||
|
||||
+18
-1
@@ -60,6 +60,7 @@ import {
|
||||
assertLegacyCocodNotRunning,
|
||||
claimLegacyCocodPidFile,
|
||||
createCocoClient,
|
||||
stopLegacyCocod,
|
||||
} from "./wallet/coco-client";
|
||||
import { migrateLegacyWallet } from "./wallet/migration";
|
||||
import {
|
||||
@@ -126,18 +127,34 @@ async function main(): Promise<void> {
|
||||
const { ensureProvidersBootstrapped, getRoutstr21Models, getModelProviders, refreshProvidersAndModels } =
|
||||
createModelService(modelManager, providerManager, store);
|
||||
|
||||
// The daemon may be launched directly (or by an older/global CLI), so do
|
||||
// not rely on the parent command having stopped the external wallet first.
|
||||
await stopLegacyCocod({
|
||||
socketPath: legacyCocodSocketPath(),
|
||||
pidFilePath: legacyCocodPidPath(),
|
||||
});
|
||||
|
||||
let migrationLockOwner: number | undefined;
|
||||
const migration = await migrateLegacyWallet({
|
||||
assertLegacyStopped: () =>
|
||||
assertLegacyCocodNotRunning({
|
||||
socketPath: legacyCocodSocketPath(),
|
||||
pidFilePath: legacyCocodPidPath(),
|
||||
ignorePid: migrationLockOwner,
|
||||
}),
|
||||
acquireLegacyLock: () => {
|
||||
mkdirSync(dirname(legacyCocodPidPath()), {
|
||||
recursive: true,
|
||||
mode: 0o700,
|
||||
});
|
||||
return claimLegacyCocodPidFile({ pidFilePath: legacyCocodPidPath() });
|
||||
const release = claimLegacyCocodPidFile({
|
||||
pidFilePath: legacyCocodPidPath(),
|
||||
});
|
||||
migrationLockOwner = process.pid;
|
||||
return () => {
|
||||
migrationLockOwner = undefined;
|
||||
release();
|
||||
};
|
||||
},
|
||||
});
|
||||
if (migration.status === "migrated") {
|
||||
|
||||
@@ -315,6 +315,19 @@ describe("assertLegacyCocodNotRunning", () => {
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("ignores the caller's own migration lock PID", async () => {
|
||||
await expect(
|
||||
assertLegacyCocodNotRunning({
|
||||
socketPath: SOCKET_PATH,
|
||||
pidFilePath: PID_FILE_PATH,
|
||||
pathExists: (path) => path === PID_FILE_PATH,
|
||||
readFile: () => "4242\n",
|
||||
isProcessRunning: () => true,
|
||||
ignorePid: 4242,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("fails closed when the socket cannot be probed safely", async () => {
|
||||
const fetchImpl = mock<LegacyFetch>(async () => {
|
||||
throw Object.assign(new Error("permission denied"), { code: "EACCES" });
|
||||
|
||||
@@ -60,6 +60,8 @@ export interface LegacyCocodGuardOptions {
|
||||
pathExists?: (path: string) => boolean;
|
||||
readFile?: (path: string) => string;
|
||||
isProcessRunning?: (pid: number) => boolean;
|
||||
/** PID owned by the caller's already-acquired legacy exclusion lock. */
|
||||
ignorePid?: number;
|
||||
fetchImpl?: LegacyCocodFetch;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
@@ -243,7 +245,10 @@ export async function assertLegacyCocodNotRunning(
|
||||
|
||||
try {
|
||||
const pid = Number.parseInt(readFile(pidFilePath).trim(), 10);
|
||||
return Number.isInteger(pid) && pid > 0 && isProcessRunning(pid)
|
||||
return Number.isInteger(pid) &&
|
||||
pid > 0 &&
|
||||
pid !== options.ignorePid &&
|
||||
isProcessRunning(pid)
|
||||
? pid
|
||||
: null;
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user