@@ -20,6 +20,7 @@ import { loadChallengesIntoPKC } from "../../challenge-packages/challenge-utils.
2020import { migrateDataDirectory } from "../../common-utils/data-migration.js" ;
2121import { createBsoResolvers , DEFAULT_PROVIDERS } from "../../common-utils/resolvers.js" ;
2222import { pruneStaleStates , writeDaemonState , deleteDaemonState } from "../../common-utils/daemon-state.js" ;
23+ import { createDaemonFileLogger , type DaemonFileLogger } from "../../common-utils/daemon-file-logger.js" ;
2324import fs from "fs" ;
2425import fsPromise from "fs/promises" ;
2526
@@ -46,6 +47,44 @@ const defaultPkcOptions: InputPKCOptions = {
4647 httpRoutersOptions : defaults . HTTP_TRACKERS
4748} ;
4849
50+ export interface KeepKuboUpTickDeps {
51+ pkcRpcUrl : URL ;
52+ tcpPortUsedCheck : ( port : number , host : string ) => Promise < boolean > ;
53+ pkcOptionsFromFlag : { kuboRpcClientsOptions ?: unknown } | undefined ;
54+ usingDifferentProcessRpc : boolean ;
55+ hasKuboProcess : boolean ;
56+ hasPendingKuboStart : boolean ;
57+ keepKuboUp : ( ) => Promise < void > ;
58+ createOrConnectRpc : ( ) => Promise < void > ;
59+ onError : ( message : string ) => void ;
60+ }
61+
62+ /**
63+ * Runs one tick of the keepKuboUp interval. Exported so it can be unit-tested.
64+ *
65+ * Both `tcpPortUsedCheck` and the downstream `keepKuboUp`/`createOrConnectRpc` calls
66+ * are wrapped in try/catch — a transient ETIMEDOUT from the port check (or any other
67+ * error from this tick) must not propagate to the setInterval callback, which would
68+ * become an unhandledRejection (issue #37 bug 3).
69+ */
70+ export async function runKeepKuboUpTick ( deps : KeepKuboUpTickDeps ) : Promise < void > {
71+ let isRpcPortTaken = false ;
72+ try {
73+ isRpcPortTaken = await deps . tcpPortUsedCheck ( Number ( deps . pkcRpcUrl . port ) , deps . pkcRpcUrl . hostname ) ;
74+ if ( ! deps . pkcOptionsFromFlag ?. kuboRpcClientsOptions && ! isRpcPortTaken && ! deps . usingDifferentProcessRpc ) await deps . keepKuboUp ( ) ;
75+ else if ( deps . pkcOptionsFromFlag ?. kuboRpcClientsOptions && ! deps . usingDifferentProcessRpc ) await deps . keepKuboUp ( ) ;
76+ // Retry if kubo died and onKuboExit's restart attempt failed (e.g. transient port conflict)
77+ else if ( ! deps . hasKuboProcess && ! deps . hasPendingKuboStart && ! deps . usingDifferentProcessRpc ) await deps . keepKuboUp ( ) ;
78+ } catch ( error ) {
79+ deps . onError ( `keepKuboUp tick error (will retry): ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
80+ }
81+ try {
82+ await deps . createOrConnectRpc ( ) ;
83+ } catch ( error ) {
84+ deps . onError ( `createOrConnectRpc tick error (will retry): ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
85+ }
86+ }
87+
4988export default class Daemon extends Command {
5089 static override description = `Run a network-connected Bitsocial node. Once the daemon is running you can create and start your communities and receive publications from users. The daemon will also serve web ui on http that can be accessed through a browser on any machine. Within the web ui users are able to browse, create and manage their communities fully P2P.
5190 Options can be passed to the RPC's instance through flag --pkcOptions.optionName. For a list of pkc options (https://github.com/pkcprotocol/pkc-js?tab=readme-ov-file#pkcoptions)
@@ -115,24 +154,13 @@ export default class Daemon extends Command {
115154 private async _pipeDebugLogsToLogFile (
116155 logPath : string ,
117156 Logger : PKCLoggerType
118- ) : Promise < { logFilePath : string ; stdoutWrite : typeof process . stdout . write } > {
157+ ) : Promise < { logFilePath : string ; stdoutWrite : typeof process . stdout . write ; fileLogger : DaemonFileLogger } > {
119158 const { logFilePath, deletedLogFile, logfilesCapacity } = await this . _getNewLogfileByEvacuatingOldLogsIfNeeded ( logPath ) ;
120159
121- const logFile = fs . createWriteStream ( logFilePath , { flags : "a" } ) ;
160+ const fileLogger = createDaemonFileLogger ( { logFilePath } ) ;
122161 const stdoutWrite = process . stdout . write . bind ( process . stdout ) ;
123162 const stderrWrite = process . stderr . write . bind ( process . stderr ) ;
124163
125- const isLogFileOverLimit = ( ) => logFile . bytesWritten > 20000000 ; // 20mb
126-
127- const writeTimestampedLine = ( text : string , stream : "stdout" | "stderr" ) => {
128- if ( isLogFileOverLimit ( ) ) return ;
129- if ( ! text || text . trim ( ) . length === 0 ) return ;
130- const timestamp = `[${ new Date ( ) . toISOString ( ) } ] [${ stream } ] ` ;
131- const lines = text . split ( "\n" ) ;
132- const timestamped = lines . map ( ( line , i ) => ( i === 0 ? timestamp + line : line ) ) . join ( "\n" ) ;
133- logFile . write ( timestamped ) ;
134- } ;
135-
136164 // Redirect debug library output directly to the log file
137165 // instead of stderr, so only real errors appear in the terminal
138166 const require = createRequire ( import . meta. url ) ;
@@ -142,24 +170,32 @@ export default class Daemon extends Command {
142170 debugModule . inspectOpts . colors = true ;
143171 debugModule . inspectOpts . hideDate = true ;
144172 debugModule . log = ( ...args : any [ ] ) => {
145- writeTimestampedLine ( formatWithOptions ( { depth : Logger . inspectOpts ?. depth || 10 , colors : true } , ...args ) . trimStart ( ) + EOL , "stderr" ) ;
173+ const text = formatWithOptions ( { depth : Logger . inspectOpts ?. depth || 10 , colors : true } , ...args ) . trimStart ( ) + EOL ;
174+ const wrote = fileLogger . writeTimestampedLine ( text , "stderr" ) ;
175+ // If the file logger could not accept the write (closed / pending buffer full),
176+ // fall back to original stderr so debug output is never silently lost
177+ if ( ! wrote ) stderrWrite ( text ) ;
146178 } ;
147179
148180 const asString = ( data : string | Uint8Array ) => ( typeof data === "string" ? data : Buffer . from ( data ) . toString ( ) ) ;
149181
150182 process . stdout . write = ( ...args ) => {
151183 //@ts -expect-error
152184 const res = stdoutWrite ( ...args ) ;
153- writeTimestampedLine ( asString ( args [ 0 ] ) , "stdout" ) ;
185+ fileLogger . writeTimestampedLine ( asString ( args [ 0 ] ) , "stdout" ) ;
154186 return res ;
155187 } ;
156188
157189 process . stderr . write = ( ...args ) => {
158- // Only write stderr to the log file, not to the terminal.
159- // Debug output goes to stderr; we want it in logs only.
160- // Real errors are caught by uncaughtException/unhandledRejection handlers
161- // which use console.error -> stderr.write -> this override -> log file.
162- writeTimestampedLine ( asString ( args [ 0 ] ) . trimStart ( ) , "stderr" ) ;
190+ // Debug output goes to stderr; route it to the log file.
191+ // If the file logger is unavailable (closed, errored), fall back to original stderr
192+ // so output is never silently swallowed.
193+ const text = asString ( args [ 0 ] ) ;
194+ const wrote = fileLogger . writeTimestampedLine ( text . trimStart ( ) , "stderr" ) ;
195+ if ( ! wrote ) {
196+ //@ts -expect-error
197+ return stderrWrite ( ...args ) ;
198+ }
163199 return true ;
164200 } ;
165201
@@ -184,9 +220,13 @@ export default class Daemon extends Command {
184220 console . error ( err ) ;
185221 } ) ;
186222
187- process . on ( "exit" , ( ) => logFile . close ( ) ) ;
223+ process . on ( "exit" , ( ) => {
224+ // close() returns a promise but exit handlers must be synchronous.
225+ // Best-effort: trigger the close; the underlying writeStream flushes on process exit.
226+ fileLogger . close ( ) . catch ( ( ) => { } ) ;
227+ } ) ;
188228
189- return { logFilePath, stdoutWrite } ;
229+ return { logFilePath, stdoutWrite, fileLogger } ;
190230 }
191231
192232 async run ( ) {
@@ -532,16 +572,17 @@ export default class Daemon extends Command {
532572
533573 keepKuboUpInterval = setInterval ( async ( ) => {
534574 if ( mainProcessExited ) return ;
535- const isRpcPortTaken = await tcpPortUsed . check ( Number ( pkcRpcUrl . port ) , pkcRpcUrl . hostname ) ;
536- try {
537- if ( ! pkcOptionsFromFlag ?. kuboRpcClientsOptions && ! isRpcPortTaken && ! usingDifferentProcessRpc ) await keepKuboUp ( ) ;
538- else if ( pkcOptionsFromFlag ?. kuboRpcClientsOptions && ! usingDifferentProcessRpc ) await keepKuboUp ( ) ;
539- // Retry if kubo died and onKuboExit's restart attempt failed (e.g. transient port conflict)
540- else if ( ! kuboProcess && ! pendingKuboStart && ! usingDifferentProcessRpc ) await keepKuboUp ( ) ;
541- } catch ( error ) {
542- log . trace ( `keepKuboUp error (will retry): ${ error instanceof Error ? error . message : String ( error ) } ` ) ;
543- }
544- await createOrConnectRpc ( ) ;
575+ await runKeepKuboUpTick ( {
576+ pkcRpcUrl,
577+ tcpPortUsedCheck : ( port , host ) => tcpPortUsed . check ( port , host ) ,
578+ pkcOptionsFromFlag,
579+ usingDifferentProcessRpc,
580+ hasKuboProcess : ! ! kuboProcess ,
581+ hasPendingKuboStart : ! ! pendingKuboStart ,
582+ keepKuboUp,
583+ createOrConnectRpc,
584+ onError : ( msg ) => log . trace ( msg )
585+ } ) ;
545586 } , 5000 ) ;
546587 } catch ( err ) {
547588 const errorMsg = err instanceof Error ? err . message : String ( err ) ;
0 commit comments