diff --git a/core/src/service/effects/subcontainer/sync.rs b/core/src/service/effects/subcontainer/sync.rs index e37207ced..c4d27994a 100644 --- a/core/src/service/effects/subcontainer/sync.rs +++ b/core/src/service/effects/subcontainer/sync.rs @@ -283,6 +283,10 @@ impl ExecParams { let set_gid = gid.ok(); unsafe { cmd.pre_exec(move || { + // Create a new process group so entrypoint scripts that do + // kill(0, SIGTERM) don't cascade to other subcontainers. + nix::unistd::setsid() + .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?; if !groups.is_empty() { nix::unistd::setgroups(&groups) .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?; diff --git a/sdk/package/lib/mainFn/Daemon.ts b/sdk/package/lib/mainFn/Daemon.ts index 242c5827d..8b464dac8 100644 --- a/sdk/package/lib/mainFn/Daemon.ts +++ b/sdk/package/lib/mainFn/Daemon.ts @@ -27,6 +27,7 @@ export class Daemon< protected exitedSuccess = false private onExitFns: ((success: boolean) => void)[] = [] private loop: { abort: AbortController; done: Promise } | null = null + private _managed = false protected constructor( private subcontainer: C, private startCommand: () => Promise>, @@ -42,7 +43,8 @@ export class Daemon< * Factory method to create a new Daemon. * * Returns a curried function: `(effects, subcontainer, exec) => Daemon`. - * The daemon auto-terminates when the effects context is left. + * Registers an `onLeaveContext` callback that terminates the daemon when the + * effects context is left. */ static of() { return | null>( @@ -60,7 +62,9 @@ export class Daemon< ) const res = new Daemon(subc, startCommand) effects.onLeaveContext(() => { - res.term({ destroySubcontainer: true }).catch((e) => logErrorOnce(e)) + if (!res._managed) { + res.term({ destroySubcontainer: true }).catch((e) => logErrorOnce(e)) + } }) return res } @@ -173,6 +177,14 @@ export class Daemon< await this.subcontainer?.destroy() } } + /** + * Mark this daemon as managed by a {@link Daemons} instance. + * Suppresses the individual `onLeaveContext` termination since the + * `Daemons` instance handles ordered shutdown. + */ + markManaged() { + this._managed = true + } /** Get a reference-counted handle to the daemon's subcontainer, or null if there is none */ subcontainerRc(): SubContainerRc | null { return this.subcontainer?.rc() ?? null diff --git a/sdk/package/lib/mainFn/Daemons.ts b/sdk/package/lib/mainFn/Daemons.ts index 4b6bc69c0..69bd078e9 100644 --- a/sdk/package/lib/mainFn/Daemons.ts +++ b/sdk/package/lib/mainFn/Daemons.ts @@ -176,6 +176,7 @@ Daemons.of({ export class Daemons implements T.DaemonBuildable { + private termPromise: Promise | null = null private constructor( readonly effects: T.Effects, readonly ids: Ids[], @@ -413,6 +414,13 @@ export class Daemons * if a dependency cycle is detected. */ async term() { + if (!this.termPromise) { + this.termPromise = this._term() + } + return this.termPromise + } + + private async _term() { const remaining = new Set(this.healthDaemons) while (remaining.size > 0) { @@ -459,7 +467,11 @@ export class Daemons * @returns This `Daemons` instance, now running */ async build() { + this.effects.onLeaveContext(() => { + this.term().catch((e) => console.error(e)) + }) for (const daemon of this.healthDaemons) { + daemon.daemon?.markManaged() await daemon.updateStatus() } return this