mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
fix: replace fire-and-forget restart loop in Daemon with tracked AbortController
- Track the restart loop as an awaitable { abort, done } handle
- Remove shouldBeRunning flag — signal.aborted serves the same purpose
- Remove exiting field — term() awaits command termination inline
- Guard start() on loop existence to prevent concurrent restart loops
- Make backoff sleep abortable so term() returns immediately
- Suppress error logging during intentional termination
- Loop clears its own handle in finally block for natural exit (oneshot)
This commit is contained in:
@@ -2,11 +2,7 @@ import * as T from '../../../base/lib/types'
|
||||
import { asError } from '../../../base/lib/util/asError'
|
||||
import { logErrorOnce } from '../../../base/lib/util/logErrorOnce'
|
||||
import { Drop } from '../util'
|
||||
import {
|
||||
SubContainer,
|
||||
SubContainerOwned,
|
||||
SubContainerRc,
|
||||
} from '../util/SubContainer'
|
||||
import { SubContainer, SubContainerRc } from '../util/SubContainer'
|
||||
import { CommandController } from './CommandController'
|
||||
import { DaemonCommandType } from './Daemons'
|
||||
import { Oneshot } from './Oneshot'
|
||||
@@ -28,10 +24,9 @@ export class Daemon<
|
||||
C extends SubContainer<Manifest> | null = SubContainer<Manifest> | null,
|
||||
> extends Drop {
|
||||
private commandController: CommandController<Manifest, C> | null = null
|
||||
private shouldBeRunning = false
|
||||
protected exitedSuccess = false
|
||||
private exiting: Promise<void> | null = null
|
||||
private onExitFns: ((success: boolean) => void)[] = []
|
||||
private loop: { abort: AbortController; done: Promise<void> } | null = null
|
||||
protected constructor(
|
||||
private subcontainer: C,
|
||||
private startCommand: () => Promise<CommandController<Manifest, C>>,
|
||||
@@ -77,31 +72,38 @@ export class Daemon<
|
||||
* until {@link term} is called.
|
||||
*/
|
||||
async start() {
|
||||
if (this.commandController) {
|
||||
if (this.loop) {
|
||||
return
|
||||
}
|
||||
this.shouldBeRunning = true
|
||||
const abort = new AbortController()
|
||||
const done = this.runLoop(abort.signal)
|
||||
this.loop = { abort, done }
|
||||
}
|
||||
|
||||
private async runLoop(signal: AbortSignal) {
|
||||
let timeoutCounter = 0
|
||||
;(async () => {
|
||||
while (this.shouldBeRunning) {
|
||||
if (this.commandController)
|
||||
await this.commandController
|
||||
.term({})
|
||||
.catch((err) => logErrorOnce(err))
|
||||
try {
|
||||
while (!signal.aborted) {
|
||||
if (this.commandController) {
|
||||
await this.commandController.term({}).catch(logErrorOnce)
|
||||
this.commandController = null
|
||||
}
|
||||
try {
|
||||
this.commandController = await this.startCommand()
|
||||
if (!this.shouldBeRunning) {
|
||||
// handles race condition if stopped while starting
|
||||
await this.term()
|
||||
if (signal.aborted) {
|
||||
await this.commandController.term({}).catch(logErrorOnce)
|
||||
this.commandController = null
|
||||
break
|
||||
}
|
||||
const success = await this.commandController.wait().then(
|
||||
(_) => true,
|
||||
(err) => {
|
||||
if (this.shouldBeRunning) logErrorOnce(err)
|
||||
if (!signal.aborted) logErrorOnce(err)
|
||||
return false
|
||||
},
|
||||
)
|
||||
this.commandController = null
|
||||
if (signal.aborted) break
|
||||
for (const fn of this.onExitFns) {
|
||||
try {
|
||||
fn(success)
|
||||
@@ -114,16 +116,28 @@ export class Daemon<
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
if (!signal.aborted) console.error(e)
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, timeoutCounter))
|
||||
if (signal.aborted) break
|
||||
await new Promise<void>((resolve) => {
|
||||
const timer = setTimeout(resolve, timeoutCounter)
|
||||
signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
})
|
||||
timeoutCounter += TIMEOUT_INCREMENT_MS
|
||||
timeoutCounter = Math.min(MAX_TIMEOUT_MS, timeoutCounter)
|
||||
}
|
||||
})().catch((err) => {
|
||||
console.error(asError(err))
|
||||
})
|
||||
} finally {
|
||||
this.loop = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate the daemon, stopping its underlying command.
|
||||
*
|
||||
@@ -140,20 +154,24 @@ export class Daemon<
|
||||
timeout?: number | undefined
|
||||
destroySubcontainer?: boolean
|
||||
}) {
|
||||
this.shouldBeRunning = false
|
||||
this.exitedSuccess = false
|
||||
if (this.commandController) {
|
||||
this.exiting = this.commandController.term({ ...termOptions })
|
||||
this.commandController = null
|
||||
this.onExitFns = []
|
||||
|
||||
if (this.loop) {
|
||||
this.loop.abort.abort()
|
||||
}
|
||||
if (this.exiting) {
|
||||
await this.exiting.catch(logErrorOnce)
|
||||
|
||||
const exiting = this.commandController?.term({ ...termOptions })
|
||||
this.commandController = null
|
||||
if (exiting) await exiting.catch(logErrorOnce)
|
||||
|
||||
if (this.loop) {
|
||||
await this.loop.done
|
||||
}
|
||||
|
||||
if (termOptions?.destroySubcontainer) {
|
||||
await this.subcontainer?.destroy()
|
||||
}
|
||||
this.exiting = null
|
||||
}
|
||||
}
|
||||
/** Get a reference-counted handle to the daemon's subcontainer, or null if there is none */
|
||||
subcontainerRc(): SubContainerRc<Manifest> | null {
|
||||
|
||||
Reference in New Issue
Block a user