From 02d92ebc559fc45e62f47bb0d6bc25f6a339a2cb Mon Sep 17 00:00:00 2001 From: Priyanshubhartistm Date: Wed, 12 Aug 2026 15:40:32 +0530 Subject: [PATCH] fix(cli): stop info command from hanging when database is unreachable Signed-off-by: Priyanshubhartistm --- .changeset/fix-cli-info-event-count-hang.md | 5 +++ src/cli/commands/info.ts | 48 ++++++++++++--------- 2 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 .changeset/fix-cli-info-event-count-hang.md diff --git a/.changeset/fix-cli-info-event-count-hang.md b/.changeset/fix-cli-info-event-count-hang.md new file mode 100644 index 00000000..1625e7a0 --- /dev/null +++ b/.changeset/fix-cli-info-event-count-hang.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +fix(cli): stop `nostream info` from hanging indefinitely when the database is unreachable diff --git a/src/cli/commands/info.ts b/src/cli/commands/info.ts index 9b162ff8..f0b62e47 100644 --- a/src/cli/commands/info.ts +++ b/src/cli/commands/info.ts @@ -1,5 +1,5 @@ import fs from 'fs' -import knex from 'knex' +import { Client } from 'pg' import packageJson from '../../../package.json' import { loadMergedSettings } from '../utils/config' @@ -24,34 +24,36 @@ type I2PGuidancePayload = { } const getEventCount = async (): Promise => { - const db = knex({ - client: 'pg', - connection: process.env.DB_URI - ? process.env.DB_URI + // Uses a raw pg.Client (connect/query/end) rather than a knex pool: when the + // connection attempt itself times out, knex/tarn's pool can leave the + // underlying socket open (never disposed), which keeps this one-shot CLI + // process alive indefinitely. pg.Client.end() reliably closes the socket + // even on a failed/timed-out connect, so the process can exit normally. + const client = new Client( + process.env.DB_URI + ? { connectionString: process.env.DB_URI, connectionTimeoutMillis: 1000 } : { host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, + connectionTimeoutMillis: 1000, }, - pool: { - min: 0, - max: 1, - idleTimeoutMillis: 1000, - acquireTimeoutMillis: 1000, - propagateCreateError: false, - }, - acquireConnectionTimeout: 1000, - } as any) + ) try { - const result = await db('events').whereNull('deleted_at').count<{ count: string | number }>('* as count').first() - return Number(result?.count ?? 0) + await client.connect() + const result = await client.query('select count(*) as count from events where deleted_at is null') + return Number(result.rows[0]?.count ?? 0) } catch { return null } finally { - await db.destroy() + try { + await client.end() + } catch { + // already disconnected/never connected — nothing to clean up + } } } @@ -66,9 +68,13 @@ const getRelayUptimeSeconds = async (): Promise => { return null } - const startedAtResult = await runCommandWithOutput('docker', ['inspect', '--format', '{{.State.StartedAt}}', containerId], { - timeoutMs: 1000, - }) + const startedAtResult = await runCommandWithOutput( + 'docker', + ['inspect', '--format', '{{.State.StartedAt}}', containerId], + { + timeoutMs: 1000, + }, + ) if (!startedAtResult.ok || startedAtResult.code !== 0) { return null } @@ -197,7 +203,7 @@ export const runInfo = async (options: InfoOptions): Promise => { 'http://127.0.0.1:7070/?page=i2p_tunnels', ]) - const matches = new Set((`${result.stdout}\n${result.stderr}`).match(/[a-z2-7]{52}\.b32\.i2p/g) ?? []) + const matches = new Set(`${result.stdout}\n${result.stderr}`.match(/[a-z2-7]{52}\.b32\.i2p/g) ?? []) if (matches.size > 0) { if (options.json) { writeJson({