From 4b76d9a29269fb92c991b6d30adfacf96e104f00 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Mon, 22 May 2023 20:44:56 +0200 Subject: [PATCH 01/27] style: add newlines between methods in GeneratorProcess --- packages/generator-helper/src/GeneratorProcess.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 5b4ee792bdb5..074eb59779c4 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -22,6 +22,7 @@ type GeneratorProcessOptions = { export class GeneratorError extends Error { public code: number public data?: any + constructor(message: string, code: number, data?: any) { super(message) this.code = code @@ -43,16 +44,19 @@ export class GeneratorProcess { resolve: (result: any) => void reject: (error: Error) => void } + constructor(private executablePath: string, { isNode = false, initWaitTime = 200 }: GeneratorProcessOptions = {}) { this.isNode = isNode this.initWaitTime = initWaitTime } + async init(): Promise { if (!this.initPromise) { this.initPromise = this.initSingleton() } return this.initPromise } + initSingleton(): Promise { return new Promise((resolve, reject) => { try { @@ -131,6 +135,7 @@ export class GeneratorProcess { } }) } + private handleResponse(data: any): void { if (data.jsonrpc && data.id) { if (typeof data.id !== 'number') { @@ -147,20 +152,25 @@ export class GeneratorProcess { } } } + private registerListener(messageId: number, cb: (result: any, err?: Error) => void): void { this.listeners[messageId] = cb } + private sendMessage(message: JsonRPC.Request): void { this.child!.stdin.write(JSON.stringify(message) + '\n') } + private getMessageId(): number { return globalMessageId++ } + stop(): void { if (!this.child!.killed) { this.child!.kill() } } + getManifest(config: GeneratorConfig): Promise { return new Promise((resolve, reject) => { const messageId = this.getMessageId() @@ -184,6 +194,7 @@ export class GeneratorProcess { }) }) } + generate(options: GeneratorOptions): Promise { return new Promise((resolve, reject) => { const messageId = this.getMessageId() From 311c2357b054e3be7ee10cbe80364314131390b7 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Mon, 22 May 2023 22:24:54 +0200 Subject: [PATCH 02/27] fix(generator-helper): continuously handle generator process errors Handle generator process errors during the lifetime of the child process and not just during the first 200 ms. This change completely removes `initWaitTime`, which means that `prisma generate` command will now be faster. --- .../generator-helper/src/GeneratorProcess.ts | 150 ++++++++---------- .../src/__tests__/failing-after-1s-executable | 5 + .../__tests__/failing-after-1s-executable.cmd | 2 + .../src/__tests__/generatorHandler.test.ts | 21 ++- 4 files changed, 83 insertions(+), 95 deletions(-) create mode 100755 packages/generator-helper/src/__tests__/failing-after-1s-executable create mode 100644 packages/generator-helper/src/__tests__/failing-after-1s-executable.cmd diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 074eb59779c4..b276ee579164 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -3,6 +3,7 @@ import type { ChildProcessByStdio } from 'child_process' import { fork } from 'child_process' import { spawn } from 'cross-spawn' import { bold } from 'kleur/colors' +import { Readable, Writable } from 'stream' import byline from './byline' import type { GeneratorConfig, GeneratorManifest, GeneratorOptions, JsonRPC } from './types' @@ -20,13 +21,10 @@ type GeneratorProcessOptions = { } export class GeneratorError extends Error { - public code: number - public data?: any + name = 'GeneratorError' - constructor(message: string, code: number, data?: any) { + constructor(message: string, public code?: number, public data?: any) { super(message) - this.code = code - this.data = data if (data?.stack) { this.stack = data.stack } @@ -34,20 +32,14 @@ export class GeneratorError extends Error { } export class GeneratorProcess { - child?: ChildProcessByStdio + child?: ChildProcessByStdio listeners: { [key: string]: (result: any, err?: Error) => void } = {} - private stderrLogs = '' private initPromise?: Promise private isNode: boolean - private initWaitTime: number - private currentGenerateDeferred?: { - resolve: (result: any) => void - reject: (error: Error) => void - } + private errorLogs = '' - constructor(private executablePath: string, { isNode = false, initWaitTime = 200 }: GeneratorProcessOptions = {}) { + constructor(private pathOrCommand: string, { isNode = false }: GeneratorProcessOptions = {}) { this.isNode = isNode - this.initWaitTime = initWaitTime } async init(): Promise { @@ -59,80 +51,68 @@ export class GeneratorProcess { initSingleton(): Promise { return new Promise((resolve, reject) => { - try { - if (this.isNode) { - this.child = fork(this.executablePath, [], { - stdio: ['pipe', 'inherit', 'pipe', 'ipc'], - env: { - ...process.env, - PRISMA_GENERATOR_INVOCATION: 'true', - }, - execArgv: ['--max-old-space-size=8096'], - }) - } else { - this.child = spawn(this.executablePath, { - stdio: ['pipe', 'inherit', 'pipe'], - env: { - ...process.env, - PRISMA_GENERATOR_INVOCATION: 'true', - }, - shell: true, - }) - } - - this.child.on('exit', (code) => { - if (code && code > 0) { - if (this.currentGenerateDeferred) { - // print last 5 lines of stderr - this.currentGenerateDeferred.reject(new Error(this.stderrLogs.split('\n').slice(-5).join('\n'))) - } else { - reject(new Error(`Generator at ${this.executablePath} could not start:\n\n${this.stderrLogs}`)) - } - } + if (this.isNode) { + this.child = fork(this.pathOrCommand, [], { + stdio: ['pipe', 'inherit', 'pipe', 'ipc'], + env: { + ...process.env, + PRISMA_GENERATOR_INVOCATION: 'true', + }, + execArgv: ['--max-old-space-size=8096'], + }) as ChildProcessByStdio + } else { + this.child = spawn(this.pathOrCommand, { + stdio: ['pipe', 'inherit', 'pipe'], + env: { + ...process.env, + PRISMA_GENERATOR_INVOCATION: 'true', + }, + shell: true, }) + } - this.child.on('error', (err) => { - if (err.message.includes('EACCES')) { - reject( - new Error( - `The executable at ${this.executablePath} lacks the right chmod. Please use ${bold( - `chmod +x ${this.executablePath}`, - )}`, - ), - ) - } else { - reject(err) + this.child.on('exit', (code) => { + debug(`child exited with code ${code}`) + if (code) { + const error = new GeneratorError( + `Generator ${JSON.stringify(this.pathOrCommand)} failed:\n\n${this.errorLogs}`, + ) + for (const listener of Object.values(this.listeners)) { + listener(null, error) } - }) + } + }) - byline(this.child.stderr).on('data', (line) => { - const response = String(line) - this.stderrLogs += response + '\n' - let data - try { - data = JSON.parse(response) - } catch (e) { - debug(response) - } - if (data) { - this.handleResponse(data) - } - }) + this.child.on('error', (err) => { + if (err.message.includes('EACCES')) { + debug(err) + reject( + new Error( + `The executable at ${this.pathOrCommand} lacks the right permissions. Please use ${bold( + `chmod +x ${this.pathOrCommand}`, + )}`, + ), + ) + } else { + reject(err) + } + }) - this.child.on('spawn', () => { - // Wait initWaitTime for the binary to report an error and exit with non-zero exit code before considering it - // successfully started. - // TODO: this is not a reliable way to detect a startup error as the initialization could take longer than - // initWaitTime (200 ms by default), and this also hurts the generation performance since it always waits even - // if the generator succesfully initialized in less than initWaitTime. The proper solution would be to make - // the generator explicitly send a notification when it is ready, and we should wait until we get that - // notification. Requiring that would be a breaking change, however we could start by introducing an optional - // notification that would stop the waiting timer as a performance optimization. - setTimeout(resolve, this.initWaitTime) - }) - } catch (e) { - reject(e) - } + byline(this.child.stderr).on('data', (line: Buffer) => { + const response = String(line) + let data: string | undefined + try { + data = JSON.parse(response) + } catch (e) { + this.errorLogs += response + '\n' + debug(response) + } + if (data) { + this.handleResponse(data) + } + }) + + this.child.on('spawn', resolve) }) } @@ -199,16 +179,12 @@ export class GeneratorProcess { return new Promise((resolve, reject) => { const messageId = this.getMessageId() - this.currentGenerateDeferred = { resolve, reject } - this.registerListener(messageId, (result, error) => { if (error) { reject(error) - this.currentGenerateDeferred = undefined return } resolve(result) - this.currentGenerateDeferred = undefined }) this.sendMessage({ diff --git a/packages/generator-helper/src/__tests__/failing-after-1s-executable b/packages/generator-helper/src/__tests__/failing-after-1s-executable new file mode 100755 index 000000000000..962cef859661 --- /dev/null +++ b/packages/generator-helper/src/__tests__/failing-after-1s-executable @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +setTimeout(() => { + throw new Error('test') +}, 1000) diff --git a/packages/generator-helper/src/__tests__/failing-after-1s-executable.cmd b/packages/generator-helper/src/__tests__/failing-after-1s-executable.cmd new file mode 100644 index 000000000000..bdca79f87349 --- /dev/null +++ b/packages/generator-helper/src/__tests__/failing-after-1s-executable.cmd @@ -0,0 +1,2 @@ +@ECHO off +node "%~dp0\immediately-failing-executable" %* diff --git a/packages/generator-helper/src/__tests__/generatorHandler.test.ts b/packages/generator-helper/src/__tests__/generatorHandler.test.ts index efc6260d37bf..116c13a002ec 100644 --- a/packages/generator-helper/src/__tests__/generatorHandler.test.ts +++ b/packages/generator-helper/src/__tests__/generatorHandler.test.ts @@ -77,8 +77,9 @@ describe('generatorHandler', () => { testIf(process.platform !== 'win32')( 'parsing error', async () => { - const generator = new GeneratorProcess(getExecutable('invalid-executable'), { initWaitTime: 5000 }) - await expect(() => generator.init()).rejects.toThrow('Cannot find module') + const generator = new GeneratorProcess(getExecutable('invalid-executable')) + await generator.init() + await expect(() => generator.getManifest(stubOptions.generator)).rejects.toThrow('Cannot find module') }, 10_000, ) @@ -118,12 +119,16 @@ describe('generatorHandler', () => { generator.stop() }) + test('failing-after-1s-executable', async () => { + const generator = new GeneratorProcess(getExecutable('failing-after-1s-executable')) + await generator.init() + await expect(generator.getManifest(stubOptions.generator)).rejects.toThrow('test') + generator.stop() + }) + test('nonexistent executable', async () => { - const generator = new GeneratorProcess(getExecutable('this-executable-does-not-exist'), { - // Make initWaitTime longer than the default because it sometimes takes longer than 200 ms for the shell to parse - // the command on macOS CI under load. - initWaitTime: 2000, - }) - await expect(() => generator.init()).rejects.toThrow() + const generator = new GeneratorProcess(getExecutable('this-executable-does-not-exist')) + await generator.init() + await expect(() => generator.getManifest(stubOptions.generator)).rejects.toThrow() }) }) From 6bee07cdffd34046b41ef3df70bb2131a8b815d9 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Mon, 22 May 2023 22:28:00 +0200 Subject: [PATCH 03/27] fix(cli): don't ignore unhandled exceptions and promise rejections --- packages/cli/src/bin.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 25d698b3e4ed..611393a517dc 100755 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -54,9 +54,11 @@ process.removeAllListeners('warning') const debug = Debug('prisma:cli') process.on('uncaughtException', (e) => { debug(e) + throw e }) process.on('unhandledRejection', (e) => { debug(e) + throw e }) // Listen to Ctr + C and exit process.once('SIGINT', () => { From 3d55765f8b8a9a193e6cb7595267d1da4bbf9341 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 15:12:24 +0200 Subject: [PATCH 04/27] Safer `stop()` method --- packages/generator-helper/src/GeneratorProcess.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index b276ee579164..f7f88faff9d1 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -146,8 +146,8 @@ export class GeneratorProcess { } stop(): void { - if (!this.child!.killed) { - this.child!.kill() + if (!this.child?.killed) { + this.child?.kill() } } From 6260c478070bb4dbe77168ce7b686b9f159c5ede Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 15:13:54 +0200 Subject: [PATCH 05/27] Make listeners and child private fields --- packages/generator-helper/src/GeneratorProcess.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index f7f88faff9d1..16b79649e30b 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -32,8 +32,8 @@ export class GeneratorError extends Error { } export class GeneratorProcess { - child?: ChildProcessByStdio - listeners: { [key: string]: (result: any, err?: Error) => void } = {} + private child?: ChildProcessByStdio + private listeners: { [key: string]: (result: any, err?: Error) => void } = {} private initPromise?: Promise private isNode: boolean private errorLogs = '' From e4602e820b66e5cfb0879e6ab7b07e1a2c496650 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 15:43:30 +0200 Subject: [PATCH 06/27] Don't send data if child not started or stdin not writable --- packages/generator-helper/src/GeneratorProcess.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 16b79649e30b..c4d776aaae02 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -138,7 +138,13 @@ export class GeneratorProcess { } private sendMessage(message: JsonRPC.Request): void { - this.child!.stdin.write(JSON.stringify(message) + '\n') + if (!this.child) { + throw new GeneratorError('Generator process has not started yet') + } + if (!this.child.stdin.writable) { + throw new GeneratorError('Cannot send data to the generator process, process already exited') + } + this.child.stdin.write(JSON.stringify(message) + '\n') } private getMessageId(): number { From f7e8a3d9dff0bc7228d2c89ee7bffe714c8e1869 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 15:45:33 +0200 Subject: [PATCH 07/27] Reject future calls if an error happened before --- .../generator-helper/src/GeneratorProcess.ts | 68 ++++++++----------- packages/internals/src/Generator.ts | 2 +- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index c4d776aaae02..58c6cf0d4dcd 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -37,6 +37,7 @@ export class GeneratorProcess { private initPromise?: Promise private isNode: boolean private errorLogs = '' + private pendingError: Error | undefined constructor(private pathOrCommand: string, { isNode = false }: GeneratorProcessOptions = {}) { this.isNode = isNode @@ -80,10 +81,12 @@ export class GeneratorProcess { for (const listener of Object.values(this.listeners)) { listener(null, error) } + this.pendingError = error } }) this.child.on('error', (err) => { + this.pendingError = err if (err.message.includes('EACCES')) { debug(err) reject( @@ -157,48 +160,37 @@ export class GeneratorProcess { } } - getManifest(config: GeneratorConfig): Promise { - return new Promise((resolve, reject) => { - const messageId = this.getMessageId() - - this.registerListener(messageId, (result, error) => { - if (error) { - return reject(error) - } - if (result.manifest) { - resolve(result.manifest) - } else { - resolve(null) + private rpcMethod(method: string, mapResult: (x: unknown) => U = (x) => x as U): (arg: T) => Promise { + return (params: T): Promise => + new Promise((resolve, reject) => { + if (this.pendingError) { + reject(this.pendingError) + return } - }) - - this.sendMessage({ - jsonrpc: '2.0', - method: 'getManifest', - params: config, - id: messageId, - }) - }) - } - generate(options: GeneratorOptions): Promise { - return new Promise((resolve, reject) => { - const messageId = this.getMessageId() + const messageId = this.getMessageId() - this.registerListener(messageId, (result, error) => { - if (error) { - reject(error) - return - } - resolve(result) - }) + this.registerListener(messageId, (result, error) => { + if (error) { + reject(error) + } else { + resolve(mapResult(result)) + } + }) - this.sendMessage({ - jsonrpc: '2.0', - method: 'generate', - params: options, - id: messageId, + this.sendMessage({ + jsonrpc: '2.0', + method, + params, + id: messageId, + }) }) - }) } + + getManifest = this.rpcMethod( + 'getManifest', + (result) => (result as { manifest?: GeneratorManifest | null }).manifest ?? null, + ) + + generate = this.rpcMethod('generate') } diff --git a/packages/internals/src/Generator.ts b/packages/internals/src/Generator.ts index 24c2298ed9a6..4b9d93833b46 100644 --- a/packages/internals/src/Generator.ts +++ b/packages/internals/src/Generator.ts @@ -19,7 +19,7 @@ export class Generator { stop(): void { this.generatorProcess.stop() } - generate(): Promise { + generate(): Promise { if (!this.options) { throw new Error(`Please first run .setOptions() on the Generator to initialize the options`) } From c18b8a14a800bba7b36c6851ea72084f43ada68a Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 19:27:34 +0200 Subject: [PATCH 08/27] Add additional logging --- packages/generator-helper/src/GeneratorProcess.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 58c6cf0d4dcd..a6c2663db133 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -72,8 +72,8 @@ export class GeneratorProcess { }) } - this.child.on('exit', (code) => { - debug(`child exited with code ${code}`) + this.child.on('exit', (code, signal) => { + debug(`child exited with code ${code} on signal ${signal}`) if (code) { const error = new GeneratorError( `Generator ${JSON.stringify(this.pathOrCommand)} failed:\n\n${this.errorLogs}`, From de08a58c88b76c4bbb1c589aaf3f91cfdb796eb4 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 19:54:22 +0200 Subject: [PATCH 09/27] Add temporary log --- packages/generator-helper/src/GeneratorProcess.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index a6c2663db133..929c7a521244 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -147,7 +147,12 @@ export class GeneratorProcess { if (!this.child.stdin.writable) { throw new GeneratorError('Cannot send data to the generator process, process already exited') } - this.child.stdin.write(JSON.stringify(message) + '\n') + + try { + this.child.stdin.write(JSON.stringify(message) + '\n') + } catch (err) { + console.error(err.code) + } } private getMessageId(): number { From 120a3ae4ceca477bd737b1b85fe4dbe16aeae785 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 20:17:55 +0200 Subject: [PATCH 10/27] Handle EPIPE error --- .../generator-helper/src/GeneratorProcess.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 929c7a521244..1987bfc08c5b 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -4,6 +4,7 @@ import { fork } from 'child_process' import { spawn } from 'cross-spawn' import { bold } from 'kleur/colors' import { Readable, Writable } from 'stream' +import timers from 'timers/promises' import byline from './byline' import type { GeneratorConfig, GeneratorManifest, GeneratorOptions, JsonRPC } from './types' @@ -78,10 +79,10 @@ export class GeneratorProcess { const error = new GeneratorError( `Generator ${JSON.stringify(this.pathOrCommand)} failed:\n\n${this.errorLogs}`, ) + this.pendingError = error for (const listener of Object.values(this.listeners)) { listener(null, error) } - this.pendingError = error } }) @@ -140,10 +141,11 @@ export class GeneratorProcess { this.listeners[messageId] = cb } - private sendMessage(message: JsonRPC.Request): void { + private async sendMessage(message: JsonRPC.Request): Promise { if (!this.child) { throw new GeneratorError('Generator process has not started yet') } + if (!this.child.stdin.writable) { throw new GeneratorError('Cannot send data to the generator process, process already exited') } @@ -151,7 +153,20 @@ export class GeneratorProcess { try { this.child.stdin.write(JSON.stringify(message) + '\n') } catch (err) { - console.error(err.code) + console.log(err.code) + if ((err as NodeJS.ErrnoException).code === 'EPIPE') { + // Child process already terminated but we didn't know about it yet on Node.js side, so the `exit` even hasn't + // been emitted yet, and the `child.stdin.writable` check also passed. Wait one even loop tick, and re-throw the + // error if it exists. + await timers.setImmediate() + if (this.pendingError) { + throw this.pendingError + } else { + throw new GeneratorError('Cannot send data to the generator process, process already exited') + } + } else { + throw err + } } } @@ -188,7 +203,7 @@ export class GeneratorProcess { method, params, id: messageId, - }) + }).catch(reject) }) } From eaf94c0873ab5a1dd2a9a55bcc8e74f5af396a47 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 20:32:51 +0200 Subject: [PATCH 11/27] Use write operation callback --- .../generator-helper/src/GeneratorProcess.ts | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 1987bfc08c5b..efcb16957f99 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -4,7 +4,6 @@ import { fork } from 'child_process' import { spawn } from 'cross-spawn' import { bold } from 'kleur/colors' import { Readable, Writable } from 'stream' -import timers from 'timers/promises' import byline from './byline' import type { GeneratorConfig, GeneratorManifest, GeneratorOptions, JsonRPC } from './types' @@ -141,33 +140,38 @@ export class GeneratorProcess { this.listeners[messageId] = cb } - private async sendMessage(message: JsonRPC.Request): Promise { + private sendMessage(message: JsonRPC.Request, callback: (error?: Error) => void): void { if (!this.child) { - throw new GeneratorError('Generator process has not started yet') + callback(new GeneratorError('Generator process has not started yet')) + return } if (!this.child.stdin.writable) { - throw new GeneratorError('Cannot send data to the generator process, process already exited') + callback(new GeneratorError('Cannot send data to the generator process, process already exited')) + return } - try { - this.child.stdin.write(JSON.stringify(message) + '\n') - } catch (err) { - console.log(err.code) - if ((err as NodeJS.ErrnoException).code === 'EPIPE') { + this.child.stdin.write(JSON.stringify(message) + '\n', (error) => { + if (!error) { + return callback() + } + + console.log((error as NodeJS.ErrnoException).code) + if ((error as NodeJS.ErrnoException).code === 'EPIPE') { // Child process already terminated but we didn't know about it yet on Node.js side, so the `exit` even hasn't // been emitted yet, and the `child.stdin.writable` check also passed. Wait one even loop tick, and re-throw the // error if it exists. - await timers.setImmediate() - if (this.pendingError) { - throw this.pendingError - } else { - throw new GeneratorError('Cannot send data to the generator process, process already exited') - } - } else { - throw err + setImmediate(() => { + if (this.pendingError) { + callback(this.pendingError) + } else { + callback(new GeneratorError('Cannot send data to the generator process, process already exited')) + } + }) } - } + + callback(error) + }) } private getMessageId(): number { @@ -198,12 +202,17 @@ export class GeneratorProcess { } }) - this.sendMessage({ - jsonrpc: '2.0', - method, - params, - id: messageId, - }).catch(reject) + this.sendMessage( + { + jsonrpc: '2.0', + method, + params, + id: messageId, + }, + (error) => { + if (error) reject(error) + }, + ) }) } From 2ac5c88431c364c6c9be92bf2c8a8e893cee41b7 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 20:37:40 +0200 Subject: [PATCH 12/27] Add missing return --- packages/generator-helper/src/GeneratorProcess.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index efcb16957f99..14febfe42ffd 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -168,6 +168,7 @@ export class GeneratorProcess { callback(new GeneratorError('Cannot send data to the generator process, process already exited')) } }) + return } callback(error) From 96d3739e42fb0b95e5563a7e00b19199a7bfcc56 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 20:37:58 +0200 Subject: [PATCH 13/27] Remove log --- packages/generator-helper/src/GeneratorProcess.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 14febfe42ffd..30fc6b6a383d 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -156,7 +156,6 @@ export class GeneratorProcess { return callback() } - console.log((error as NodeJS.ErrnoException).code) if ((error as NodeJS.ErrnoException).code === 'EPIPE') { // Child process already terminated but we didn't know about it yet on Node.js side, so the `exit` even hasn't // been emitted yet, and the `child.stdin.writable` check also passed. Wait one even loop tick, and re-throw the From 963446170a303bce3ce227d7dd52b7974a1c165d Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 20:54:18 +0200 Subject: [PATCH 14/27] Set up an error event handler --- .../generator-helper/src/GeneratorProcess.ts | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 30fc6b6a383d..ab658f58e6d0 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -85,6 +85,15 @@ export class GeneratorProcess { } }) + this.child.stdin.on('error', (error) => { + if ((error as NodeJS.ErrnoException).code !== 'EPIPE') { + this.pendingError = error + for (const listener of Object.values(this.listeners)) { + listener(null, error) + } + } + }) + this.child.on('error', (err) => { this.pendingError = err if (err.message.includes('EACCES')) { @@ -151,27 +160,29 @@ export class GeneratorProcess { return } - this.child.stdin.write(JSON.stringify(message) + '\n', (error) => { - if (!error) { - return callback() - } - - if ((error as NodeJS.ErrnoException).code === 'EPIPE') { - // Child process already terminated but we didn't know about it yet on Node.js side, so the `exit` even hasn't - // been emitted yet, and the `child.stdin.writable` check also passed. Wait one even loop tick, and re-throw the - // error if it exists. - setImmediate(() => { - if (this.pendingError) { - callback(this.pendingError) - } else { - callback(new GeneratorError('Cannot send data to the generator process, process already exited')) - } - }) - return - } - - callback(error) - }) + this.child.stdin.write(JSON.stringify(message) + '\n') + callback() + // this.child.stdin.write(JSON.stringify(message) + '\n', (error) => { + // if (!error) { + // return callback() + // } + + // if ((error as NodeJS.ErrnoException).code === 'EPIPE') { + // // Child process already terminated but we didn't know about it yet on Node.js side, so the `exit` even hasn't + // // been emitted yet, and the `child.stdin.writable` check also passed. Wait one even loop tick, and re-throw the + // // error if it exists. + // setImmediate(() => { + // if (this.pendingError) { + // callback(this.pendingError) + // } else { + // callback(new GeneratorError('Cannot send data to the generator process, process already exited')) + // } + // }) + // return + // } + + // callback(error) + // }) } private getMessageId(): number { From 5d812da9bfe3ed2abb3618fc03f6b3ca72326fc9 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 21:11:19 +0200 Subject: [PATCH 15/27] Simplify logic --- .../generator-helper/src/GeneratorProcess.ts | 48 +++++++------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index ab658f58e6d0..063b0ebd58f4 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -85,14 +85,9 @@ export class GeneratorProcess { } }) - this.child.stdin.on('error', (error) => { - if ((error as NodeJS.ErrnoException).code !== 'EPIPE') { - this.pendingError = error - for (const listener of Object.values(this.listeners)) { - listener(null, error) - } - } - }) + // Set the error handler for stdin to prevent unhandled error events. + // We handle write errors explicitly in `sendMessage` method. + this.child.stdin.on('error', () => {}) this.child.on('error', (err) => { this.pendingError = err @@ -160,29 +155,20 @@ export class GeneratorProcess { return } - this.child.stdin.write(JSON.stringify(message) + '\n') - callback() - // this.child.stdin.write(JSON.stringify(message) + '\n', (error) => { - // if (!error) { - // return callback() - // } - - // if ((error as NodeJS.ErrnoException).code === 'EPIPE') { - // // Child process already terminated but we didn't know about it yet on Node.js side, so the `exit` even hasn't - // // been emitted yet, and the `child.stdin.writable` check also passed. Wait one even loop tick, and re-throw the - // // error if it exists. - // setImmediate(() => { - // if (this.pendingError) { - // callback(this.pendingError) - // } else { - // callback(new GeneratorError('Cannot send data to the generator process, process already exited')) - // } - // }) - // return - // } - - // callback(error) - // }) + this.child.stdin.write(JSON.stringify(message) + '\n', (error) => { + if (!error) { + return callback() + } + + if ((error as NodeJS.ErrnoException).code === 'EPIPE') { + // Child process already terminated but we didn't know about it yet on Node.js side, so the `exit` event hasn't + // been emitted yet, and the `child.stdin.writable` check also passed. We skip this error and let the `exit` + // event handler reject active requests (including this one). + return callback() + } + + callback(error) + }) } private getMessageId(): number { From ca4f9102ab63ba0975020f20a8b86ed90ba99747 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 21:17:20 +0200 Subject: [PATCH 16/27] Remove unnecessary closure --- .../generator-helper/src/__tests__/generatorHandler.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/generator-helper/src/__tests__/generatorHandler.test.ts b/packages/generator-helper/src/__tests__/generatorHandler.test.ts index 116c13a002ec..e6a8d3da1ab5 100644 --- a/packages/generator-helper/src/__tests__/generatorHandler.test.ts +++ b/packages/generator-helper/src/__tests__/generatorHandler.test.ts @@ -129,6 +129,6 @@ describe('generatorHandler', () => { test('nonexistent executable', async () => { const generator = new GeneratorProcess(getExecutable('this-executable-does-not-exist')) await generator.init() - await expect(() => generator.getManifest(stubOptions.generator)).rejects.toThrow() + await expect(generator.getManifest(stubOptions.generator)).resolves.toThrow() }) }) From d4c10e46df5f26d89f31597358febab134d42894 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 21:22:47 +0200 Subject: [PATCH 17/27] Make handleResponse typed --- packages/generator-helper/src/GeneratorProcess.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 063b0ebd58f4..f4dba61628f2 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -107,7 +107,7 @@ export class GeneratorProcess { byline(this.child.stderr).on('data', (line: Buffer) => { const response = String(line) - let data: string | undefined + let data: JsonRPC.Response | undefined try { data = JSON.parse(response) } catch (e) { @@ -123,13 +123,13 @@ export class GeneratorProcess { }) } - private handleResponse(data: any): void { + private handleResponse(data: JsonRPC.Response): void { if (data.jsonrpc && data.id) { if (typeof data.id !== 'number') { throw new Error(`message.id has to be a number. Found value ${data.id}`) } if (this.listeners[data.id]) { - if (data.error) { + if (isErrorResponse(data)) { const error = new GeneratorError(data.error.message, data.error.code, data.error.data) this.listeners[data.id](null, error) } else { @@ -220,3 +220,7 @@ export class GeneratorProcess { generate = this.rpcMethod('generate') } + +function isErrorResponse(response: JsonRPC.Response): response is JsonRPC.ErrorResponse { + return (response as JsonRPC.ErrorResponse).error !== undefined +} From c6f81c8067b2f275c74059bd088ab6c159537727 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 21:25:16 +0200 Subject: [PATCH 18/27] Fix wrong path in .cmd file --- .../src/__tests__/failing-after-1s-executable.cmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/generator-helper/src/__tests__/failing-after-1s-executable.cmd b/packages/generator-helper/src/__tests__/failing-after-1s-executable.cmd index bdca79f87349..fdead15d4c48 100644 --- a/packages/generator-helper/src/__tests__/failing-after-1s-executable.cmd +++ b/packages/generator-helper/src/__tests__/failing-after-1s-executable.cmd @@ -1,2 +1,2 @@ @ECHO off -node "%~dp0\immediately-failing-executable" %* +node "%~dp0\failing-after-1s-executable" %* From 79f2ec9a20334d9f94a69b1ad7e60b097ed2992b Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 21:48:58 +0200 Subject: [PATCH 19/27] Fix typo --- .../generator-helper/src/__tests__/generatorHandler.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/generator-helper/src/__tests__/generatorHandler.test.ts b/packages/generator-helper/src/__tests__/generatorHandler.test.ts index e6a8d3da1ab5..5d57704cde26 100644 --- a/packages/generator-helper/src/__tests__/generatorHandler.test.ts +++ b/packages/generator-helper/src/__tests__/generatorHandler.test.ts @@ -129,6 +129,6 @@ describe('generatorHandler', () => { test('nonexistent executable', async () => { const generator = new GeneratorProcess(getExecutable('this-executable-does-not-exist')) await generator.init() - await expect(generator.getManifest(stubOptions.generator)).resolves.toThrow() + await expect(generator.getManifest(stubOptions.generator)).rejects.toThrow() }) }) From fe4962bf851de226aad639b88a6f92933f770dd0 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 21:56:52 +0200 Subject: [PATCH 20/27] Handle failed startup on Windows --- packages/generator-helper/src/GeneratorProcess.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index f4dba61628f2..6705289f5bc5 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -90,9 +90,11 @@ export class GeneratorProcess { this.child.stdin.on('error', () => {}) this.child.on('error', (err) => { + debug(err) this.pendingError = err - if (err.message.includes('EACCES')) { - debug(err) + + // Handle startup errors: reject the `init` promise. + if ((err as NodeJS.ErrnoException).code === 'EACCES') { reject( new Error( `The executable at ${this.pathOrCommand} lacks the right permissions. Please use ${bold( @@ -103,6 +105,12 @@ export class GeneratorProcess { } else { reject(err) } + + // Reject any pending requests if the error event happened after spawning. + const error = new GeneratorError(`${err.message}\n${this.errorLogs}`) + for (const listener of Object.values(this.listeners)) { + listener(null, error) + } }) byline(this.child.stderr).on('data', (line: Buffer) => { From 36001bad7d6d94a1cdcb5f3533f2f50d70687eb6 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 23 May 2023 21:59:45 +0200 Subject: [PATCH 21/27] Enable skipped tests on Windows --- .../src/__tests__/generatorHandler.test.ts | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/generator-helper/src/__tests__/generatorHandler.test.ts b/packages/generator-helper/src/__tests__/generatorHandler.test.ts index 5d57704cde26..e03c03652a41 100644 --- a/packages/generator-helper/src/__tests__/generatorHandler.test.ts +++ b/packages/generator-helper/src/__tests__/generatorHandler.test.ts @@ -3,8 +3,6 @@ import path from 'path' import { GeneratorProcess } from '../GeneratorProcess' import type { GeneratorOptions } from '../types' -const testIf = (condition: boolean) => (condition ? test : test.skip) - const stubOptions: GeneratorOptions = { datamodel: '', datasources: [], @@ -61,8 +59,7 @@ function getExecutable(name: string): string { } describe('generatorHandler', () => { - // TODO: Windows: this test fails with timeout. - testIf(process.platform !== 'win32')('exiting', async () => { + test('exiting', async () => { const generator = new GeneratorProcess(getExecutable('exiting-executable')) await generator.init() try { @@ -73,16 +70,11 @@ describe('generatorHandler', () => { } }) - // TODO: Windows: this test fails with ENOENT even though the .cmd file is there and can be run manually. - testIf(process.platform !== 'win32')( - 'parsing error', - async () => { - const generator = new GeneratorProcess(getExecutable('invalid-executable')) - await generator.init() - await expect(() => generator.getManifest(stubOptions.generator)).rejects.toThrow('Cannot find module') - }, - 10_000, - ) + test('parsing error', async () => { + const generator = new GeneratorProcess(getExecutable('invalid-executable')) + await generator.init() + await expect(() => generator.getManifest(stubOptions.generator)).rejects.toThrow('Cannot find module') + }, 10_000) test('minimal-executable', async () => { const generator = new GeneratorProcess(getExecutable('minimal-executable')) From 59808fcf165576703702d8260a328c8aa60694fd Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Wed, 24 May 2023 12:17:27 +0200 Subject: [PATCH 22/27] Remove listeners after rejecting them --- .../generator-helper/src/GeneratorProcess.ts | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 6705289f5bc5..4bbf712c276c 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -79,9 +79,7 @@ export class GeneratorProcess { `Generator ${JSON.stringify(this.pathOrCommand)} failed:\n\n${this.errorLogs}`, ) this.pendingError = error - for (const listener of Object.values(this.listeners)) { - listener(null, error) - } + this.rejectAllListeners(error) } }) @@ -89,12 +87,12 @@ export class GeneratorProcess { // We handle write errors explicitly in `sendMessage` method. this.child.stdin.on('error', () => {}) - this.child.on('error', (err) => { - debug(err) - this.pendingError = err + this.child.on('error', (error) => { + debug(error) + this.pendingError = error // Handle startup errors: reject the `init` promise. - if ((err as NodeJS.ErrnoException).code === 'EACCES') { + if ((error as NodeJS.ErrnoException).code === 'EACCES') { reject( new Error( `The executable at ${this.pathOrCommand} lacks the right permissions. Please use ${bold( @@ -103,14 +101,11 @@ export class GeneratorProcess { ), ) } else { - reject(err) + reject(error) } // Reject any pending requests if the error event happened after spawning. - const error = new GeneratorError(`${err.message}\n${this.errorLogs}`) - for (const listener of Object.values(this.listeners)) { - listener(null, error) - } + this.rejectAllListeners(error) }) byline(this.child.stderr).on('data', (line: Buffer) => { @@ -131,6 +126,13 @@ export class GeneratorProcess { }) } + private rejectAllListeners(error: Error) { + for (const id of Object.keys(this.listeners)) { + this.listeners[id](null, error) + delete this.listeners[id] + } + } + private handleResponse(data: JsonRPC.Response): void { if (data.jsonrpc && data.id) { if (typeof data.id !== 'number') { From 8bbb96ccab4effe10212ed607cb91333d8852292 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Wed, 24 May 2023 12:23:41 +0200 Subject: [PATCH 23/27] Refactor listeners and rename to handlers --- .../generator-helper/src/GeneratorProcess.ts | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 4bbf712c276c..21e3a32260d2 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -31,9 +31,14 @@ export class GeneratorError extends Error { } } +type ResultHandler = { + resolve: (value: T) => void + reject: (error: Error) => void +} + export class GeneratorProcess { private child?: ChildProcessByStdio - private listeners: { [key: string]: (result: any, err?: Error) => void } = {} + private handlers: Record = {} private initPromise?: Promise private isNode: boolean private errorLogs = '' @@ -79,7 +84,7 @@ export class GeneratorProcess { `Generator ${JSON.stringify(this.pathOrCommand)} failed:\n\n${this.errorLogs}`, ) this.pendingError = error - this.rejectAllListeners(error) + this.rejectAllHandlers(error) } }) @@ -105,7 +110,7 @@ export class GeneratorProcess { } // Reject any pending requests if the error event happened after spawning. - this.rejectAllListeners(error) + this.rejectAllHandlers(error) }) byline(this.child.stderr).on('data', (line: Buffer) => { @@ -126,10 +131,10 @@ export class GeneratorProcess { }) } - private rejectAllListeners(error: Error) { - for (const id of Object.keys(this.listeners)) { - this.listeners[id](null, error) - delete this.listeners[id] + private rejectAllHandlers(error: Error) { + for (const id of Object.keys(this.handlers)) { + this.handlers[id].reject(error) + delete this.handlers[id] } } @@ -138,22 +143,18 @@ export class GeneratorProcess { if (typeof data.id !== 'number') { throw new Error(`message.id has to be a number. Found value ${data.id}`) } - if (this.listeners[data.id]) { + if (this.handlers[data.id]) { if (isErrorResponse(data)) { const error = new GeneratorError(data.error.message, data.error.code, data.error.data) - this.listeners[data.id](null, error) + this.handlers[data.id].reject(error) } else { - this.listeners[data.id](data.result) + this.handlers[data.id].resolve(data.result) } - delete this.listeners[data.id] + delete this.handlers[data.id] } } } - private registerListener(messageId: number, cb: (result: any, err?: Error) => void): void { - this.listeners[messageId] = cb - } - private sendMessage(message: JsonRPC.Request, callback: (error?: Error) => void): void { if (!this.child) { callback(new GeneratorError('Generator process has not started yet')) @@ -201,13 +202,10 @@ export class GeneratorProcess { const messageId = this.getMessageId() - this.registerListener(messageId, (result, error) => { - if (error) { - reject(error) - } else { - resolve(mapResult(result)) - } - }) + this.handlers[messageId] = { + resolve: (result) => resolve(mapResult(result)), + reject, + } this.sendMessage( { From 5d9e1304bef51182a49e08adc73adcea8ed132ba Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Wed, 24 May 2023 12:26:08 +0200 Subject: [PATCH 24/27] Add a TODO comment --- packages/generator-helper/src/GeneratorProcess.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/generator-helper/src/GeneratorProcess.ts b/packages/generator-helper/src/GeneratorProcess.ts index 21e3a32260d2..5360534a1051 100644 --- a/packages/generator-helper/src/GeneratorProcess.ts +++ b/packages/generator-helper/src/GeneratorProcess.ts @@ -64,6 +64,7 @@ export class GeneratorProcess { ...process.env, PRISMA_GENERATOR_INVOCATION: 'true', }, + // TODO: this assumes the host has at least 8 GB of RAM which may not be the case. execArgv: ['--max-old-space-size=8096'], }) as ChildProcessByStdio } else { From 8356314d59e7647ab02e465a10986c9b04057610 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Wed, 24 May 2023 13:39:49 +0200 Subject: [PATCH 25/27] Revert "Enable skipped tests on Windows" This reverts commit 36001bad7d6d94a1cdcb5f3533f2f50d70687eb6. --- .../src/__tests__/generatorHandler.test.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/generator-helper/src/__tests__/generatorHandler.test.ts b/packages/generator-helper/src/__tests__/generatorHandler.test.ts index e03c03652a41..5d57704cde26 100644 --- a/packages/generator-helper/src/__tests__/generatorHandler.test.ts +++ b/packages/generator-helper/src/__tests__/generatorHandler.test.ts @@ -3,6 +3,8 @@ import path from 'path' import { GeneratorProcess } from '../GeneratorProcess' import type { GeneratorOptions } from '../types' +const testIf = (condition: boolean) => (condition ? test : test.skip) + const stubOptions: GeneratorOptions = { datamodel: '', datasources: [], @@ -59,7 +61,8 @@ function getExecutable(name: string): string { } describe('generatorHandler', () => { - test('exiting', async () => { + // TODO: Windows: this test fails with timeout. + testIf(process.platform !== 'win32')('exiting', async () => { const generator = new GeneratorProcess(getExecutable('exiting-executable')) await generator.init() try { @@ -70,11 +73,16 @@ describe('generatorHandler', () => { } }) - test('parsing error', async () => { - const generator = new GeneratorProcess(getExecutable('invalid-executable')) - await generator.init() - await expect(() => generator.getManifest(stubOptions.generator)).rejects.toThrow('Cannot find module') - }, 10_000) + // TODO: Windows: this test fails with ENOENT even though the .cmd file is there and can be run manually. + testIf(process.platform !== 'win32')( + 'parsing error', + async () => { + const generator = new GeneratorProcess(getExecutable('invalid-executable')) + await generator.init() + await expect(() => generator.getManifest(stubOptions.generator)).rejects.toThrow('Cannot find module') + }, + 10_000, + ) test('minimal-executable', async () => { const generator = new GeneratorProcess(getExecutable('minimal-executable')) From 3270c623851c4dce94982bee346a93f062f78cdd Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Wed, 24 May 2023 13:42:04 +0200 Subject: [PATCH 26/27] Update comments for skipped tests --- .../src/__tests__/generatorHandler.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/generator-helper/src/__tests__/generatorHandler.test.ts b/packages/generator-helper/src/__tests__/generatorHandler.test.ts index 5d57704cde26..b4c877637087 100644 --- a/packages/generator-helper/src/__tests__/generatorHandler.test.ts +++ b/packages/generator-helper/src/__tests__/generatorHandler.test.ts @@ -61,7 +61,9 @@ function getExecutable(name: string): string { } describe('generatorHandler', () => { - // TODO: Windows: this test fails with timeout. + // TODO: Windows: this test fails with ENOENT on CI because it can't file the .cmd file: + // spawn D:\\a\\prisma\\prisma\\packages\\generator-helper\\src\\__tests__\\exiting-executable.cmd ENOENT + // This does not happen on Windows locally. testIf(process.platform !== 'win32')('exiting', async () => { const generator = new GeneratorProcess(getExecutable('exiting-executable')) await generator.init() @@ -73,7 +75,9 @@ describe('generatorHandler', () => { } }) - // TODO: Windows: this test fails with ENOENT even though the .cmd file is there and can be run manually. + // TODO: Windows: this test fails with ENOENT on CI because it can't file the .cmd file: + // spawn D:\\a\\prisma\\prisma\\packages\\generator-helper\\src\\__tests__\\invalid-executable.cmd ENOENT + // This does not happen on Windows locally. testIf(process.platform !== 'win32')( 'parsing error', async () => { From d280fe7418ce45b20f0da83b9f7029df9bab6ed5 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Thu, 25 May 2023 15:58:44 +0200 Subject: [PATCH 27/27] Remove uncaughtException and unhandledRejection handlers --- packages/cli/src/bin.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 611393a517dc..d5ef86816623 100755 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -51,15 +51,6 @@ const commandArray = process.argv.slice(2) process.removeAllListeners('warning') -const debug = Debug('prisma:cli') -process.on('uncaughtException', (e) => { - debug(e) - throw e -}) -process.on('unhandledRejection', (e) => { - debug(e) - throw e -}) // Listen to Ctr + C and exit process.once('SIGINT', () => { process.exit(130)