From 089b8c9d6af8db1cfe5fdb8ba6508d8a870a1c5e Mon Sep 17 00:00:00 2001 From: Alexander Marks Date: Tue, 25 Oct 2022 10:26:04 -0700 Subject: [PATCH 01/11] Gracefully close socket on test process SIGINT --- src/test/util/test-rig-command-child.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/util/test-rig-command-child.ts b/src/test/util/test-rig-command-child.ts index bf1b9aae9..498308e91 100644 --- a/src/test/util/test-rig-command-child.ts +++ b/src/test/util/test-rig-command-child.ts @@ -59,3 +59,11 @@ if (!ipcPath) { } const socket = net.createConnection(ipcPath); new ChildIpcClient(socket); + +process.on('SIGINT', () => { + // Gracefully close the socket before we are terminated. This helps avoid + // occasional ECONNRESET errors on the other side. + socket.end(() => { + process.exit(1); + }); +}); From ca06a3c45c5fcf491b5130de6fa80361827e15de Mon Sep 17 00:00:00 2001 From: Alexander Marks Date: Tue, 25 Oct 2022 10:26:47 -0700 Subject: [PATCH 02/11] Test matcher: only consume output up to the match and no further --- src/test/util/test-rig.ts | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/test/util/test-rig.ts b/src/test/util/test-rig.ts index 997bb170e..d0ca4f915 100644 --- a/src/test/util/test-rig.ts +++ b/src/test/util/test-rig.ts @@ -326,28 +326,35 @@ class ExecResult { /** * Waits for the given content to be logged to either stdout or stderr. * - * When it does, it consumes all the stdout and stderr that's been emitted - * so far and returns it. + * When it does, it consumes all stdout or stderr that's been emitted up to + * that match so far. */ - async waitForLog(matcher: RegExp): Promise<{stdout: string; stderr: string}> { + waitForLog(matcher: RegExp): Promise { const deferred = new Deferred(); this._logMatchers.push({re: matcher, deferred}); // In case we've already received the log we're watching for this._checkMatchersAgainstLogs(); - await deferred.promise; - const stdout = this._stdout; - const stderr = this._stderr; - this._stdout = ''; - this._stderr = ''; - return {stdout, stderr}; + return deferred.promise; } private _checkMatchersAgainstLogs() { - for (const matcher of this._logMatchers) { - if (matcher.re.test(this._stdout) || matcher.re.test(this._stderr)) { - matcher.deferred.resolve(); + let stdoutLastIndex = -1; + let stderrLastIndex = -1; + for (const {re, deferred} of this._logMatchers) { + if (re.test(this._stdout)) { + deferred.resolve(); + stdoutLastIndex = Math.max(stdoutLastIndex, re.lastIndex); + } else if (re.test(this._stderr)) { + stderrLastIndex = Math.max(stderrLastIndex, re.lastIndex); + deferred.resolve(); } } + if (stdoutLastIndex > 0) { + this._stdout = this._stdout.slice(stdoutLastIndex); + } + if (stderrLastIndex > 0) { + this._stderr = this._stderr.slice(stderrLastIndex); + } } private readonly _onStdout = (chunk: string | Buffer) => { From 1597fa38c41d45661fd323dd0894a654a6793a1e Mon Sep 17 00:00:00 2001 From: Alexander Marks Date: Sun, 23 Oct 2022 20:07:16 -0700 Subject: [PATCH 03/11] Check that we show service stdout/stderr --- src/execution/service.ts | 16 ++++++++++++++++ src/test/service.test.ts | 8 +++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/execution/service.ts b/src/execution/service.ts index 1cf5924de..81444ec8b 100644 --- a/src/execution/service.ts +++ b/src/execution/service.ts @@ -271,6 +271,22 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand { this._onChildExited(); }); + this._state.child.stdout.on('data', (data: string | Buffer) => { + this._logger.log({ + script: this._config, + type: 'output', + stream: 'stdout', + data, + }); + }); + this._state.child.stderr.on('data', (data: string | Buffer) => { + this._logger.log({ + script: this._config, + type: 'output', + stream: 'stderr', + data, + }); + }); return this._state.started.promise; } case 'initial': diff --git a/src/test/service.test.ts b/src/test/service.test.ts index 1ed8eced2..91f25aaff 100644 --- a/src/test/service.test.ts +++ b/src/test/service.test.ts @@ -35,7 +35,7 @@ test.after.each(async (ctx) => { }); test( - 'simple consumer and service', + 'simple consumer and service with stdout', timeout(async ({rig}) => { // consumer // | @@ -69,6 +69,12 @@ test( const serviceInv = await service.nextInvocation(); await wireit.waitForLog(/Service started/); + // Confirm we show stdout/stderr from services + serviceInv.stdout('service stdout'); + await wireit.waitForLog(/service stdout/); + serviceInv.stderr('service stderr'); + await wireit.waitForLog(/service stderr/); + // The consumer starts and finishes const consumerInv = await consumer.nextInvocation(); // Wait a moment to ensure the service stays running From 85f9130e847098fed9ee06bfa401162b46ff699a Mon Sep 17 00:00:00 2001 From: Alexander Marks Date: Sun, 23 Oct 2022 11:12:48 -0700 Subject: [PATCH 04/11] Make services wait for their own services to start --- src/execution/service.ts | 57 ++++++++++++++++++++++++--- src/test/service.test.ts | 85 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 5 deletions(-) diff --git a/src/execution/service.ts b/src/execution/service.ts index 81444ec8b..5554fef82 100644 --- a/src/execution/service.ts +++ b/src/execution/service.ts @@ -27,6 +27,10 @@ type ServiceState = fingerprint: Deferred; } | {id: 'unstarted'} + | { + id: 'depsStarting'; + started: Deferred>; + } | { id: 'starting'; child: ScriptChildProcess; @@ -82,12 +86,18 @@ function unexpectedState(state: ServiceState) { * ├─◄─ abort ───┤ UNSTARTED │ │ * │ └─────┬─────┘ ▼ * │ │ │ - * │ start │ + * │ start ╭─╮ │ + * │ │ │ start │ + * │ ┌───────▼────▼─┴┐ │ + * ├─◄─ abort ─┤ DEPS_STARTING ├───── depStartErr ───►───┤ + * │ └───────┬───────┘ │ + * │ │ │ + * │ depsStarted │ * │ │ ╭─╮ │ * │ │ │ start │ * │ ┌────▼──▼─┴┐ │ - * │ ╭◄─ abort ┤ STARTING ├─── startErr or ────►──────┤ - * │ │ └────┬────┬┘ depServiceStartErr │ + * │ ╭◄─ abort ┤ STARTING ├──── startErr ──────►──────┤ + * │ │ └────┬────┬┘ │ * ▼ │ │ │ │ * │ │ │ ▼ │ * │ │ │ ╰─── depServiceExit ──►──╮ │ @@ -168,6 +178,7 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand> { switch (this._state.id) { case 'unstarted': { + this._state = { + id: 'depsStarting', + started: new Deferred(), + }; + void this._startServices().then(() => { + this._onDepsStarted(); + }); + return this._state.started.promise; + } + case 'initial': + case 'executingDeps': + case 'fingerprinting': + case 'depsStarting': + case 'starting': + case 'started': + case 'stopping': + case 'stopped': { + throw unexpectedState(this._state); + } + default: { + throw unknownState(this._state); + } + } + } + + private _onDepsStarted() { + switch (this._state.id) { + case 'depsStarting': { this._state = { id: 'starting', child: new ScriptChildProcess(this._config), - started: new Deferred(), + started: this._state.started, }; void this._state.child.started.then(() => { this._onChildStarted(); @@ -287,11 +329,12 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand { + // consumer + // | + // v + // service ---> serviceDep + // | + // v + // standardDep + + const consumer = await rig.newCommand(); + const service = await rig.newCommand(); + const standardDep = await rig.newCommand(); + const serviceDep = await rig.newCommand(); + await rig.writeAtomic({ + 'package.json': { + scripts: { + consumer: 'wireit', + service: 'wireit', + standardDep: 'wireit', + serviceDep: 'wireit', + }, + wireit: { + consumer: { + command: consumer.command, + dependencies: ['service'], + }, + service: { + command: service.command, + service: true, + dependencies: ['standardDep', 'serviceDep'], + }, + standardDep: { + command: standardDep.command, + }, + serviceDep: { + command: serviceDep.command, + service: true, + }, + }, + }, + }); + + const wireit = rig.exec('npm run consumer'); + + // The service's standard dep must finish before the service can start + const standardDepInv = await standardDep.nextInvocation(); + // Wait a moment to ensure the service hasn't started yet + await new Promise((resolve) => setTimeout(resolve, 100)); + assert.equal(service.numInvocations, 0); + assert.equal(serviceDep.numInvocations, 0); + assert.equal(consumer.numInvocations, 0); + standardDepInv.exit(0); + + // The service's own service dep must start first + const serviceDepInv = await serviceDep.nextInvocation(); + await wireit.waitForLog(/\[serviceDep\] Service started/); + + // Now the main service can start + const serviceInv = await service.nextInvocation(); + await wireit.waitForLog(/\[service\] Service started/); + + // The consumer starts and finishes + const consumerInv = await consumer.nextInvocation(); + // Wait a moment to ensure the services stay running + await new Promise((resolve) => setTimeout(resolve, 100)); + assert.ok(serviceInv.isRunning); + assert.ok(serviceDepInv.isRunning); + consumerInv.exit(0); + + // Services shut down in reverse order + await serviceInv.closed; + await wireit.waitForLog(/\[service\] Service stopped/); + await serviceDepInv.closed; + await wireit.waitForLog(/\[serviceDep\] Service stopped/); + + await wireit.exit; + assert.equal(standardDep.numInvocations, 1); + assert.equal(serviceDep.numInvocations, 1); + assert.equal(service.numInvocations, 1); + assert.equal(consumer.numInvocations, 1); + }) +); + test.run(); From 75db754b0d96165c0e3a7a17d03eb3b5bd8d8616 Mon Sep 17 00:00:00 2001 From: Alexander Marks Date: Tue, 25 Oct 2022 09:02:03 -0700 Subject: [PATCH 05/11] Standard scripts fail when service exits unexpectedly --- src/execution/service.ts | 12 +++++++++- src/test/service.test.ts | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/execution/service.ts b/src/execution/service.ts index 5554fef82..f46a2c42b 100644 --- a/src/execution/service.ts +++ b/src/execution/service.ts @@ -422,13 +422,23 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand { + // consumer + // | + // v + // service + + const consumer = await rig.newCommand(); + const service = await rig.newCommand(); + await rig.writeAtomic({ + 'package.json': { + scripts: { + consumer: 'wireit', + service: 'wireit', + }, + wireit: { + consumer: { + command: consumer.command, + dependencies: ['service'], + }, + service: { + command: service.command, + service: true, + }, + }, + }, + }); + + const wireit = rig.exec('npm run consumer'); + + // Service starts + const serviceInv = await service.nextInvocation(); + + // Consumer starts + const consumerInv = await consumer.nextInvocation(); + + // Service exits unexpectedly + serviceInv.exit(1); + await wireit.waitForLog(/\[service\] Service exited unexpectedly/); + + // Consumer is killed + await consumerInv.closed; + await wireit.waitForLog(/\[consumer\] Killed/); + + // Wireit exits with an error code + assert.equal((await wireit.exit).code, 1); + }) +); + test.run(); From 7c3324cb0e24767eb70d6b6d147541e05e384413 Mon Sep 17 00:00:00 2001 From: Alexander Marks Date: Tue, 25 Oct 2022 12:08:41 -0700 Subject: [PATCH 06/11] Remember service failures for next start call --- src/execution/service.ts | 55 ++++++++++++++++++++++----- src/test/service.test.ts | 81 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 9 deletions(-) diff --git a/src/execution/service.ts b/src/execution/service.ts index f46a2c42b..fe21aa45f 100644 --- a/src/execution/service.ts +++ b/src/execution/service.ts @@ -41,7 +41,11 @@ type ServiceState = child: ScriptChildProcess; } | {id: 'stopping'} - | {id: 'stopped'}; + | {id: 'stopped'} + | { + id: 'failed'; + failure: Failure; + }; function unknownState(state: never) { return new Error( @@ -182,7 +186,8 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand { + // entrypoint + // / \ + // v v + // consumer1 consumer2 + // \ / \ + // \ / v + // v v blocker + // service + + const consumer1 = await rig.newCommand(); + const consumer2 = await rig.newCommand(); + const service = await rig.newCommand(); + const blocker = await rig.newCommand(); + + await rig.writeAtomic({ + 'package.json': { + scripts: { + entrypoint: 'wireit', + consumer1: 'wireit', + consumer2: 'wireit', + service: 'wireit', + blocker: 'wireit', + }, + wireit: { + entrypoint: { + dependencies: ['consumer1', 'consumer2'], + }, + consumer1: { + command: consumer1.command, + dependencies: ['service'], + }, + consumer2: { + command: consumer2.command, + dependencies: ['service', 'blocker'], + }, + service: { + command: service.command, + service: true, + }, + blocker: { + command: blocker.command, + }, + }, + }, + }); + + const wireit = rig.exec('npm run entrypoint', { + env: { + // Set "continue" failure mode so that consumer2 tries to start the + // service even though consumer1 will have already failed. + WIREIT_FAILURES: 'continue', + }, + }); + + // Service starts + const serviceInv = await service.nextInvocation(); + + // Blocker starts + const blockerInv = await blocker.nextInvocation(); + + // Consumer 1 starts + const consumer1Inv = await consumer1.nextInvocation(); + + // Service fails + serviceInv.exit(1); + + // Consumer 1 is killed + await consumer1Inv.closed; + + // Blocker unblocks + blockerInv.exit(0); + + // Consumer 2 can't start becuase the consumer already failed, so wireit + // exits. + assert.equal((await wireit.exit).code, 1); + }) +); + test.run(); From 5a6a88f9401633d1c29dc359ec32f1e4048feb47 Mon Sep 17 00:00:00 2001 From: Alexander Marks Date: Tue, 25 Oct 2022 14:49:24 -0700 Subject: [PATCH 07/11] Make anyServiceTerminated protected --- src/execution/base.ts | 2 +- src/execution/standard.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/execution/base.ts b/src/execution/base.ts index d47b622d1..dfdd6a89e 100644 --- a/src/execution/base.ts +++ b/src/execution/base.ts @@ -107,7 +107,7 @@ export abstract class BaseExecutionWithCommand< * Resolves when any of the services this script depends on have terminated * (see {@link ServiceScriptExecution.terminated} for exact definiton). */ - readonly anyServiceTerminated = Promise.race( + protected readonly _anyServiceTerminated = Promise.race( this._config.services.map( (service) => this._executor.getExecution(service).terminated ) diff --git a/src/execution/standard.ts b/src/execution/standard.ts index f73e6939c..e2a64dbe2 100644 --- a/src/execution/standard.ts +++ b/src/execution/standard.ts @@ -323,7 +323,7 @@ export class StandardScriptExecution extends BaseExecutionWithCommand { + void this._anyServiceTerminated.then((result) => { if (this._state === 'after-running') { // This is expected after we're done. return; From a9ece489bc4bd8f0afa6b4d5d010ae19843b3ec4 Mon Sep 17 00:00:00 2001 From: Alexander Marks Date: Tue, 25 Oct 2022 14:47:46 -0700 Subject: [PATCH 08/11] Services shut down when their own service deps fail --- src/execution/service.ts | 127 ++++++++++++++++++++++++++++----------- src/test/service.test.ts | 68 +++++++++++++++++++++ 2 files changed, 159 insertions(+), 36 deletions(-) diff --git a/src/execution/service.ts b/src/execution/service.ts index fe21aa45f..93436962f 100644 --- a/src/execution/service.ts +++ b/src/execution/service.ts @@ -42,6 +42,10 @@ type ServiceState = } | {id: 'stopping'} | {id: 'stopped'} + | { + id: 'failing'; + failure: Failure; + } | { id: 'failed'; failure: Failure; @@ -84,11 +88,11 @@ function unexpectedState(state: ServiceState) { * ├─◄─ abort ─┤ FINGERPRINTING │ │ * │ └───────┬────────┘ │ * │ │ │ - * ▼ fingerprinted │ + * ▼ fingerprinted ▼ * │ │ │ * │ ┌─────▼─────┐ │ * ├─◄─ abort ───┤ UNSTARTED │ │ - * │ └─────┬─────┘ ▼ + * │ └─────┬─────┘ │ * │ │ │ * │ start ╭─╮ │ * │ │ │ start │ @@ -96,39 +100,42 @@ function unexpectedState(state: ServiceState) { * ├─◄─ abort ─┤ DEPS_STARTING ├───── depStartErr ───►───┤ * │ └───────┬───────┘ │ * │ │ │ - * │ depsStarted │ + * │ depsStarted ▼ * │ │ ╭─╮ │ * │ │ │ start │ * │ ┌────▼──▼─┴┐ │ * │ ╭◄─ abort ┤ STARTING ├──── startErr ──────►──────┤ * │ │ └────┬────┬┘ │ - * ▼ │ │ │ │ - * │ │ │ ▼ │ - * │ │ │ ╰─── depServiceExit ──►──╮ │ - * │ │ started │ │ - * │ ▼ │ ╭─╮ ▼ │ - * │ │ │ │ start │ │ - * │ │ ┌────▼─▼─┴┐ │ │ - * │ ├◄─ abort ┤ STARTED ├── exit ─────────────►──│───┤ - * │ │ └────┬─┬─┬┘ │ │ - * │ │ │ │ ╰─── detach ──╮ │ │ - * │ │ │ ▼ │ │ │ - * │ │ │ ╰───── depServiceExit ───►──┤ │ - * │ │ │ │ │ │ - * │ │ allConsumersDone │ │ │ - * │ ▼ (unless directly invoked) │ │ │ - * │ │ │ ▼ ▼ ▼ - * ▼ │ │ ╭─╮ │ │ │ - * │ │ │ │ start │ │ │ - * │ │ ┌────▼──▼─┴┐ │ │ │ - * │ ╰─────────► STOPPING ◄─────────────◄─────────╯ │ - * │ └┬─▲─┬─────┘ │ │ - * │ abort │ │ │ │ - * │ ╰─╯ │ │ │ - * │ exit │ │ - * │ │ ╭─╮ │ │ ╭─╮ - * │ │ │ start │ │ │ start - * │ ┌────▼─▼─┴┐ ┌────▼─────┐ ┌───▼─▼─┴┐ + * │ │ │ │ │ + * │ │ │ ╰─ depServiceExit ─►─╮ │ + * ▼ │ │ │ │ + * │ │ │ │ │ + * │ ▼ │ ▼ ▼ + * │ │ started │ │ + * │ │ │ ╭─╮ │ │ + * │ │ │ │ start │ │ + * │ │ ┌────▼─▼─┴┐ │ │ + * │ ├◄─ abort ┤ STARTED ├── exit ────────────────────┤ + * │ │ └────┬─┬─┬┘ │ │ + * │ │ │ │ │ │ │ + * │ │ │ │ ╰── depServiceExit ─►─┤ │ + * │ │ │ │ │ │ + * │ │ │ ╰───── detach ──╮ │ │ + * │ ▼ │ │ │ │ + * │ │ allConsumersDone │ │ │ + * │ │ (unless directly invoked) │ │ │ + * │ │ │ ▼ │ ▼ + * ▼ │ │ ╭─╮ │ │ │ + * │ │ │ │ start │ │ │ + * │ │ ┌────▼──▼─┴┐ │ ┌────▼────┐ │ + * │ ╰─────────► STOPPING │ │ │ FAILING │ │ + * │ └┬─▲─┬─────┘ │ └────┬────┘ │ + * │ abort │ │ │ │ │ + * │ ╰─╯ │ │ exit │ + * │ exit │ │ │ + * │ │ ╭─╮ │ ╰─────╮ │ ╭─╮ + * │ │ │ start │ │ │ │ start + * │ ┌────▼─▼─┴┐ ┌────▼─────┐ ┌─▼─▼─▼─┴┐ * ╰──────────────► STOPPED │ │ DETACHED │ │ FAILED │ * └┬─▲──────┘ └┬─▲───────┘ └┬─▲─────┘ * abort │ *all* │ abort │ @@ -187,7 +194,8 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand { this._onDepsStarted(); }); + void this._anyServiceTerminated.then(() => { + this._onDepServiceExit(); + }); return this._state.started.promise; } + case 'failing': case 'failed': { return Promise.resolve({ok: false, error: [this._state.failure]}); } @@ -358,7 +373,41 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand { + // consumer + // | + // v + // service1 + // | + // v + // service2 + + const consumer = await rig.newCommand(); + const service1 = await rig.newCommand(); + const service2 = await rig.newCommand(); + await rig.writeAtomic({ + 'package.json': { + scripts: { + consumer: 'wireit', + service1: 'wireit', + service2: 'wireit', + }, + wireit: { + consumer: { + command: consumer.command, + dependencies: ['service1'], + }, + service1: { + command: service1.command, + service: true, + dependencies: ['service2'], + }, + service2: { + command: service2.command, + service: true, + }, + }, + }, + }); + + const wireit = rig.exec('npm run consumer'); + + // Service2 starts + const service2Inv = await service2.nextInvocation(); + + // Service1 starts + const service1Inv = await service1.nextInvocation(); + + // Consumer starts + const consumerInv = await consumer.nextInvocation(); + + // Service 2 exits unexpectedly + service2Inv.exit(1); + await wireit.waitForLog(/\[service2\] Service exited unexpectedly/); + + // Consumer killed + await consumerInv.closed; + + // Service 1 shuts down + await service1Inv.closed; + + // Wireit exits with an error code + assert.equal((await wireit.exit).code, 1); + assert.equal(consumer.numInvocations, 1); + assert.equal(service1.numInvocations, 1); + assert.equal(service2.numInvocations, 1); + }) +); + test.run(); From bf3b962471416d064f0b596d800e3200968a101b Mon Sep 17 00:00:00 2001 From: Alexander Marks Date: Tue, 25 Oct 2022 16:57:58 -0700 Subject: [PATCH 09/11] Wait for services to shut down before ending an execution --- src/cli.ts | 3 ++- src/executor.ts | 20 ++++++++++++++++++++ src/watcher.ts | 3 ++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 2232e8454..c50304fe9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -94,13 +94,14 @@ const run = async (): Promise> => { return config; } const executor = new Executor( + config.value, logger, workerPool, cache, options.failureMode, abort ); - const result = await executor.getExecution(config.value).execute(); + const result = await executor.execute(); if (!result.ok) { return result; } diff --git a/src/executor.ts b/src/executor.ts index b800a2dcf..ea474943f 100644 --- a/src/executor.ts +++ b/src/executor.ts @@ -19,6 +19,8 @@ import type { ServiceScriptConfig, StandardScriptConfig, } from './config.js'; +import type {Result} from './error.js'; +import type {Failure} from './event.js'; type Execution = | NoCommandScriptExecution @@ -47,7 +49,9 @@ export type FailureMode = 'no-new' | 'continue' | 'kill'; * Executes a script that has been analyzed and validated by the Analyzer. */ export class Executor { + private readonly _rootConfig: ScriptConfig; private readonly _executions = new Map(); + private readonly _allServices: Array = []; private readonly _logger: Logger; private readonly _workerPool: WorkerPool; private readonly _cache?: Cache; @@ -61,12 +65,14 @@ export class Executor { private readonly _killRunningScripts = new Deferred(); constructor( + rootConfig: ScriptConfig, logger: Logger, workerPool: WorkerPool, cache: Cache | undefined, failureMode: FailureMode, abort: Deferred ) { + this._rootConfig = rootConfig; this._logger = logger; this._workerPool = workerPool; this._cache = cache; @@ -106,6 +112,19 @@ export class Executor { }); } + /** + * Execute the root script. + */ + async execute(): Promise> { + const result = await this.getExecution(this._rootConfig).execute(); + // Wait for services to shut down. + // TODO(aomarks) In watch mode, directly-invoked scripts (and the services + // they depend on) should not block here, since they should continue + // running. + await Promise.all(this._allServices.map((service) => service.terminated)); + return result; + } + /** * Signal that a script has failed, which will potentially stop starting or * kill other scripts depending on the {@link FailureMode}. @@ -148,6 +167,7 @@ export class Executor { this._logger, this._abort.promise ); + this._allServices.push(execution); } else { execution = new StandardScriptExecution( config, diff --git a/src/watcher.ts b/src/watcher.ts index af18a461b..7a1fe4cb6 100644 --- a/src/watcher.ts +++ b/src/watcher.ts @@ -276,13 +276,14 @@ export class Watcher { throw unexpectedState(this._state); } const executor = new Executor( + script, this._logger, this._workerPool, this._cache, this._failureMode, this._abort ); - const result = await executor.getExecution(script).execute(); + const result = await executor.execute(); if (!result.ok) { for (const error of result.error) { this._logger.log(error); From 13f353197600c12b1f7d19706bb1f3c3375c397f Mon Sep 17 00:00:00 2001 From: Alexander Marks Date: Tue, 25 Oct 2022 16:59:39 -0700 Subject: [PATCH 10/11] Use "on" prefix consistently for state machine events --- src/execution/service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/execution/service.ts b/src/execution/service.ts index 93436962f..9a3bcdaa4 100644 --- a/src/execution/service.ts +++ b/src/execution/service.ts @@ -436,7 +436,7 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand { - this._allConsumersDone(); + this._onAllConsumersDone(); }); return; } @@ -458,7 +458,7 @@ export class ServiceScriptExecution extends BaseExecutionWithCommand Date: Wed, 26 Oct 2022 08:56:52 -0700 Subject: [PATCH 11/11] Address feedback --- src/test/util/test-rig.ts | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/test/util/test-rig.ts b/src/test/util/test-rig.ts index d0ca4f915..5b4e7d931 100644 --- a/src/test/util/test-rig.ts +++ b/src/test/util/test-rig.ts @@ -320,8 +320,10 @@ class ExecResult { } } - private readonly _logMatchers: Array<{re: RegExp; deferred: Deferred}> = - []; + private readonly _logMatchers = new Set<{ + re: RegExp; + deferred: Deferred; + }>(); /** * Waits for the given content to be logged to either stdout or stderr. @@ -331,7 +333,7 @@ class ExecResult { */ waitForLog(matcher: RegExp): Promise { const deferred = new Deferred(); - this._logMatchers.push({re: matcher, deferred}); + this._logMatchers.add({re: matcher, deferred}); // In case we've already received the log we're watching for this._checkMatchersAgainstLogs(); return deferred.promise; @@ -340,13 +342,28 @@ class ExecResult { private _checkMatchersAgainstLogs() { let stdoutLastIndex = -1; let stderrLastIndex = -1; - for (const {re, deferred} of this._logMatchers) { - if (re.test(this._stdout)) { - deferred.resolve(); - stdoutLastIndex = Math.max(stdoutLastIndex, re.lastIndex); - } else if (re.test(this._stderr)) { - stderrLastIndex = Math.max(stderrLastIndex, re.lastIndex); + for (const matcher of this._logMatchers) { + const {re, deferred} = matcher; + // Use exec instead of match because otherwise if the user used the /g/ + // flag, we'll get an array and can't access the index. + const stdoutMatch = re.exec(this._stdout); + if (stdoutMatch !== null) { deferred.resolve(); + this._logMatchers.delete(matcher); + stdoutLastIndex = Math.max( + stdoutLastIndex, + stdoutMatch.index + stdoutMatch[0].length + ); + } else { + const stderrMatch = re.exec(this._stderr); + if (stderrMatch !== null) { + deferred.resolve(); + this._logMatchers.delete(matcher); + stderrLastIndex = Math.max( + stderrLastIndex, + stderrMatch.index + stderrMatch[0].length + ); + } } } if (stdoutLastIndex > 0) {