From c7d50c8e29a7d68853aa2fd9b16d4da3cd5bd5a4 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Thu, 21 Jan 2021 23:04:02 -0800 Subject: [PATCH] fix: handle error gracefully Fixes #5 --- .vscode/launch.json | 22 ++++++++++++++++++++++ src/index.ts | 4 +++- src/master.ts | 2 +- src/progress-reporter.ts | 2 ++ src/protocol.ts | 1 + src/worker-pool.ts | 30 ++++++++++++++++++++++-------- src/worker.ts | 20 ++++++++++++++------ tsconfig.json | 5 ++++- 8 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8767e5b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "pwa-node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}\\src\\index.ts", + "args": ["src/**/*.ts"], + "preLaunchTask": "tsc: build - tsconfig.json", + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ] + } + ] +} diff --git a/src/index.ts b/src/index.ts index 99fc676..02d6093 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,9 @@ import * as cluster from 'cluster'; import * as commander from 'commander'; import { cpus } from 'os'; import * as prettier from 'prettier'; -import { version } from '../package.json'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { version } = require('../package.json'); function startMaster() { const program = commander diff --git a/src/master.ts b/src/master.ts index b7b5036..c86d7db 100644 --- a/src/master.ts +++ b/src/master.ts @@ -39,7 +39,7 @@ export function spawnWorkers(options: IOptions) { () => { progress.complete(); - if (progress.reformatted && options.check) { + if ((progress.reformatted && options.check) || progress.failed) { process.exit(1); } else { process.exit(0); diff --git a/src/progress-reporter.ts b/src/progress-reporter.ts index 7d9df4b..c4053d7 100644 --- a/src/progress-reporter.ts +++ b/src/progress-reporter.ts @@ -12,6 +12,7 @@ import { IFormatResults } from './protocol'; export class ProgressReporter { public total = 0; public reformatted = 0; + public failed = 0; private spinner?: ora.Ora; constructor(quiet: boolean, private readonly check: boolean) { @@ -26,6 +27,7 @@ export class ProgressReporter { public update(results: IFormatResults) { this.total += results.files; this.reformatted += results.formatted.length; + this.failed += results.failed.length; if (results.formatted.length) { if (this.spinner) { diff --git a/src/protocol.ts b/src/protocol.ts index 0c1e16c..2a1e490 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -46,6 +46,7 @@ export type MasterMessage = IInitializationMessage | IFilesMessage; */ export interface IFormatResults { files: number; + failed: IDiscoveredFile[]; formatted: IDiscoveredFile[]; } diff --git a/src/worker-pool.ts b/src/worker-pool.ts index 4f306d1..c273c6d 100644 --- a/src/worker-pool.ts +++ b/src/worker-pool.ts @@ -3,8 +3,8 @@ *--------------------------------------------------------*/ import * as cluster from 'cluster'; -import { fromEvent, Observable } from 'rxjs'; -import { filter, map, take, tap } from 'rxjs/operators'; +import { BehaviorSubject, fromEvent, Observable } from 'rxjs'; +import { filter, map, switchMap, take, tap } from 'rxjs/operators'; import { IFormatResults, IInitializationMessage, @@ -14,11 +14,17 @@ import { WorkerMode, } from './protocol'; +export class WorkerExitedError extends Error { + constructor(codeOrSignal: number | string) { + super(`Worker exited with unexpected ${codeOrSignal} code`); + } +} + /** * Pool of workers. */ export class WorkerPool { - private readonly workers: Array<{ worker: cluster.Worker; active: number }> = []; + private readonly workers: Array<{ worker: Observable; active: number }> = []; private workIdCounter = 0; /** @@ -41,10 +47,13 @@ export class WorkerPool { const target = this.workers[0]; const id = this.workIdCounter++; target.active++; - target.worker.send({ type: MessageType.WorkerFiles, files, id }); this.sortWorkers(); - return fromEvent<[WorkerMessage]>(target.worker, 'message').pipe( + return target.worker.pipe( + switchMap((worker) => { + worker.send({ type: MessageType.WorkerFiles, files, id }); + return fromEvent<[WorkerMessage]>(worker, 'message'); + }), map(([m]) => m), filter((m) => m.id === id), take(1), @@ -60,9 +69,14 @@ export class WorkerPool { } private spawnWorker() { - const worker = { worker: cluster.fork(), active: 0 }; - this.workers.unshift(worker); - worker.worker.send({ + const worker = cluster.fork(); + const subject = new BehaviorSubject(worker); + this.workers.unshift({ worker: subject, active: 0 }); + + worker.on('exit', (code, signal) => subject.error(new WorkerExitedError(code ?? signal))); + worker.on('error', (err) => subject.error(err)); + + worker.send({ mode: this.options.check ? WorkerMode.Assert : this.options.write diff --git a/src/worker.ts b/src/worker.ts index 4f4a1d3..46e970e 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -6,7 +6,7 @@ import { readFile, writeFile } from 'fs'; import * as prettier from 'prettier'; import { combineLatest, Observable, of, Subject } from 'rxjs'; import { last, mergeMap } from 'rxjs/operators'; -import { promisify } from 'util'; +import { inspect, promisify } from 'util'; import { IFilesMessage, IFormattedMessage, @@ -32,6 +32,7 @@ function runFormatting( const output: IFormattedMessage = { files: files.files.length, formatted: [], + failed: [], id: files.id, type: MessageType.Formatted, }; @@ -39,10 +40,17 @@ function runFormatting( return of(...files.files).pipe( mergeMap(async (file) => { const contents = await readFileAsync(file.path, 'utf-8'); - const formatted = prettier.format(contents, { - ...(await prettier.resolveConfig(file.path)), - filepath: file.path, - }); + let formatted: string; + try { + formatted = prettier.format(contents, { + ...(await prettier.resolveConfig(file.path)), + filepath: file.path, + }); + } catch (e) { + process.stderr.write('\r\n' + inspect(e) + '\r\n'); + output.failed.push(file); + return output; + } if (formatted === contents) { return output; @@ -76,7 +84,7 @@ export function startWorker() { } }); - combineLatest(settings, files) + combineLatest([settings, files]) .pipe(mergeMap(([s, f]) => runFormatting(s, f))) .subscribe( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/tsconfig.json b/tsconfig.json index 61c191b..409dd01 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,5 +14,8 @@ "lib": ["es6", "es7"], "outDir": "dist", "types": ["node"] - } + }, + "include": [ + "src" + ] }