diff --git a/src/execution/base.ts b/src/execution/base.ts index 13647b0e5..d47b622d1 100644 --- a/src/execution/base.ts +++ b/src/execution/base.ts @@ -102,4 +102,37 @@ export abstract class BaseExecutionWithCommand< * needed to run at all. */ readonly servicesNotNeeded = this._servicesNotNeeded.promise; + + /** + * Resolves when any of the services this script depends on have terminated + * (see {@link ServiceScriptExecution.terminated} for exact definiton). + */ + readonly anyServiceTerminated = Promise.race( + this._config.services.map( + (service) => this._executor.getExecution(service).terminated + ) + ); + + /** + * Ensure that all of the services this script depends on are running. + */ + protected async _startServices(): Promise> { + if (this._config.services.length > 0) { + const results = await Promise.all( + this._config.services.map((service) => + this._executor.getExecution(service).start() + ) + ); + const errors: Failure[] = []; + for (const result of results) { + if (!result.ok) { + errors.push(...result.error); + } + } + if (errors.length > 0) { + return {ok: false, error: errors}; + } + } + return {ok: true, value: undefined}; + } } diff --git a/src/execution/service.ts b/src/execution/service.ts index 499d6885c..6323ca6cd 100644 --- a/src/execution/service.ts +++ b/src/execution/service.ts @@ -6,16 +6,31 @@ import {BaseExecutionWithCommand} from './base.js'; import {Fingerprint} from '../fingerprint.js'; +import {Deferred} from '../util/deferred.js'; import type {ExecutionResult} from './base.js'; import type {ServiceScriptConfig} from '../config.js'; import type {Executor} from '../executor.js'; import type {Logger} from '../logging/logger.js'; +import type {Failure} from '../event.js'; +import type {Result} from '../error.js'; /** * Execution for a {@link ServiceScriptConfig}. */ export class ServiceScriptExecution extends BaseExecutionWithCommand { + private readonly _terminated = new Deferred>(); + + /** + * Resolves as "ok" when this script decides it is no longer needed, and + * either has begun shutting down, or never needed to start in the first + * place. + * + * Resolves with an error if this service exited unexpectedly, or if any of + * its own service dependencies exited unexpectedly. + */ + readonly terminated = this._terminated.promise; + constructor( config: ServiceScriptConfig, executor: Executor, @@ -42,5 +57,11 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand> { + // TODO(aomarks) Implement service starting/stopping. + throw new Error('Not implemented'); + } } diff --git a/src/execution/standard.ts b/src/execution/standard.ts index 920303d44..04904d1af 100644 --- a/src/execution/standard.ts +++ b/src/execution/standard.ts @@ -24,7 +24,7 @@ import type {StandardScriptConfig} from '../config.js'; import type {FingerprintString} from '../fingerprint.js'; import type {Logger} from '../logging/logger.js'; import type {Cache, CacheHit} from '../caching/cache.js'; -import type {StartCancelled} from '../event.js'; +import type {Failure, StartCancelled} from '../event.js'; import type {AbsoluteEntry} from '../util/glob.js'; import type {FileManifestEntry, FileManifestString} from '../util/manifest.js'; @@ -316,6 +316,41 @@ export class StandardScriptExecution extends BaseExecutionWithCommand 0) { + const servicesStarted = await this._startServices(); + if (!servicesStarted.ok) { + return servicesStarted; + } + + void this.anyServiceTerminated.then((result) => { + if (this._state === 'after-running') { + // This is expected after we're done. + return; + } + if (result.ok) { + // This should never happen and indicates an internal error. The + // service believed that nothing was depending on it anymore, but + // we're still running. + earlyServiceTermination = { + script: this._config, + type: 'failure', + reason: 'unknown-error-thrown', + error: new Error( + 'Internal error: service dependency terminated unexpectedly' + ), + }; + } else { + // The service knows it exited too early. Propagate that error. + earlyServiceTermination = result.error; + } + // Stop running. If a service we depend on is down, then we know we're + // in an invalid state too. + child.kill(); + this._executor.notifyFailure(); + }); + } + this._state = 'running'; this._logger.log({ script: this._config, @@ -353,11 +388,15 @@ export class StandardScriptExecution extends BaseExecutionWithCommand