|
1 | | -import { PassThrough } from 'stream' |
| 1 | +import { Readable, PassThrough } from 'stream' |
2 | 2 | import { ChildProcessWithoutNullStreams } from 'child_process' |
3 | | -import { once } from 'events' |
4 | 3 | import mergeStream from 'merge-stream' |
5 | 4 | import splitStream from 'split' |
6 | 5 | import { NormalizedServiceConfig } from './validateAndNormalizeConfig' |
7 | 6 | import { spawnProcess } from './spawnProcess' |
8 | 7 |
|
9 | | -const split = () => splitStream((line: string) => `${line}\n`) |
10 | | - |
11 | 8 | export class ServiceProcess { |
12 | 9 | public readonly output = new PassThrough({ objectMode: true }) |
13 | 10 | public readonly started: Promise<void> |
14 | | - public isEnded = false |
15 | 11 | public logTail: string[] = [] |
16 | 12 | private readonly process: ChildProcessWithoutNullStreams |
| 13 | + private didError = false |
| 14 | + private didEnd = false |
17 | 15 | private readonly ended: Promise<void> |
18 | 16 | private wasEndCalled = false |
19 | 17 | constructor(config: NormalizedServiceConfig, onCrash: () => void) { |
20 | 18 | this.process = spawnProcess(config) |
21 | | - const childOutput = mergeStream( |
22 | | - this.process.stdout.setEncoding('utf8').pipe(split()), |
23 | | - this.process.stderr.setEncoding('utf8').pipe(split()) |
24 | | - ) |
25 | | - childOutput.pipe(this.output) |
26 | | - if (config.logTailLength > 0) { |
27 | | - this.output.on('data', line => { |
28 | | - this.logTail.push(line) |
29 | | - if (this.logTail.length > config.logTailLength) { |
30 | | - this.logTail.shift() |
31 | | - } |
32 | | - }) |
33 | | - } |
34 | | - const error = new Promise(resolve => this.process.on('error', resolve)) |
35 | 19 | this.started = Promise.race([ |
36 | | - error, |
37 | | - new Promise(resolve => setTimeout(resolve, 100)), |
| 20 | + new Promise(resolve => this.process.once('error', resolve)), |
| 21 | + new Promise(resolve => setTimeout(() => resolve(), 100)), |
38 | 22 | ]).then(error => { |
39 | | - if (!error) { |
40 | | - return |
| 23 | + if (error) { |
| 24 | + this.didError = true |
| 25 | + throw error |
41 | 26 | } |
42 | | - childOutput.unpipe(this.output) |
43 | | - this.output.end() |
44 | | - return Promise.reject(error) |
45 | 27 | }) |
46 | | - const didStart = this.started.then( |
47 | | - () => true, |
48 | | - () => false |
| 28 | + const processOutput = mergeStream( |
| 29 | + transformStream(this.process.stdout), |
| 30 | + transformStream(this.process.stderr) |
49 | 31 | ) |
50 | | - this.ended = Promise.race([error, once(childOutput, 'end')]).then(() => { |
51 | | - this.isEnded = true |
52 | | - if (!this.wasEndCalled) { |
53 | | - didStart.then(didStart => { |
54 | | - if (didStart) { |
55 | | - onCrash() |
| 32 | + this.ended = (async () => { |
| 33 | + for await (const line of processOutput as AsyncIterable<string>) { |
| 34 | + if (this.didError) { |
| 35 | + break |
| 36 | + } |
| 37 | + this.output.write(line) |
| 38 | + if (config.logTailLength > 0) { |
| 39 | + this.logTail.push(line) |
| 40 | + if (this.logTail.length > config.logTailLength) { |
| 41 | + this.logTail.shift() |
56 | 42 | } |
57 | | - }) |
| 43 | + } |
| 44 | + } |
| 45 | + this.didEnd = true |
| 46 | + this.output.end() |
| 47 | + })() |
| 48 | + Promise.all([this.started.catch(() => {}), this.ended]).then(() => { |
| 49 | + if (!this.didError && !this.wasEndCalled) { |
| 50 | + onCrash() |
58 | 51 | } |
59 | 52 | }) |
60 | 53 | } |
61 | | - end() { |
| 54 | + public isRunning() { |
| 55 | + return !this.didError && !this.didEnd |
| 56 | + } |
| 57 | + public end() { |
62 | 58 | if (!this.wasEndCalled) { |
63 | 59 | this.wasEndCalled = true |
64 | | - if (!this.isEnded) { |
| 60 | + if (this.isRunning()) { |
65 | 61 | this.process.kill('SIGINT') |
66 | 62 | } |
67 | 63 | } |
68 | 64 | return this.ended |
69 | 65 | } |
70 | 66 | } |
| 67 | + |
| 68 | +/** |
| 69 | + * Split input into stream of utf8 strings ending in '\n' |
| 70 | + * */ |
| 71 | +function transformStream(input: Readable): Readable { |
| 72 | + return input |
| 73 | + .setEncoding('utf8') |
| 74 | + .pipe(splitStream((line: string) => `${line}\n`)) |
| 75 | +} |
0 commit comments