@cogcoin/client 1.1.14 → 1.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bitcoind/indexer-daemon/lifecycle.d.ts +6 -0
- package/dist/bitcoind/indexer-daemon/lifecycle.js +43 -5
- package/dist/bitcoind/indexer-daemon/native-dependencies.d.ts +7 -0
- package/dist/bitcoind/indexer-daemon/native-dependencies.js +27 -0
- package/dist/bitcoind/indexer-daemon/runtime.js +20 -0
- package/dist/bitcoind/indexer-daemon/startup.d.ts +44 -0
- package/dist/bitcoind/indexer-daemon/startup.js +152 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.js +10 -0
- package/dist/bitcoind/service-paths.d.ts +1 -0
- package/dist/bitcoind/service-paths.js +1 -0
- package/dist/cli/output/rules/services.js +39 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cogcoin/client`
|
|
2
2
|
|
|
3
|
-
`@cogcoin/client@1.1.
|
|
3
|
+
`@cogcoin/client@1.1.15` is the reference Cogcoin client package for applications that want a local wallet, durable SQLite-backed state, and a managed Bitcoin Core integration around `@cogcoin/indexer`. It publishes the reusable client APIs, the SQLite adapter, the managed `bitcoind` integration, and the first-party `cogcoin` CLI in one package.
|
|
4
4
|
|
|
5
5
|
Use Node 22 or newer.
|
|
6
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ManagedIndexerDaemonProbeResult } from "../managed-runtime/types.js";
|
|
2
2
|
import type { ManagedIndexerDaemonObservedStatus } from "../types.js";
|
|
3
|
+
import { resolveManagedServicePaths } from "../service-paths.js";
|
|
3
4
|
import type { CoherentIndexerSnapshotLease, IndexerDaemonClient, IndexerDaemonStopResult, ManagedIndexerDaemonServiceLifetime } from "./types.js";
|
|
4
5
|
export type { IndexerDaemonCompatibility } from "../managed-runtime/types.js";
|
|
5
6
|
export declare const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
|
|
@@ -8,6 +9,11 @@ export declare function probeIndexerDaemon(options: {
|
|
|
8
9
|
dataDir: string;
|
|
9
10
|
walletRootId?: string;
|
|
10
11
|
}): Promise<IndexerDaemonProbeResult>;
|
|
12
|
+
export declare function probeIndexerDaemonForStart(options: {
|
|
13
|
+
dataDir: string;
|
|
14
|
+
walletRootId: string;
|
|
15
|
+
paths?: ReturnType<typeof resolveManagedServicePaths>;
|
|
16
|
+
}): Promise<IndexerDaemonProbeResult>;
|
|
11
17
|
export declare function readSnapshotWithRetry(daemon: IndexerDaemonClient, expectedWalletRootId: string): Promise<CoherentIndexerSnapshotLease>;
|
|
12
18
|
export declare function readObservedIndexerDaemonStatus(options: {
|
|
13
19
|
dataDir: string;
|
|
@@ -7,7 +7,8 @@ import { attachOrStartManagedIndexerRuntime } from "../managed-runtime/indexer-r
|
|
|
7
7
|
import { readJsonFileIfPresent } from "../managed-runtime/status.js";
|
|
8
8
|
import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "../service-paths.js";
|
|
9
9
|
import { createIndexerDaemonClient, probeIndexerDaemonAtSocket, } from "./client.js";
|
|
10
|
-
import { DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS, DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS, sleep, stopIndexerDaemonService as stopIndexerDaemonServiceInternal, stopIndexerDaemonServiceWithLockHeld, waitForIndexerDaemon, } from "./process.js";
|
|
10
|
+
import { DEFAULT_INDEXER_DAEMON_SHUTDOWN_TIMEOUT_MS, DEFAULT_INDEXER_DAEMON_STARTUP_TIMEOUT_MS, clearIndexerDaemonRuntimeArtifacts, isIndexerDaemonProcessAlive, sleep, stopIndexerDaemonService as stopIndexerDaemonServiceInternal, stopIndexerDaemonServiceWithLockHeld, waitForIndexerDaemon, } from "./process.js";
|
|
11
|
+
import { openIndexerDaemonStartupLog, recordIndexerDaemonStartupFailure, waitForIndexerDaemonStartup, } from "./startup.js";
|
|
11
12
|
export const INDEXER_DAEMON_BACKGROUND_FOLLOW_RECOVERY_FAILED = "indexer_daemon_background_follow_recovery_failed";
|
|
12
13
|
const INDEXER_DAEMON_BACKGROUND_FOLLOW_NOT_ACTIVE = "indexer_daemon_background_follow_not_active";
|
|
13
14
|
export async function probeIndexerDaemon(options) {
|
|
@@ -15,6 +16,25 @@ export async function probeIndexerDaemon(options) {
|
|
|
15
16
|
const paths = resolveManagedServicePaths(options.dataDir, walletRootId);
|
|
16
17
|
return probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, walletRootId);
|
|
17
18
|
}
|
|
19
|
+
export async function probeIndexerDaemonForStart(options) {
|
|
20
|
+
const paths = options.paths ?? resolveManagedServicePaths(options.dataDir, options.walletRootId);
|
|
21
|
+
const probe = await probeIndexerDaemonAtSocket(paths.indexerDaemonSocketPath, options.walletRootId);
|
|
22
|
+
if (probe.compatibility === "compatible") {
|
|
23
|
+
return probe;
|
|
24
|
+
}
|
|
25
|
+
const status = await readJsonFileIfPresent(paths.indexerDaemonStatusPath).catch(() => null);
|
|
26
|
+
const processId = status?.processId ?? null;
|
|
27
|
+
if (processId !== null && !await isIndexerDaemonProcessAlive(processId)) {
|
|
28
|
+
await clearIndexerDaemonRuntimeArtifacts(paths);
|
|
29
|
+
return {
|
|
30
|
+
compatibility: "unreachable",
|
|
31
|
+
status: null,
|
|
32
|
+
client: null,
|
|
33
|
+
error: null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return probe;
|
|
37
|
+
}
|
|
18
38
|
export async function readSnapshotWithRetry(daemon, expectedWalletRootId) {
|
|
19
39
|
let lastError = null;
|
|
20
40
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
@@ -69,14 +89,16 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
69
89
|
const expectedBinaryVersion = options.expectedBinaryVersion ?? null;
|
|
70
90
|
const startDaemon = async () => {
|
|
71
91
|
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
92
|
+
const startupLog = await openIndexerDaemonStartupLog(paths);
|
|
72
93
|
const daemonEntryPath = fileURLToPath(new URL("../indexer-daemon-main.js", import.meta.url));
|
|
94
|
+
const stdio = ["ignore", startupLog.fd, startupLog.fd];
|
|
73
95
|
const spawnOptions = serviceLifetime === "ephemeral"
|
|
74
96
|
? {
|
|
75
|
-
stdio
|
|
97
|
+
stdio,
|
|
76
98
|
}
|
|
77
99
|
: {
|
|
78
100
|
detached: true,
|
|
79
|
-
stdio
|
|
101
|
+
stdio,
|
|
80
102
|
};
|
|
81
103
|
const child = spawn(process.execPath, [
|
|
82
104
|
daemonEntryPath,
|
|
@@ -86,11 +108,16 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
86
108
|
], {
|
|
87
109
|
...spawnOptions,
|
|
88
110
|
});
|
|
111
|
+
await startupLog.close().catch(() => undefined);
|
|
89
112
|
if (serviceLifetime !== "ephemeral") {
|
|
90
113
|
child.unref();
|
|
91
114
|
}
|
|
92
115
|
try {
|
|
93
|
-
await
|
|
116
|
+
await waitForIndexerDaemonStartup({
|
|
117
|
+
child,
|
|
118
|
+
logPath: startupLog.logPath,
|
|
119
|
+
waitForReady: () => waitForIndexerDaemon(options.dataDir, walletRootId, startupTimeoutMs),
|
|
120
|
+
});
|
|
94
121
|
}
|
|
95
122
|
catch (error) {
|
|
96
123
|
if (child.pid !== undefined) {
|
|
@@ -101,6 +128,13 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
101
128
|
// ignore shutdown failures while unwinding startup errors
|
|
102
129
|
}
|
|
103
130
|
}
|
|
131
|
+
await recordIndexerDaemonStartupFailure({
|
|
132
|
+
paths,
|
|
133
|
+
walletRootId,
|
|
134
|
+
binaryVersion: expectedBinaryVersion ?? "0.0.0",
|
|
135
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
136
|
+
processId: child.pid ?? null,
|
|
137
|
+
}).catch(() => undefined);
|
|
104
138
|
throw error;
|
|
105
139
|
}
|
|
106
140
|
return createIndexerDaemonClient(paths.indexerDaemonSocketPath, {
|
|
@@ -122,7 +156,11 @@ export async function attachOrStartIndexerDaemon(options) {
|
|
|
122
156
|
expectedBinaryVersion,
|
|
123
157
|
}, {
|
|
124
158
|
getPaths: (runtimeOptions) => resolveManagedServicePaths(runtimeOptions.dataDir, runtimeOptions.walletRootId),
|
|
125
|
-
probeDaemon: async (runtimeOptions, runtimePaths) =>
|
|
159
|
+
probeDaemon: async (runtimeOptions, runtimePaths) => probeIndexerDaemonForStart({
|
|
160
|
+
dataDir: runtimeOptions.dataDir,
|
|
161
|
+
walletRootId: runtimeOptions.walletRootId,
|
|
162
|
+
paths: runtimePaths,
|
|
163
|
+
}),
|
|
126
164
|
requestBackgroundFollow,
|
|
127
165
|
closeClient: async (client) => {
|
|
128
166
|
await client.close();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { loadBetterSqlite3 } from "../../sqlite/better-sqlite3.js";
|
|
2
|
+
export declare const SQLITE_NATIVE_MODULE_UNAVAILABLE = "sqlite_native_module_unavailable";
|
|
3
|
+
type LoadBetterSqlite3 = typeof loadBetterSqlite3;
|
|
4
|
+
export declare function isSqliteNativeModuleLoadFailure(error: unknown): boolean;
|
|
5
|
+
export declare function createSqliteNativeModuleUnavailableError(error: unknown): Error;
|
|
6
|
+
export declare function assertIndexerDaemonNativeDependencies(loadBetterSqlite3Impl?: LoadBetterSqlite3): Promise<void>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { loadBetterSqlite3 } from "../../sqlite/better-sqlite3.js";
|
|
2
|
+
export const SQLITE_NATIVE_MODULE_UNAVAILABLE = "sqlite_native_module_unavailable";
|
|
3
|
+
export function isSqliteNativeModuleLoadFailure(error) {
|
|
4
|
+
if (!(error instanceof Error)) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
const message = error.message;
|
|
8
|
+
return message.includes("NODE_MODULE_VERSION")
|
|
9
|
+
|| message.includes("better_sqlite3.node")
|
|
10
|
+
|| message.includes("Cannot find module 'better-sqlite3'")
|
|
11
|
+
|| message.includes('Cannot find module "better-sqlite3"');
|
|
12
|
+
}
|
|
13
|
+
export function createSqliteNativeModuleUnavailableError(error) {
|
|
14
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
15
|
+
return new Error(`${SQLITE_NATIVE_MODULE_UNAVAILABLE}: ${detail}`, { cause: error });
|
|
16
|
+
}
|
|
17
|
+
export async function assertIndexerDaemonNativeDependencies(loadBetterSqlite3Impl = loadBetterSqlite3) {
|
|
18
|
+
try {
|
|
19
|
+
await loadBetterSqlite3Impl();
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (isSqliteNativeModuleLoadFailure(error)) {
|
|
23
|
+
throw createSqliteNativeModuleUnavailableError(error);
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -5,6 +5,7 @@ import { createBootstrapProgress } from "../progress/formatting.js";
|
|
|
5
5
|
import { resolveManagedServicePaths } from "../service-paths.js";
|
|
6
6
|
import { UNINITIALIZED_WALLET_ROOT_ID } from "../service-paths.js";
|
|
7
7
|
import { pauseBackgroundFollow, recordBackgroundFollowFailure, resumeBackgroundFollow, withTimeout, } from "./background-follow.js";
|
|
8
|
+
import { assertIndexerDaemonNativeDependencies } from "./native-dependencies.js";
|
|
8
9
|
import { createIndexerDaemonServer } from "./server.js";
|
|
9
10
|
import { buildIndexerDaemonStatus, deriveIndexerDaemonLeaseState, observeIndexerAppliedTip, readCoreTipStatus, refreshIndexerDaemonStatus, writeIndexerDaemonStatus, } from "./status.js";
|
|
10
11
|
import { closeSnapshotLease, createSnapshotHandle, loadSnapshotMaterial, pruneExpiredSnapshotLeases, readSnapshotLease, storeSnapshotLease, } from "./snapshot-leases.js";
|
|
@@ -152,6 +153,25 @@ export function createIndexerDaemonRuntime(options) {
|
|
|
152
153
|
return;
|
|
153
154
|
}
|
|
154
155
|
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
156
|
+
try {
|
|
157
|
+
await assertIndexerDaemonNativeDependencies();
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const now = Date.now();
|
|
161
|
+
state.state = "failed";
|
|
162
|
+
state.lastError = error instanceof Error ? error.message : String(error);
|
|
163
|
+
state.heartbeatAtUnixMs = now;
|
|
164
|
+
state.updatedAtUnixMs = now;
|
|
165
|
+
state.bootstrapPhase = "error";
|
|
166
|
+
state.bootstrapProgress = {
|
|
167
|
+
...createBootstrapProgress("error", DEFAULT_SNAPSHOT_METADATA),
|
|
168
|
+
message: state.lastError,
|
|
169
|
+
lastError: state.lastError,
|
|
170
|
+
updatedAt: now,
|
|
171
|
+
};
|
|
172
|
+
await writeStatus().catch(() => undefined);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
155
175
|
await rm(paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
156
176
|
server = createIndexerDaemonServer({
|
|
157
177
|
getStatus: () => buildIndexerDaemonStatus(state),
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ManagedServicePaths } from "../service-paths.js";
|
|
2
|
+
type ChildProcessLike = {
|
|
3
|
+
pid?: number;
|
|
4
|
+
once(event: "error", listener: (error: Error) => void): unknown;
|
|
5
|
+
once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
6
|
+
off(event: "error", listener: (error: Error) => void): unknown;
|
|
7
|
+
off(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
8
|
+
};
|
|
9
|
+
export declare class IndexerDaemonStartupError extends Error {
|
|
10
|
+
readonly logPath: string;
|
|
11
|
+
readonly logTail: string | null;
|
|
12
|
+
readonly exitCode: number | null;
|
|
13
|
+
readonly signal: NodeJS.Signals | null;
|
|
14
|
+
constructor(message: string, options: {
|
|
15
|
+
logPath: string;
|
|
16
|
+
logTail?: string | null;
|
|
17
|
+
exitCode?: number | null;
|
|
18
|
+
signal?: NodeJS.Signals | null;
|
|
19
|
+
cause?: unknown;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export declare function getIndexerDaemonStartupLogPath(error: unknown): string | null;
|
|
23
|
+
export declare function getIndexerDaemonStartupLogTail(error: unknown): string | null;
|
|
24
|
+
export declare function openIndexerDaemonStartupLog(paths: ManagedServicePaths): Promise<{
|
|
25
|
+
fd: number;
|
|
26
|
+
logPath: string;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function readIndexerDaemonStartupLogTail(logPath: string, maxBytes?: number): Promise<string | null>;
|
|
30
|
+
export declare function classifyIndexerDaemonStartupFailure(logTail: string | null): string;
|
|
31
|
+
export declare function waitForIndexerDaemonStartup(options: {
|
|
32
|
+
child: ChildProcessLike;
|
|
33
|
+
logPath: string;
|
|
34
|
+
waitForReady(): Promise<void>;
|
|
35
|
+
readLogTail?: (logPath: string) => Promise<string | null>;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
export declare function recordIndexerDaemonStartupFailure(options: {
|
|
38
|
+
paths: ManagedServicePaths;
|
|
39
|
+
walletRootId: string;
|
|
40
|
+
binaryVersion: string;
|
|
41
|
+
lastError: string;
|
|
42
|
+
processId?: number | null;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { writeRuntimeStatusFile } from "../../wallet/fs/status-file.js";
|
|
4
|
+
import { readJsonFileIfPresent } from "../managed-runtime/status.js";
|
|
5
|
+
import { SQLITE_NATIVE_MODULE_UNAVAILABLE } from "./native-dependencies.js";
|
|
6
|
+
import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "../types.js";
|
|
7
|
+
const STARTUP_LOG_TAIL_BYTES = 4_096;
|
|
8
|
+
export class IndexerDaemonStartupError extends Error {
|
|
9
|
+
logPath;
|
|
10
|
+
logTail;
|
|
11
|
+
exitCode;
|
|
12
|
+
signal;
|
|
13
|
+
constructor(message, options) {
|
|
14
|
+
super(message, { cause: options.cause });
|
|
15
|
+
this.name = "IndexerDaemonStartupError";
|
|
16
|
+
this.logPath = options.logPath;
|
|
17
|
+
this.logTail = options.logTail ?? null;
|
|
18
|
+
this.exitCode = options.exitCode ?? null;
|
|
19
|
+
this.signal = options.signal ?? null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function getIndexerDaemonStartupLogPath(error) {
|
|
23
|
+
return error instanceof IndexerDaemonStartupError ? error.logPath : null;
|
|
24
|
+
}
|
|
25
|
+
export function getIndexerDaemonStartupLogTail(error) {
|
|
26
|
+
return error instanceof IndexerDaemonStartupError ? error.logTail : null;
|
|
27
|
+
}
|
|
28
|
+
export async function openIndexerDaemonStartupLog(paths) {
|
|
29
|
+
await mkdir(paths.indexerServiceRoot, { recursive: true });
|
|
30
|
+
const handle = await open(paths.indexerDaemonLogPath, "w", 0o600);
|
|
31
|
+
await handle.writeFile([
|
|
32
|
+
`Cogcoin indexer daemon startup log`,
|
|
33
|
+
`pid=${process.pid}`,
|
|
34
|
+
`node=${process.version}`,
|
|
35
|
+
`time=${new Date().toISOString()}`,
|
|
36
|
+
"",
|
|
37
|
+
].join("\n"));
|
|
38
|
+
return {
|
|
39
|
+
fd: handle.fd,
|
|
40
|
+
logPath: paths.indexerDaemonLogPath,
|
|
41
|
+
async close() {
|
|
42
|
+
await handle.close();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export async function readIndexerDaemonStartupLogTail(logPath, maxBytes = STARTUP_LOG_TAIL_BYTES) {
|
|
47
|
+
try {
|
|
48
|
+
const bytes = await readFile(logPath);
|
|
49
|
+
const tail = bytes.subarray(Math.max(0, bytes.byteLength - maxBytes));
|
|
50
|
+
const text = tail.toString("utf8").trim();
|
|
51
|
+
return text.length > 0 ? text : null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function classifyIndexerDaemonStartupFailure(logTail) {
|
|
58
|
+
return logTail?.includes(SQLITE_NATIVE_MODULE_UNAVAILABLE) === true
|
|
59
|
+
? SQLITE_NATIVE_MODULE_UNAVAILABLE
|
|
60
|
+
: "indexer_daemon_start_failed";
|
|
61
|
+
}
|
|
62
|
+
export async function waitForIndexerDaemonStartup(options) {
|
|
63
|
+
const readLogTail = options.readLogTail ?? readIndexerDaemonStartupLogTail;
|
|
64
|
+
const ready = options.waitForReady();
|
|
65
|
+
const childFailure = new Promise((_, reject) => {
|
|
66
|
+
const onError = async (error) => {
|
|
67
|
+
const logTail = await readLogTail(options.logPath);
|
|
68
|
+
reject(new IndexerDaemonStartupError(classifyIndexerDaemonStartupFailure(logTail), {
|
|
69
|
+
logPath: options.logPath,
|
|
70
|
+
logTail,
|
|
71
|
+
cause: error,
|
|
72
|
+
}));
|
|
73
|
+
};
|
|
74
|
+
const onExit = async (code, signal) => {
|
|
75
|
+
const logTail = await readLogTail(options.logPath);
|
|
76
|
+
reject(new IndexerDaemonStartupError(classifyIndexerDaemonStartupFailure(logTail), {
|
|
77
|
+
logPath: options.logPath,
|
|
78
|
+
logTail,
|
|
79
|
+
exitCode: code,
|
|
80
|
+
signal,
|
|
81
|
+
}));
|
|
82
|
+
};
|
|
83
|
+
options.child.once("error", onError);
|
|
84
|
+
options.child.once("exit", onExit);
|
|
85
|
+
ready.then(() => {
|
|
86
|
+
options.child.off("error", onError);
|
|
87
|
+
options.child.off("exit", onExit);
|
|
88
|
+
}, () => {
|
|
89
|
+
options.child.off("error", onError);
|
|
90
|
+
options.child.off("exit", onExit);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
await Promise.race([
|
|
95
|
+
ready,
|
|
96
|
+
childFailure,
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof IndexerDaemonStartupError) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
const logTail = await readLogTail(options.logPath);
|
|
104
|
+
throw new IndexerDaemonStartupError(error instanceof Error ? error.message : "indexer_daemon_start_timeout", {
|
|
105
|
+
logPath: options.logPath,
|
|
106
|
+
logTail,
|
|
107
|
+
cause: error,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export async function recordIndexerDaemonStartupFailure(options) {
|
|
112
|
+
await rm(options.paths.indexerDaemonSocketPath, { force: true }).catch(() => undefined);
|
|
113
|
+
await mkdir(options.paths.indexerServiceRoot, { recursive: true });
|
|
114
|
+
const existing = await readJsonFileIfPresent(options.paths.indexerDaemonStatusPath).catch(() => null);
|
|
115
|
+
if (existing?.processId === (options.processId ?? null)
|
|
116
|
+
&& (existing.state === "schema-mismatch"
|
|
117
|
+
|| (existing.state === "failed" && existing.lastError !== null))) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const status = {
|
|
122
|
+
serviceApiVersion: INDEXER_DAEMON_SERVICE_API_VERSION,
|
|
123
|
+
binaryVersion: options.binaryVersion,
|
|
124
|
+
buildId: null,
|
|
125
|
+
updatedAtUnixMs: now,
|
|
126
|
+
walletRootId: options.walletRootId,
|
|
127
|
+
daemonInstanceId: randomUUID(),
|
|
128
|
+
schemaVersion: INDEXER_DAEMON_SCHEMA_VERSION,
|
|
129
|
+
state: "failed",
|
|
130
|
+
processId: options.processId ?? null,
|
|
131
|
+
startedAtUnixMs: now,
|
|
132
|
+
heartbeatAtUnixMs: now,
|
|
133
|
+
ipcReady: false,
|
|
134
|
+
rpcReachable: false,
|
|
135
|
+
coreBestHeight: null,
|
|
136
|
+
coreBestHash: null,
|
|
137
|
+
appliedTipHeight: null,
|
|
138
|
+
appliedTipHash: null,
|
|
139
|
+
snapshotSeq: null,
|
|
140
|
+
backlogBlocks: null,
|
|
141
|
+
reorgDepth: null,
|
|
142
|
+
lastAppliedAtUnixMs: null,
|
|
143
|
+
activeSnapshotCount: 0,
|
|
144
|
+
lastError: options.lastError,
|
|
145
|
+
backgroundFollowActive: false,
|
|
146
|
+
bootstrapPhase: "error",
|
|
147
|
+
bootstrapProgress: null,
|
|
148
|
+
cogcoinSyncHeight: null,
|
|
149
|
+
cogcoinSyncTargetHeight: null,
|
|
150
|
+
};
|
|
151
|
+
await writeRuntimeStatusFile(options.paths.indexerDaemonStatusPath, status);
|
|
152
|
+
}
|
|
@@ -115,6 +115,16 @@ function mapIndexerStartupError(message) {
|
|
|
115
115
|
health: "starting",
|
|
116
116
|
message: "Indexer daemon is still starting.",
|
|
117
117
|
};
|
|
118
|
+
case "indexer_daemon_start_failed":
|
|
119
|
+
return {
|
|
120
|
+
health: "failed",
|
|
121
|
+
message: "The managed indexer daemon exited before it opened its local IPC socket.",
|
|
122
|
+
};
|
|
123
|
+
case "sqlite_native_module_unavailable":
|
|
124
|
+
return {
|
|
125
|
+
health: "failed",
|
|
126
|
+
message: "The managed indexer daemon could not load its SQLite native module.",
|
|
127
|
+
};
|
|
118
128
|
case "indexer_daemon_service_version_mismatch":
|
|
119
129
|
return {
|
|
120
130
|
health: "service-version-mismatch",
|
|
@@ -15,5 +15,6 @@ export interface ManagedServicePaths {
|
|
|
15
15
|
indexerDaemonLockPath: string;
|
|
16
16
|
indexerDaemonStatusPath: string;
|
|
17
17
|
indexerDaemonSocketPath: string;
|
|
18
|
+
indexerDaemonLogPath: string;
|
|
18
19
|
}
|
|
19
20
|
export declare function resolveManagedServicePaths(dataDir: string, walletRootId?: string): ManagedServicePaths;
|
|
@@ -41,5 +41,6 @@ export function resolveManagedServicePaths(dataDir, walletRootId = UNINITIALIZED
|
|
|
41
41
|
indexerDaemonLockPath: join(walletRuntimeRoot, "indexer-daemon.lock"),
|
|
42
42
|
indexerDaemonStatusPath: join(indexerServiceRoot, "status.json"),
|
|
43
43
|
indexerDaemonSocketPath: resolveIndexerDaemonSocketPath(serviceRootId),
|
|
44
|
+
indexerDaemonLogPath: join(indexerServiceRoot, "daemon.log"),
|
|
44
45
|
};
|
|
45
46
|
}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
import { getIndexerDaemonStartupLogPath, getIndexerDaemonStartupLogTail, } from "../../../bitcoind/indexer-daemon/startup.js";
|
|
2
|
+
function formatIndexerStartupDetail(error) {
|
|
3
|
+
const logPath = getIndexerDaemonStartupLogPath(error);
|
|
4
|
+
const logTail = getIndexerDaemonStartupLogTail(error);
|
|
5
|
+
if (logPath === null && logTail === null) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const tail = logTail === null
|
|
9
|
+
? null
|
|
10
|
+
: logTail.replace(/\s+/g, " ").slice(0, 300);
|
|
11
|
+
return tail === null
|
|
12
|
+
? `Startup log: ${logPath}.`
|
|
13
|
+
: `Startup log: ${logPath}. Last output: ${tail}`;
|
|
14
|
+
}
|
|
1
15
|
export const serviceErrorRules = [
|
|
2
|
-
({ errorCode }) => {
|
|
16
|
+
({ errorCode, error }) => {
|
|
3
17
|
if (errorCode.endsWith("_requires_tty") && errorCode !== "cli_update_requires_tty") {
|
|
4
18
|
return {
|
|
5
19
|
what: "Interactive terminal input is required.",
|
|
@@ -14,6 +28,30 @@ export const serviceErrorRules = [
|
|
|
14
28
|
next: "Check `cogcoin status`, wait for services to settle, and retry. If the state stays degraded, run `cogcoin repair`.",
|
|
15
29
|
};
|
|
16
30
|
}
|
|
31
|
+
if (errorCode === "sqlite_native_module_unavailable") {
|
|
32
|
+
const detail = formatIndexerStartupDetail(error);
|
|
33
|
+
return {
|
|
34
|
+
what: "The managed indexer daemon could not load its SQLite native module.",
|
|
35
|
+
why: `The active Node runtime is ${process.version}, but the installed native sqlite dependency appears to be missing or built for a different Node ABI.${detail === null ? "" : ` ${detail}`}`,
|
|
36
|
+
next: "Use the supported Node runtime for this checkout, then run `npm rebuild better-sqlite3 zeromq` or reinstall dependencies and retry.",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (errorCode === "indexer_daemon_start_failed") {
|
|
40
|
+
const detail = formatIndexerStartupDetail(error);
|
|
41
|
+
return {
|
|
42
|
+
what: "The managed indexer daemon exited before it opened its local IPC socket.",
|
|
43
|
+
why: detail ?? "The daemon process died during startup before Cogcoin could read a service status.",
|
|
44
|
+
next: "Run `cogcoin repair` to clear stale managed indexer artifacts. If this was a local checkout, verify the Node version and rebuild native dependencies.",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (errorCode === "indexer_daemon_start_timeout") {
|
|
48
|
+
const detail = formatIndexerStartupDetail(error);
|
|
49
|
+
return {
|
|
50
|
+
what: "The managed indexer daemon stayed alive but did not open its local IPC socket in time.",
|
|
51
|
+
why: detail ?? "Cogcoin could not attach to the daemon before the startup deadline.",
|
|
52
|
+
next: "Run `cogcoin repair` if this persists. If this was a local checkout, verify the Node version and rebuild native dependencies.",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
17
55
|
if (errorCode === "indexer_daemon_background_follow_recovery_failed") {
|
|
18
56
|
return {
|
|
19
57
|
what: "The managed indexer daemon could not recover automatic background follow.",
|
package/package.json
CHANGED