diff --git a/container-runtime/src/Adapters/EffectCreator.ts b/container-runtime/src/Adapters/EffectCreator.ts index c549a28ab..dc020d23c 100644 --- a/container-runtime/src/Adapters/EffectCreator.ts +++ b/container-runtime/src/Adapters/EffectCreator.ts @@ -289,6 +289,7 @@ export function makeEffects(context: EffectContext): Effects { getStatus(...[o]: Parameters) { return rpcRound("get-status", o) as ReturnType }, + /// DEPRECATED setMainStatus(o: { status: "running" | "stopped" }): Promise { return rpcRound("set-main-status", o) as ReturnType< T.Effects["setHealth"] diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index f595de0b9..5567dd979 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -292,10 +292,13 @@ export class RpcListener { ) }) .when(stopType, async ({ id }) => { - this.callbacks?.removeChild("main") return handleRpc( id, - this.system.stop().then((result) => ({ result })), + this.system.stop().then((result) => { + this.callbacks?.removeChild("main") + + return { result } + }), ) }) .when(exitType, async ({ id, params }) => { diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index 0eba3277d..1d7e8fc97 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -70,20 +70,13 @@ export class SystemForStartOs implements System { this.starting = true effects.constRetry = utils.once(() => effects.restart()) let mainOnTerm: () => Promise | undefined - const started = async (onTerm: () => Promise) => { - await effects.setMainStatus({ status: "running" }) - mainOnTerm = onTerm - return null - } const daemons = await ( await this.abi.main({ effects, - started, }) ).build() this.runningMain = { stop: async () => { - if (mainOnTerm) await mainOnTerm() await daemons.term() }, } diff --git a/core/startos/src/service/effects/subcontainer/sync.rs b/core/startos/src/service/effects/subcontainer/sync.rs index c92dbefff..39e40243e 100644 --- a/core/startos/src/service/effects/subcontainer/sync.rs +++ b/core/startos/src/service/effects/subcontainer/sync.rs @@ -53,16 +53,22 @@ pub fn kill_init(procfs: &Path, chroot: &Path) -> Result<(), Error> { ) })?; if pids.0.len() == 2 && pids.0[1] == 1 { - nix::sys::signal::kill(Pid::from_raw(pid), nix::sys::signal::SIGKILL) - .with_ctx(|_| { - ( - ErrorKind::Filesystem, - lazy_format!( - "kill pid {} (determined to be pid 1 in subcontainer)", - pid - ), - ) - })?; + match nix::sys::signal::kill( + Pid::from_raw(pid), + Some(nix::sys::signal::SIGKILL), + ) { + Err(Errno::ESRCH) => Ok(()), + a => a, + } + .with_ctx(|_| { + ( + ErrorKind::Filesystem, + lazy_format!( + "kill pid {} (determined to be pid 1 in subcontainer)", + pid + ), + ) + })?; } } } @@ -510,10 +516,13 @@ pub fn exec( std::thread::spawn(move || { if let Ok(pid) = recv_pid.blocking_recv() { for sig in sig.forever() { - nix::sys::signal::kill( + match nix::sys::signal::kill( Pid::from_raw(pid), Some(nix::sys::signal::Signal::try_from(sig).unwrap()), - ) + ) { + Err(Errno::ESRCH) => Ok(()), + a => a, + } .unwrap(); } } diff --git a/core/startos/src/service/transition/mod.rs b/core/startos/src/service/transition/mod.rs index e04dcea41..3eafa0774 100644 --- a/core/startos/src/service/transition/mod.rs +++ b/core/startos/src/service/transition/mod.rs @@ -29,7 +29,25 @@ impl ServiceActorSeed { pub fn start(&self) -> Transition<'_> { Transition { kind: TransitionKind::Starting, - future: self.persistent_container.start().boxed(), + future: async { + self.persistent_container.start().await?; + let id = &self.id; + self.ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(id) + .or_not_found(id)? + .as_status_info_mut() + .started() + }) + .await + .result?; + + Ok(()) + } + .boxed(), } } @@ -47,8 +65,7 @@ impl ServiceActorSeed { .as_idx_mut(id) .or_not_found(id)? .as_status_info_mut() - .as_started_mut() - .ser(&None) + .stopped() }) .await .result?; diff --git a/core/startos/src/status/mod.rs b/core/startos/src/status/mod.rs index 7d7830c1c..5fd03f14d 100644 --- a/core/startos/src/status/mod.rs +++ b/core/startos/src/status/mod.rs @@ -28,11 +28,24 @@ impl StatusInfo { } } impl Model { + pub fn start(&mut self) -> Result<(), Error> { + self.as_desired_mut().map_mutate(|s| Ok(s.start()))?; + Ok(()) + } + pub fn started(&mut self) -> Result<(), Error> { + self.as_started_mut() + .map_mutate(|s| Ok(Some(s.unwrap_or_else(|| Utc::now()))))?; + Ok(()) + } pub fn stop(&mut self) -> Result<(), Error> { self.as_desired_mut().map_mutate(|s| Ok(s.stop()))?; self.as_health_mut().ser(&Default::default())?; Ok(()) } + pub fn stopped(&mut self) -> Result<(), Error> { + self.as_started_mut().ser(&None)?; + Ok(()) + } pub fn init(&mut self) -> Result<(), Error> { self.as_started_mut().ser(&None)?; self.as_desired_mut().map_mutate(|s| { diff --git a/sdk/base/lib/Effects.ts b/sdk/base/lib/Effects.ts index eb44fae3e..69164d40b 100644 --- a/sdk/base/lib/Effects.ts +++ b/sdk/base/lib/Effects.ts @@ -67,7 +67,7 @@ export type Effects = { packageId?: PackageId callback?: () => void }): Promise - /** indicate to the host os what runstate the service is in */ + /** DEPRECATED: indicate to the host os what runstate the service is in */ setMainStatus(options: SetMainStatus): Promise // dependency diff --git a/sdk/base/lib/types.ts b/sdk/base/lib/types.ts index 87bc3f11a..c86743696 100644 --- a/sdk/base/lib/types.ts +++ b/sdk/base/lib/types.ts @@ -44,10 +44,7 @@ export namespace ExpectedExports { * This is the entrypoint for the main container. Used to start up something like the service that the * package represents, like running a bitcoind in a bitcoind-wrapper. */ - export type main = (options: { - effects: Effects - started(onTerm: () => PromiseLike): PromiseLike - }) => Promise + export type main = (options: { effects: Effects }) => Promise /** * Every time a service launches (both on startup, and on install) this function is called before packageInit diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index a8f8c1ce6..8510b251f 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -634,10 +634,7 @@ export class StartSdk { */ setupInterfaces: setupServiceInterfaces, setupMain: ( - fn: (o: { - effects: Effects - started(onTerm: () => PromiseLike): PromiseLike - }) => Promise>, + fn: (o: { effects: Effects }) => Promise>, ) => setupMain(fn), trigger: { defaultTrigger, @@ -690,13 +687,8 @@ export class StartSdk { }, }, Daemons: { - of( - effects: Effects, - started: - | ((onTerm: () => PromiseLike) => PromiseLike) - | null, - ) { - return Daemons.of({ effects, started }) + of(effects: Effects) { + return Daemons.of({ effects }) }, }, SubContainer: { diff --git a/sdk/package/lib/mainFn/Daemon.ts b/sdk/package/lib/mainFn/Daemon.ts index f17b79bf7..24688e6cd 100644 --- a/sdk/package/lib/mainFn/Daemon.ts +++ b/sdk/package/lib/mainFn/Daemon.ts @@ -24,6 +24,7 @@ export class Daemon< private commandController: CommandController | null = null private shouldBeRunning = false protected exitedSuccess = false + private exiting: Promise | null = null private onExitFns: ((success: boolean) => void)[] = [] protected constructor( private subcontainer: C, @@ -36,7 +37,7 @@ export class Daemon< return this.oneshot } static of() { - return async | null>( + return | null>( effects: T.Effects, subcontainer: C, exec: DaemonCommandType, @@ -51,9 +52,7 @@ export class Daemon< ) const res = new Daemon(subc, startCommand) effects.onLeaveContext(() => { - res - .term({ destroySubcontainer: true }) - .catch((e) => console.error(asError(e))) + res.term({ destroySubcontainer: true }).catch((e) => console.error(e)) }) return res } @@ -114,19 +113,26 @@ export class Daemon< this.shouldBeRunning = false this.exitedSuccess = false if (this.commandController) { - await this.commandController - .term({ ...termOptions }) - .catch((e) => console.error(asError(e))) + this.exiting = this.commandController.term({ ...termOptions }) this.commandController = null this.onExitFns = [] + } + if (this.exiting) { + await this.exiting.catch(console.error) if (termOptions?.destroySubcontainer) { await this.subcontainer?.destroy() } + this.exiting = null } } subcontainerRc(): SubContainerRc | null { return this.subcontainer?.rc() ?? null } + sharesSubcontainerWith( + other: Daemon | null>, + ): boolean { + return this.subcontainer?.guid === other.subcontainer?.guid + } onExit(fn: (success: boolean) => void) { this.onExitFns.push(fn) } diff --git a/sdk/package/lib/mainFn/Daemons.ts b/sdk/package/lib/mainFn/Daemons.ts index fa7c5934c..9bb2bc693 100644 --- a/sdk/package/lib/mainFn/Daemons.ts +++ b/sdk/package/lib/mainFn/Daemons.ts @@ -161,9 +161,6 @@ export class Daemons { private constructor( readonly effects: T.Effects, - readonly started: - | ((onTerm: () => PromiseLike) => PromiseLike) - | null, readonly ids: Ids[], readonly healthDaemons: HealthDaemon[], ) {} @@ -180,26 +177,13 @@ export class Daemons * @param started * @returns */ - static of(options: { - effects: T.Effects - /** - * A closure to run once the system is launched. If you are in main, provide the `started` argument you receive from the function arguments - */ - started: ((onTerm: () => PromiseLike) => PromiseLike) | null - }) { - return new Daemons( - options.effects, - options.started, - [], - [], - ) + static of(options: { effects: T.Effects }) { + return new Daemons(options.effects, [], []) } private addDaemonImpl( id: Id, - daemon: Promise< - Daemon | null> - > | null, + daemon: Daemon | null> | null, requires: Ids[], ready: Ready | typeof EXIT_SUCCESS, ) { @@ -215,12 +199,7 @@ export class Daemons ) const ids = [...this.ids, id] as (Ids | Id)[] const healthDaemons = [...this.healthDaemons, healthDaemon] - return new Daemons( - this.effects, - this.started, - ids, - healthDaemons, - ) + return new Daemons(this.effects, ids, healthDaemons) } /** @@ -256,7 +235,7 @@ export class Daemons if (!options) return prev const daemon = "daemon" in options - ? Promise.resolve(options.daemon) + ? options.daemon : Daemon.of()( this.effects, options.subcontainer, @@ -397,12 +376,10 @@ export class Daemons "EXIT_SUCCESS", this.effects, ) - const daemons = await new Daemons( - this.effects, - this.started, - this.ids, - [...this.healthDaemons, healthDaemon], - ).build() + const daemons = await new Daemons(this.effects, this.ids, [ + ...this.healthDaemons, + healthDaemon, + ]).build() try { await res } finally { @@ -412,23 +389,51 @@ export class Daemons } async term() { - for (let result of await Promise.allSettled( - this.healthDaemons.map((x) => x.term({ destroySubcontainer: true })), - )) { - if (result.status === "rejected") { - console.error(result.reason) + const remaining = new Set(this.healthDaemons) + + while (remaining.size > 0) { + // Find daemons with no remaining dependents + const canShutdown = [...remaining].filter( + (daemon) => + ![...remaining].some((other) => + other.dependencies.some((dep) => dep.id === daemon.id), + ), + ) + + if (canShutdown.length === 0) { + // Dependency cycle that should not happen, just shutdown remaining daemons + console.warn( + "Dependency cycle detected, shutting down remaining daemons", + ) + canShutdown.push(...[...remaining].reverse()) } + + // remove from remaining set + canShutdown.forEach((daemon) => remaining.delete(daemon)) + + // Shutdown daemons with no remaining dependents concurrently + await Promise.allSettled( + canShutdown.map(async (daemon) => { + try { + console.debug(`Terminating daemon ${daemon.id}`) + const destroySubcontainer = daemon.daemon + ? ![...remaining].some((d) => + d.daemon?.sharesSubcontainerWith(daemon.daemon!), + ) + : false + await daemon.term({ destroySubcontainer }) + } catch (e) { + console.error(e) + } + }), + ) } } async build() { - this.effects.onLeaveContext(() => { - this.term().catch((e) => console.error(asError(e))) - }) for (const daemon of this.healthDaemons) { await daemon.init() } - this.started?.(() => this.term()) return this } } diff --git a/sdk/package/lib/mainFn/HealthDaemon.ts b/sdk/package/lib/mainFn/HealthDaemon.ts index 836171619..2c498dfc6 100644 --- a/sdk/package/lib/mainFn/HealthDaemon.ts +++ b/sdk/package/lib/mainFn/HealthDaemon.ts @@ -34,8 +34,8 @@ export class HealthDaemon { private resolvedReady: boolean = false private readyPromise: Promise constructor( - private readonly daemon: Promise> | null, - private readonly dependencies: HealthDaemon[], + readonly daemon: Daemon | null, + readonly dependencies: HealthDaemon[], readonly id: string, readonly ready: Ready | typeof EXIT_SUCCESS, readonly effects: Effects, @@ -60,11 +60,9 @@ export class HealthDaemon { this.running = false this.healthCheckCleanup?.() - await this.daemon?.then((d) => - d.term({ - ...termOptions, - }), - ) + await this.daemon?.term({ + ...termOptions, + }) } /** Want to add another notifier that the health might have changed */ diff --git a/sdk/package/lib/mainFn/Oneshot.ts b/sdk/package/lib/mainFn/Oneshot.ts index b210b868b..380f8a453 100644 --- a/sdk/package/lib/mainFn/Oneshot.ts +++ b/sdk/package/lib/mainFn/Oneshot.ts @@ -15,7 +15,7 @@ export class Oneshot< C extends SubContainer | null = SubContainer | null, > extends Daemon { static of() { - return async | null>( + return | null>( effects: T.Effects, subcontainer: C, exec: DaemonCommandType, diff --git a/sdk/package/lib/mainFn/index.ts b/sdk/package/lib/mainFn/index.ts index 3730058d7..45748d194 100644 --- a/sdk/package/lib/mainFn/index.ts +++ b/sdk/package/lib/mainFn/index.ts @@ -15,10 +15,7 @@ export const DEFAULT_SIGTERM_TIMEOUT = 60_000 * @returns */ export const setupMain = ( - fn: (o: { - effects: T.Effects - started(onTerm: () => PromiseLike): PromiseLike - }) => Promise>, + fn: (o: { effects: T.Effects }) => Promise>, ): T.ExpectedExports.main => { return async (options) => { const result = await fn(options)