fix(daemon): send the pid-publish degrade to the log the detached daemon keeps

The degrade warning exists so an operator on a volume without hard links can
tell torn-write protection is off, but publishDaemonPidFile runs inside the
detached daemon: daemon-init spawns it with stdio 'ignore' and destroys the
startup stderr pipe once it reports ready, so console alone reaches nobody on
exactly the path the warning was written for -- the constraint daemon-file-log
states in its header comment.

Take an optional log and emit both, matching daemon-endpoint-lifecycle's
handling of the sibling endpoint degrade three lines from this call site.
This commit is contained in:
Brennan Benson
2026-08-28 08:02:58 -07:00
parent 0a5d79aeee
commit 8b69e9e833
3 changed files with 43 additions and 9 deletions
+12 -8
View File
@@ -274,14 +274,18 @@ async function main(): Promise<void> {
...(pidPath && launchNonce
? {
publishEndpointOwnership: () =>
publishDaemonPidFile(pidPath, {
pid: process.pid,
...readyIdentity,
...(entryPath ? { entryPath } : {}),
...(appVersion ? { appVersion } : {}),
...(spawnerExecPath ? { spawnerExecPath } : {}),
launchNonce
})
publishDaemonPidFile(
pidPath,
{
pid: process.pid,
...readyIdentity,
...(entryPath ? { entryPath } : {}),
...(appVersion ? { appVersion } : {}),
...(spawnerExecPath ? { spawnerExecPath } : {}),
launchNonce
},
daemonLog
)
}
: {}),
log: daemonLog,
@@ -149,6 +149,27 @@ describe('publishDaemonPidFile atomicity', () => {
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('non-atomic'))
})
it('records the degrade on the daemon file log, which is the only channel the detached daemon keeps', () => {
// The daemon runs with stdio 'ignore' and its startup stderr pipe is destroyed once
// it is up, so a console-only warning never reaches the operator it is written for.
const entries: { event: string; details?: Record<string, unknown> }[] = []
fsFaults.linkError = Object.assign(new Error('injected ENOTSUP'), { code: 'ENOTSUP' })
publishDaemonPidFile(pidPath, RECORD, {
log: (event, details) => entries.push({ event, ...(details ? { details } : {}) })
})
expect(entries).toEqual([{ event: 'pid-publish-degraded', details: { errno: 'ENOTSUP' } }])
})
it('leaves the daemon file log untouched when the atomic path succeeds', () => {
const entries: string[] = []
publishDaemonPidFile(pidPath, RECORD, { log: (event) => entries.push(event) })
expect(entries).toEqual([])
})
it('keeps exclusive-create semantics on the degraded path too', () => {
publishDaemonPidFile(pidPath, { pid: 1, startedAtMs: 5, launchNonce: 'other' })
fsFaults.linkError = Object.assign(new Error('injected ENOTSUP'), { code: 'ENOTSUP' })
+10 -1
View File
@@ -11,6 +11,7 @@ import {
} from 'node:fs'
import { join } from 'node:path'
import { fsyncFileSync } from '../../shared/secure-file'
import type { DaemonFileLog } from './daemon-file-log'
import { PROTOCOL_VERSION } from './types'
import { reapOrphanedDaemonPidPublishClaims } from './daemon-pid-publish-claim-reap'
import { DaemonCrashLoopError, DaemonRespawnThrottle } from './daemon-respawn-throttle'
@@ -176,7 +177,11 @@ export function getDaemonPidPublishClaimPath(pidPath: string): string {
return `${pidPath}.publish-${process.pid}-${randomUUID()}`
}
export function publishDaemonPidFile(pidPath: string, pidFile: DaemonPidFile): void {
export function publishDaemonPidFile(
pidPath: string,
pidFile: DaemonPidFile,
log?: Pick<DaemonFileLog, 'log'>
): void {
const claimPath = getDaemonPidPublishClaimPath(pidPath)
try {
writeFileSync(claimPath, serializeDaemonPidFile(pidFile), { mode: 0o600, flag: 'wx' })
@@ -199,6 +204,10 @@ export function publishDaemonPidFile(pidPath: string, pidFile: DaemonPidFile): v
// instead of trading a torn-write window for no pid record at all. Warn because the
// degrade is otherwise invisible: an operator on such a volume (network mount,
// container overlay) should be able to tell the atomic path is not in use.
// Both channels, matching daemon-endpoint-lifecycle.ts: inside the detached
// daemon stdio is 'ignore' and its startup stderr pipe is destroyed once it is
// up, so console alone reaches nobody on the path this warning exists for.
log?.log('pid-publish-degraded', { errno: describeErrnoCode(error) })
console.warn(
`[daemon] pid-record hard-link publish failed (${describeErrnoCode(error)}); ` +
'degrading to the non-atomic exclusive write — torn-write protection is off for this record'