From 49551eb9ebcb2a856f1e8c06d6a1abeab7ea72e1 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Fri, 1 Jan 2021 16:30:20 +0100 Subject: [PATCH] fix: Distinguish instantiation and initialization errors. --- bin/server.js | 5 +- src/init/CliRunner.ts | 139 +++++++++++++++++++------------ test/unit/init/CliRunner.test.ts | 39 ++++++--- 3 files changed, 115 insertions(+), 68 deletions(-) diff --git a/bin/server.js b/bin/server.js index dcfa789f7..0fed11b85 100755 --- a/bin/server.js +++ b/bin/server.js @@ -1,3 +1,4 @@ #!/usr/bin/env node -const { runCli } = require('..'); -runCli(); +// eslint-disable-next-line @typescript-eslint/naming-convention +const { CliRunner } = require('..'); +new CliRunner().run(); diff --git a/src/init/CliRunner.ts b/src/init/CliRunner.ts index bbda7786f..50a416fb3 100644 --- a/src/init/CliRunner.ts +++ b/src/init/CliRunner.ts @@ -1,68 +1,97 @@ +/* eslint-disable unicorn/no-process-exit */ + import * as path from 'path'; import type { ReadStream, WriteStream } from 'tty'; import type { LoaderProperties } from 'componentsjs'; import { Loader } from 'componentsjs'; import yargs from 'yargs'; +import { getLoggerFor } from '../logging/LogUtil'; import { ensureTrailingSlash } from '../util/PathUtil'; import type { Initializer } from './Initializer'; -/** - * Generic run function for starting the server from a given config - * @param args - Command line arguments. - * @param stderr - Standard error stream. - * @param properties - Components loader properties. - */ -export const runCli = function({ - argv = process.argv, - stderr = process.stderr, - properties = { - mainModulePath: path.join(__dirname, '../../'), - }, -}: { - argv?: string[]; - stdin?: ReadStream; - stdout?: WriteStream; - stderr?: WriteStream; - properties?: LoaderProperties; -} = {}): void { - const { argv: params } = yargs(argv.slice(2)) - .usage('node ./bin/server.js [args]') - .options({ - baseUrl: { type: 'string', alias: 'b' }, - config: { type: 'string', alias: 'c' }, - loggingLevel: { type: 'string', alias: 'l', default: 'info' }, - port: { type: 'number', alias: 'p', default: 3000 }, - rootFilePath: { type: 'string', alias: 'f' }, - sparqlEndpoint: { type: 'string', alias: 's' }, - podTemplateFolder: { type: 'string', alias: 't' }, - }) - .help(); +export class CliRunner { + private readonly logger = getLoggerFor(this); - (async(): Promise => { - // Load provided or default config file - const configPath = params.config ? + /** + * Generic run function for starting the server from a given config + * @param args - Command line arguments. + * @param stderr - Standard error stream. + * @param loaderProperties - Components loader properties. + */ + public run({ + argv = process.argv, + stderr = process.stderr, + loaderProperties = { + mainModulePath: path.join(__dirname, '../../'), + }, + }: { + argv?: string[]; + stdin?: ReadStream; + stdout?: WriteStream; + stderr?: WriteStream; + loaderProperties?: LoaderProperties; + } = {}): void { + // Parse the command-line arguments + const { argv: params } = yargs(argv.slice(2)) + .usage('node ./bin/server.js [args]') + .options({ + baseUrl: { type: 'string', alias: 'b' }, + config: { type: 'string', alias: 'c' }, + loggingLevel: { type: 'string', alias: 'l', default: 'info' }, + port: { type: 'number', alias: 'p', default: 3000 }, + rootFilePath: { type: 'string', alias: 'f' }, + sparqlEndpoint: { type: 'string', alias: 's' }, + podTemplateFolder: { type: 'string', alias: 't' }, + }) + .help(); + + // Gather settings for instantiating the server + const configFile = params.config ? path.join(process.cwd(), params.config) : path.join(__dirname, '/../../config/config-default.json'); + const variables = this.createVariables(params); - // Setup from config file - const loader = new Loader(properties); - await loader.registerAvailableModuleResources(); - const initializer: Initializer = await loader - .instantiateFromUrl('urn:solid-server:default:Initializer', configPath, undefined, { - variables: { - 'urn:solid-server:default:variable:baseUrl': - params.baseUrl ? ensureTrailingSlash(params.baseUrl) : `http://localhost:${params.port}/`, - 'urn:solid-server:default:variable:loggingLevel': params.loggingLevel, - 'urn:solid-server:default:variable:port': params.port, - 'urn:solid-server:default:variable:rootFilePath': params.rootFilePath ?? process.cwd(), - 'urn:solid-server:default:variable:sparqlEndpoint': params.sparqlEndpoint, - 'urn:solid-server:default:variable:podTemplateFolder': - params.podTemplateFolder ?? path.join(__dirname, '../../templates'), + // Create and execute the server initializer + this.createInitializer(loaderProperties, configFile, variables) + .then( + async(initializer): Promise => initializer.handleSafe(), + (error: Error): void => { + // Instantiation of components has failed, so there is no logger to use + stderr.write(`Error: could not instantiate server from ${configFile}\n`); + stderr.write(`${error.stack}\n`); + process.exit(1); }, - }) as Initializer; - await initializer.handleSafe(); - })().catch((error): void => { - // This is the only time we can *not* use the logger to print error messages, as dependency injection has failed. - stderr.write(`${error}\n`); - }); -}; + ).catch((error): void => { + this.logger.error(`Could not initialize server: ${error}`, { error }); + process.exit(1); + }); + } + + /** + * Translates command-line parameters into configuration variables + */ + protected createVariables(params: Record): Record { + return { + 'urn:solid-server:default:variable:baseUrl': + params.baseUrl ? ensureTrailingSlash(params.baseUrl) : `http://localhost:${params.port}/`, + 'urn:solid-server:default:variable:loggingLevel': params.loggingLevel, + 'urn:solid-server:default:variable:port': params.port, + 'urn:solid-server:default:variable:rootFilePath': params.rootFilePath ?? process.cwd(), + 'urn:solid-server:default:variable:sparqlEndpoint': params.sparqlEndpoint, + 'urn:solid-server:default:variable:podTemplateFolder': + params.podTemplateFolder ?? path.join(__dirname, '../../templates'), + }; + } + + /** + * Creates the server initializer + */ + protected async createInitializer(loaderProperties: LoaderProperties, configFile: string, + variables: Record): Promise { + const loader = new Loader(loaderProperties); + await loader.registerAvailableModuleResources(); + + const initializer = 'urn:solid-server:default:Initializer'; + return await loader.instantiateFromUrl(initializer, configFile, undefined, { variables }) as Initializer; + } +} diff --git a/test/unit/init/CliRunner.test.ts b/test/unit/init/CliRunner.test.ts index 90be924a9..80dafb4d5 100644 --- a/test/unit/init/CliRunner.test.ts +++ b/test/unit/init/CliRunner.test.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { Loader } from 'componentsjs'; -import { runCli } from '../../../src/init/CliRunner'; +import { CliRunner } from '../../../src/init/CliRunner'; import type { Initializer } from '../../../src/init/Initializer'; const mainModulePath = path.join(__dirname, '../../../'); @@ -19,13 +19,16 @@ jest.mock('componentsjs', (): any => ({ Loader: jest.fn((): Loader => loader), })); +const write = jest.spyOn(process.stderr, 'write').mockImplementation(jest.fn()); +const exit = jest.spyOn(process, 'exit').mockImplementation(jest.fn() as any); + describe('CliRunner', (): void => { afterEach((): void => { jest.clearAllMocks(); }); it('starts the server with default settings.', async(): Promise => { - runCli({ + new CliRunner().run({ argv: [ 'node', 'script' ], }); await initializer.handleSafe(); @@ -55,7 +58,7 @@ describe('CliRunner', (): void => { }); it('accepts abbreviated flags.', async(): Promise => { - runCli({ + new CliRunner().run({ argv: [ 'node', 'script', '-p', '4000', @@ -87,7 +90,7 @@ describe('CliRunner', (): void => { }); it('accepts full flags.', async(): Promise => { - runCli({ + new CliRunner().run({ argv: [ 'node', 'script', '--port', '4000', @@ -118,15 +121,29 @@ describe('CliRunner', (): void => { ); }); - it('writes to stderr when an error occurs.', async(): Promise => { - const write = jest.spyOn(process.stderr, 'write').mockImplementation((): any => null); + it('exits with output to stderr when instantiation fails.', async(): Promise => { loader.instantiateFromUrl.mockRejectedValueOnce(new Error('Fatal')); - - runCli(); + new CliRunner().run(); await new Promise((resolve): any => setImmediate(resolve)); - expect(write).toHaveBeenCalledTimes(1); - expect(write).toHaveBeenCalledWith('Error: Fatal\n'); - write.mockClear(); + expect(write).toHaveBeenCalledTimes(2); + expect(write).toHaveBeenNthCalledWith(1, + expect.stringMatching(/^Error: could not instantiate server from .*config-default\.json/u)); + expect(write).toHaveBeenNthCalledWith(2, + expect.stringMatching(/^Error: Fatal/u)); + + expect(exit).toHaveBeenCalledTimes(1); + expect(exit).toHaveBeenCalledWith(1); + }); + + it('exits without output to stderr when initialization fails.', async(): Promise => { + initializer.handleSafe.mockRejectedValueOnce(new Error('Fatal')); + new CliRunner().run(); + await new Promise((resolve): any => setImmediate(resolve)); + + expect(write).toHaveBeenCalledTimes(0); + + expect(exit).toHaveBeenCalledTimes(1); + expect(exit).toHaveBeenCalledWith(1); }); });