mirror of
https://github.com/Routstr/routstrd.git
synced 2026-07-30 15:46:14 +00:00
fix: capture daemon crash logs and prevent silent crashes
- Redirect detached daemon stdout/stderr to ~/.routstrd/debug.log instead of ignoring them, so uncaught exception stack traces are no longer lost - Add process-level uncaughtException/unhandledRejection handlers that log to the file logger before the process dies - Wrap setInterval async callbacks (model refresh + refund jobs) in IIFE catch chains so rejected promises can't escape and kill the process silently
This commit is contained in:
+66
-51
@@ -49,6 +49,17 @@ import { FileRequestResponseLogSink } from "./request-response-log-sink";
|
||||
import { refreshModelsAndIntegrations } from "../integrations";
|
||||
import { RoutstrClient } from "@routstr/sdk";
|
||||
|
||||
// Global error handlers — the daemon is spawned detached with stdout/stderr
|
||||
// redirected to a file, so without these, uncaught async errors would kill
|
||||
// the process silently. Log to the file logger before exiting.
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error("UNCAUGHT EXCEPTION:", error);
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
logger.error("UNHANDLED REJECTION:", reason);
|
||||
});
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs(process.argv);
|
||||
const config = await loadDaemonConfig();
|
||||
@@ -172,20 +183,22 @@ async function main(): Promise<void> {
|
||||
`Starting recurring model refresh job (every ${REFRESH_INTERVAL_MS / 1000 / 60 / 60} hours)`,
|
||||
);
|
||||
|
||||
refreshInterval = setInterval(async () => {
|
||||
logger.log("Running scheduled Nostr event refresh...");
|
||||
try {
|
||||
await modelManager.refreshNostrEvents();
|
||||
} catch (error) {
|
||||
logger.error("Scheduled Nostr event refresh failed:", error);
|
||||
}
|
||||
refreshInterval = setInterval(() => {
|
||||
(async () => {
|
||||
logger.log("Running scheduled Nostr event refresh...");
|
||||
try {
|
||||
await modelManager.refreshNostrEvents();
|
||||
} catch (error) {
|
||||
logger.error("Scheduled Nostr event refresh failed:", error);
|
||||
}
|
||||
|
||||
logger.log("Running scheduled model refresh...");
|
||||
try {
|
||||
await refreshModelsAndIntegrations(getRoutstr21Models, updatedConfig, "Scheduled");
|
||||
} catch (error) {
|
||||
logger.error("Scheduled model refresh failed:", error);
|
||||
}
|
||||
logger.log("Running scheduled model refresh...");
|
||||
try {
|
||||
await refreshModelsAndIntegrations(getRoutstr21Models, updatedConfig, "Scheduled");
|
||||
} catch (error) {
|
||||
logger.error("Scheduled model refresh failed:", error);
|
||||
}
|
||||
})().catch((error) => logger.error("Model refresh interval escaped:", error));
|
||||
}, REFRESH_INTERVAL_MS);
|
||||
};
|
||||
|
||||
@@ -206,46 +219,48 @@ async function main(): Promise<void> {
|
||||
`Starting recurring refund job (every ${REFUND_INTERVAL_MS / 1000 / 60} minutes)`,
|
||||
);
|
||||
|
||||
refundInterval = setInterval(async () => {
|
||||
logger.log("Running scheduled refund...");
|
||||
try {
|
||||
const state = store.getState() as any;
|
||||
const pendingDistribution = (state.cachedTokens || []).map(
|
||||
(t: { baseUrl: string; balance?: number }) => ({
|
||||
baseUrl: t.baseUrl,
|
||||
amount: t.balance || 0,
|
||||
}),
|
||||
);
|
||||
const apiKeysStored = (state.apiKeys || []).map(
|
||||
(k: { baseUrl: string; balance?: number }) => ({
|
||||
baseUrl: k.baseUrl,
|
||||
amount: k.balance || 0,
|
||||
}),
|
||||
);
|
||||
refundInterval = setInterval(() => {
|
||||
(async () => {
|
||||
logger.log("Running scheduled refund...");
|
||||
try {
|
||||
const state = store.getState() as any;
|
||||
const pendingDistribution = (state.cachedTokens || []).map(
|
||||
(t: { baseUrl: string; balance?: number }) => ({
|
||||
baseUrl: t.baseUrl,
|
||||
amount: t.balance || 0,
|
||||
}),
|
||||
);
|
||||
const apiKeysStored = (state.apiKeys || []).map(
|
||||
(k: { baseUrl: string; balance?: number }) => ({
|
||||
baseUrl: k.baseUrl,
|
||||
amount: k.balance || 0,
|
||||
}),
|
||||
);
|
||||
|
||||
if (pendingDistribution.length === 0 && apiKeysStored.length === 0) {
|
||||
logger.log("No pending tokens to refund.");
|
||||
return;
|
||||
if (pendingDistribution.length === 0 && apiKeysStored.length === 0) {
|
||||
logger.log("No pending tokens to refund.");
|
||||
return;
|
||||
}
|
||||
|
||||
const mintUrl = walletAdapter.getActiveMintUrl();
|
||||
if (!mintUrl) {
|
||||
logger.log("No active mint URL for refund.");
|
||||
return;
|
||||
}
|
||||
|
||||
const spender = refundClient.getCashuSpender();
|
||||
const results = await spender.refundProviders(mintUrl);
|
||||
|
||||
const successCount = results.filter(
|
||||
(r: { success: boolean }) => r.success,
|
||||
).length;
|
||||
logger.log(
|
||||
`Scheduled refund completed: ${successCount}/${results.length} providers refunded.`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Scheduled refund failed:", error);
|
||||
}
|
||||
|
||||
const mintUrl = walletAdapter.getActiveMintUrl();
|
||||
if (!mintUrl) {
|
||||
logger.log("No active mint URL for refund.");
|
||||
return;
|
||||
}
|
||||
|
||||
const spender = refundClient.getCashuSpender();
|
||||
const results = await spender.refundProviders(mintUrl);
|
||||
|
||||
const successCount = results.filter(
|
||||
(r: { success: boolean }) => r.success,
|
||||
).length;
|
||||
logger.log(
|
||||
`Scheduled refund completed: ${successCount}/${results.length} providers refunded.`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Scheduled refund failed:", error);
|
||||
}
|
||||
})().catch((error) => logger.error("Refund interval escaped:", error));
|
||||
}, REFUND_INTERVAL_MS);
|
||||
};
|
||||
|
||||
|
||||
+6
-2
@@ -1,8 +1,10 @@
|
||||
import { openSync } from "fs";
|
||||
import { logger } from "./utils/logger";
|
||||
import { CONFIG_DIR, LOGS_DIR } from "./utils/config";
|
||||
import { withCrossProcessLock } from "./utils/process-lock";
|
||||
|
||||
const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`;
|
||||
const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`;
|
||||
|
||||
async function isDaemonHealthy(port: string): Promise<boolean> {
|
||||
const controller = new AbortController();
|
||||
@@ -42,9 +44,11 @@ async function startDaemonUnlocked(
|
||||
const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname;
|
||||
const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`;
|
||||
|
||||
const debugLogFd = openSync(DEBUG_LOG_PATH, "a");
|
||||
|
||||
const proc = Bun.spawn(["sh", "-c", shellCmd], {
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
stdout: debugLogFd,
|
||||
stderr: debugLogFd,
|
||||
stdin: "ignore",
|
||||
detached: true,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user