| // Copyright 2024 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import { type ChildProcessWithoutNullStreams } from 'child_process'; |
| import JsonParser from 'jsonparse'; |
| |
| export class DataStreamProcess { |
| private outputChunks: Buffer[] = []; |
| |
| constructor( |
| private process: ChildProcessWithoutNullStreams, |
| onData: (data: Buffer) => void, |
| onError: (data: Buffer) => void, |
| ) { |
| this.process.stdout.on('data', (chunk: unknown) => { |
| const buffer = Buffer.isBuffer(chunk) ? |
| chunk : |
| Buffer.from(typeof chunk === 'string' ? chunk : String(chunk)); |
| this.outputChunks.push(buffer); |
| onData(buffer); |
| }); |
| this.process.stderr.on('data', (chunk: unknown) => { |
| const buffer = Buffer.isBuffer(chunk) ? |
| chunk : |
| Buffer.from(typeof chunk === 'string' ? chunk : String(chunk)); |
| this.outputChunks.push(buffer); |
| onError(buffer); |
| }); |
| } |
| |
| /** |
| * Returns the combined stdout and stderr that the process has written so far. |
| */ |
| public get output(): string { |
| return this.outputChunks.map(chunk => chunk.toString()).join(''); |
| } |
| |
| /** |
| * A future that resolves when the process exits. |
| */ |
| public get exitCode(): Promise<number | null> { |
| return new Promise((resolve) => { |
| this.process.on('exit', resolve); |
| }); |
| } |
| |
| /** |
| * Kills the underlying process and all its children. |
| */ |
| public stop() { |
| const pid = this.process?.pid; |
| if (pid) { |
| try { |
| // This handles the in-tree use case where `ffx` launches a subprocess `fx ffx`. |
| process.kill(-pid); |
| } catch { |
| // If the process with the given `pid` didn't exist, this error will rise, which we can |
| // just swallow. |
| } |
| } |
| } |
| } |
| |
| /** |
| * A stream of JSON Data coming from a process. |
| */ |
| export class JsonStreamProcess { |
| private process: DataStreamProcess | undefined; |
| private parser: JsonParser; |
| |
| constructor( |
| process: ChildProcessWithoutNullStreams | undefined, |
| private onData: (data: object) => void, |
| onError: (data: Buffer) => void, |
| ) { |
| this.parser = new JsonParser(); |
| if (process) { |
| this.process = new DataStreamProcess(process, (chunk) => { |
| this.parser.write(chunk); |
| }, onError); |
| } |
| this.parser.onValue = (value: unknown) => { |
| if (this.parser.stack.length === 0) { |
| this.onData(value as object); |
| } |
| }; |
| } |
| |
| /** |
| * Kills the underlying process and all its children. |
| */ |
| public stop() { |
| this.process?.stop(); |
| } |
| |
| /** |
| * Returns the raw combined stdout and stderr that the process has written so far. |
| */ |
| public get rawOutput(): string { |
| return this.process?.output ?? ''; |
| } |
| |
| /** |
| * A future that resolves when the process exits. |
| */ |
| public get exitCode(): Promise<number | null> { |
| return (async () => { |
| const exitCode = await this.process?.exitCode; |
| if (typeof exitCode === 'undefined') { |
| return null; |
| } |
| return exitCode; |
| })(); |
| } |
| } |