Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/webui/daemon-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,23 @@ export async function startDaemonServer(rpcUrl: URL, ipfsGatewayUrl: URL, pkcOpt
// Start pkc-js RPC
const log = PKCLogger("bitsocial-cli:daemon:startDaemonServer");
const webuiExpressApp = express();
const httpServer = webuiExpressApp.listen(Number(rpcUrl.port));
// Wait for bind to actually complete before returning. Calling express.listen() without
// awaiting 'listening' lets startup proceed before the port is accepting connections,
// and without an 'error' handler a bind failure becomes an uncaughtException that kills
// the daemon *after* it has already logged "Communities in data path" — see issue #42.
const httpServer = await new Promise<import("http").Server>((resolve, reject) => {
const server = webuiExpressApp.listen(Number(rpcUrl.port));
const onListening = () => {
server.off("error", onError);
resolve(server);
};
const onError = (err: Error) => {
server.off("listening", onListening);
reject(err);
};
server.once("listening", onListening);
server.once("error", onError);
});
log("HTTP server is running on", "0.0.0.0" + ":" + rpcUrl.port);
const rpcAuthKey = await _generateRpcAuthKeyIfNotExisting(pkcOptions.dataPath!);
const PKCRpc = await import("@pkcprotocol/pkc-js/rpc");
Expand Down
52 changes: 52 additions & 0 deletions test/webui/daemon-server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, it, expect } from "vitest";
import net from "net";
import { directory as randomDirectory } from "tempy";
import { startDaemonServer } from "../../dist/webui/daemon-server.js";

// Regression test for issue #42:
// startDaemonServer used to call webuiExpressApp.listen(port) fire-and-forget — no
// await on 'listening', no 'error' handler. If bind failed, the promise still resolved
// and the bind error showed up later as an uncaughtException, crashing the daemon
// AFTER the test helper had already accepted "Communities in data path" as readiness.
// The contract we want: if the port can't be bound, startDaemonServer must reject.
describe("startDaemonServer bind contract", () => {
it("rejects when the RPC port is already taken (regression for issue #42)", async () => {
// The blocker must bind the *same way* the daemon does — no host argument —
// so it lands on whatever default Node picks for this platform. On macOS and
// Windows that default is the IPv6 wildcard `::` with IPV6_V6ONLY=true, which
// does NOT conflict with a 0.0.0.0 bind. Binding the blocker to 127.0.0.1 or
// 0.0.0.0 would let the daemon's listen succeed there and never hit EADDRINUSE.
const blocker = net.createServer();
const port = await new Promise<number>((resolve, reject) => {
blocker.once("listening", () => {
const addr = blocker.address();
if (addr && typeof addr === "object") resolve(addr.port);
else reject(new Error(`unexpected address ${JSON.stringify(addr)}`));
});
blocker.once("error", reject);
blocker.listen(0);
});

// Swallow any stray uncaughtException emitted by an unguarded server.listen()
// so the test process survives long enough to assert the actual behavior.
const stray: Error[] = [];
const uncaughtHandler = (err: Error) => stray.push(err);
process.on("uncaughtException", uncaughtHandler);

try {
await expect(
startDaemonServer(
new URL(`ws://127.0.0.1:${port}`),
new URL("http://127.0.0.1:6754"),
{ dataPath: randomDirectory() }
)
).rejects.toThrow(/EADDRINUSE|address already in use/i);
// And no stray uncaughtException either — the bind error must come back
// through the promise, not the process.
expect(stray).toEqual([]);
} finally {
process.off("uncaughtException", uncaughtHandler);
await new Promise<void>((resolve) => blocker.close(() => resolve()));
}
});
});
Loading