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 { asError } from '../../../base/lib/util/asError'
|
||||||
import { logErrorOnce } from '../../../base/lib/util/logErrorOnce'
|
import { logErrorOnce } from '../../../base/lib/util/logErrorOnce'
|
||||||
import { Drop } from '../util'
|
import { Drop } from '../util'
|
||||||
import {
|
import { SubContainer, SubContainerRc } from '../util/SubContainer'
|
||||||
SubContainer,
|
|
||||||
SubContainerOwned,
|
|
||||||
SubContainerRc,
|
|
||||||
} from '../util/SubContainer'
|
|
||||||
import { CommandController } from './CommandController'
|
import { CommandController } from './CommandController'
|
||||||
import { DaemonCommandType } from './Daemons'
|
import { DaemonCommandType } from './Daemons'
|
||||||
import { Oneshot } from './Oneshot'
|
import { Oneshot } from './Oneshot'
|
||||||
@@ -28,10 +24,9 @@ export class Daemon<
|
|||||||
C extends SubContainer<Manifest> | null = SubContainer<Manifest> | null,
|
C extends SubContainer<Manifest> | null = SubContainer<Manifest> | null,
|
||||||
> extends Drop {
|
> extends Drop {
|
||||||
private commandController: CommandController<Manifest, C> | null = null
|
private commandController: CommandController<Manifest, C> | null = null
|
||||||
private shouldBeRunning = false
|
|
||||||
protected exitedSuccess = false
|
protected exitedSuccess = false
|
||||||
private exiting: Promise<void> | null = null
|
|
||||||
private onExitFns: ((success: boolean) => void)[] = []
|
private onExitFns: ((success: boolean) => void)[] = []
|
||||||
|
private loop: { abort: AbortController; done: Promise<void> } | null = null
|
||||||
protected constructor(
|
protected constructor(
|
||||||
private subcontainer: C,
|
private subcontainer: C,
|
||||||
private startCommand: () => Promise<CommandController<Manifest, C>>,
|
private startCommand: () => Promise<CommandController<Manifest, C>>,
|
||||||
@@ -77,31 +72,38 @@ export class Daemon<
|
|||||||
* until {@link term} is called.
|
* until {@link term} is called.
|
||||||
*/
|
*/
|
||||||
async start() {
|
async start() {
|
||||||
if (this.commandController) {
|
if (this.loop) {
|
||||||
return
|
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
|
let timeoutCounter = 0
|
||||||
;(async () => {
|
try {
|
||||||
while (this.shouldBeRunning) {
|
while (!signal.aborted) {
|
||||||
if (this.commandController)
|
if (this.commandController) {
|
||||||
await this.commandController
|
await this.commandController.term({}).catch(logErrorOnce)
|
||||||
.term({})
|
this.commandController = null
|
||||||
.catch((err) => logErrorOnce(err))
|
}
|
||||||
try {
|
try {
|
||||||
this.commandController = await this.startCommand()
|
this.commandController = await this.startCommand()
|
||||||
if (!this.shouldBeRunning) {
|
if (signal.aborted) {
|
||||||
// handles race condition if stopped while starting
|
await this.commandController.term({}).catch(logErrorOnce)
|
||||||
await this.term()
|
this.commandController = null
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
const success = await this.commandController.wait().then(
|
const success = await this.commandController.wait().then(
|
||||||
(_) => true,
|
(_) => true,
|
||||||
(err) => {
|
(err) => {
|
||||||
if (this.shouldBeRunning) logErrorOnce(err)
|
if (!signal.aborted) logErrorOnce(err)
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
this.commandController = null
|
||||||
|
if (signal.aborted) break
|
||||||
for (const fn of this.onExitFns) {
|
for (const fn of this.onExitFns) {
|
||||||
try {
|
try {
|
||||||
fn(success)
|
fn(success)
|
||||||
@@ -114,16 +116,28 @@ export class Daemon<
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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 += TIMEOUT_INCREMENT_MS
|
||||||
timeoutCounter = Math.min(MAX_TIMEOUT_MS, timeoutCounter)
|
timeoutCounter = Math.min(MAX_TIMEOUT_MS, timeoutCounter)
|
||||||
}
|
}
|
||||||
})().catch((err) => {
|
} finally {
|
||||||
console.error(asError(err))
|
this.loop = null
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Terminate the daemon, stopping its underlying command.
|
* Terminate the daemon, stopping its underlying command.
|
||||||
*
|
*
|
||||||
@@ -140,19 +154,23 @@ export class Daemon<
|
|||||||
timeout?: number | undefined
|
timeout?: number | undefined
|
||||||
destroySubcontainer?: boolean
|
destroySubcontainer?: boolean
|
||||||
}) {
|
}) {
|
||||||
this.shouldBeRunning = false
|
|
||||||
this.exitedSuccess = false
|
this.exitedSuccess = false
|
||||||
if (this.commandController) {
|
this.onExitFns = []
|
||||||
this.exiting = this.commandController.term({ ...termOptions })
|
|
||||||
this.commandController = null
|
if (this.loop) {
|
||||||
this.onExitFns = []
|
this.loop.abort.abort()
|
||||||
}
|
}
|
||||||
if (this.exiting) {
|
|
||||||
await this.exiting.catch(logErrorOnce)
|
const exiting = this.commandController?.term({ ...termOptions })
|
||||||
if (termOptions?.destroySubcontainer) {
|
this.commandController = null
|
||||||
await this.subcontainer?.destroy()
|
if (exiting) await exiting.catch(logErrorOnce)
|
||||||
}
|
|
||||||
this.exiting = null
|
if (this.loop) {
|
||||||
|
await this.loop.done
|
||||||
|
}
|
||||||
|
|
||||||
|
if (termOptions?.destroySubcontainer) {
|
||||||
|
await this.subcontainer?.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** Get a reference-counted handle to the daemon's subcontainer, or null if there is none */
|
/** Get a reference-counted handle to the daemon's subcontainer, or null if there is none */
|
||||||
|
|||||||
Reference in New Issue
Block a user