From 3e586f2eb0b5671e91c654b3a25ee31e2252df9e Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Sun, 21 Apr 2019 12:55:54 +0300 Subject: [PATCH] feat: allow executing JS funcitons on cleanup In some cases it is required to execute some JS functions to clean used resources by CLI. Add an easy way to specify JS files which should be executed by the cleanup process. Each JS action is defined by a JS File, which should be required, data that will be passed to the default exported function in the file and timeout - if the action cannot be executed for specified time (3 seconds is the default), the child process in which the JS action is executed will be killed. --- lib/definitions/cleanup-service.d.ts | 16 +++++ .../cleanup-js-subprocess.ts | 58 +++++++++++++++ .../cleanup-process-definitions.d.ts | 34 ++++++--- lib/detached-processes/cleanup-process.ts | 70 +++++++++++++++++-- .../detached-process-enums.d.ts | 11 ++- lib/services/cleanup-service.ts | 14 +++- 6 files changed, 183 insertions(+), 20 deletions(-) create mode 100644 lib/detached-processes/cleanup-js-subprocess.ts diff --git a/lib/definitions/cleanup-service.d.ts b/lib/definitions/cleanup-service.d.ts index 59e56a350f..abec414790 100644 --- a/lib/definitions/cleanup-service.d.ts +++ b/lib/definitions/cleanup-service.d.ts @@ -40,4 +40,20 @@ interface ICleanupService extends IShouldDispose, IDisposable { * @returns {Promise} */ removeCleanupDeleteAction(filePath: string): Promise; + + /** + * Adds JS file to be required and executed during cleanup. + * NOTE: The JS file will be required in a new child process, so you can pass timeout for the execution. + * In the child process you can use all injected dependencies of CLI. + * @param {IJSCommand} jsCommand Information about the JS file to be required and the data that should be passed to it. + * @returns {Promise} + */ + addCleanupJS(jsCommand: IJSCommand): Promise; + + /** + * Removes JS file to be required and executed during cleanup. + * @param {IJSCommand} filePath jsCommand Information about the JS file to be required and the data that should not be passed to it. + * @returns {Promise} + */ + removeCleanupJS(jsCommand: IJSCommand): Promise; } diff --git a/lib/detached-processes/cleanup-js-subprocess.ts b/lib/detached-processes/cleanup-js-subprocess.ts new file mode 100644 index 0000000000..c1c5adbd54 --- /dev/null +++ b/lib/detached-processes/cleanup-js-subprocess.ts @@ -0,0 +1,58 @@ +#!/usr/bin/env node + +// NOTE: This file is used to call JS functions when cleaning resources used by CLI, after the CLI is killed. +// The instances here are not shared with the ones in main CLI process. +import * as fs from "fs"; +import * as uuid from "uuid"; +import { FileLogService } from "./file-log-service"; + +const pathToBootstrap = process.argv[2]; +if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) { + throw new Error("Invalid path to bootstrap."); +} + +// After requiring the bootstrap we can use $injector +require(pathToBootstrap); + +const logFile = process.argv[3]; +const jsFilePath = process.argv[4]; + +const fileLogService = $injector.resolve(FileLogService, { logFile }); +const uniqueId = uuid.v4(); +fileLogService.logData({ message: `Initializing Cleanup process for path: ${jsFilePath} Unique id: ${uniqueId}` }); + +if (!fs.existsSync(jsFilePath)) { + throw new Error(`Unable to find file ${jsFilePath}. Ensure it exists.`); +} + +let data: any; +try { + data = process.argv[5] && JSON.parse(process.argv[5]); +} catch (err) { + throw new Error(`Unable to parse data from argv ${process.argv[5]}.`); +} + +const logMessage = (msg: string, type?: FileLogMessageType): void => { + fileLogService.logData({ message: `[${uniqueId}] ${msg}`, type }); +}; + +/* tslint:disable:no-floating-promises */ +(async () => { + try { + logMessage(`Requiring file ${jsFilePath}`); + + const func = require(jsFilePath); + if (func && typeof func === "function") { + try { + logMessage(`Passing data: ${JSON.stringify(data)} to the default function exported by currently required file ${jsFilePath}`); + await func(data); + logMessage(`Finished execution with data: ${JSON.stringify(data)} to the default function exported by currently required file ${jsFilePath}`); + } catch (err) { + logMessage(`Unable to execute action of file ${jsFilePath} when passed data is ${JSON.stringify(data)}. Error is: ${err}.`, FileLogMessageType.Error); + } + } + } catch (err) { + logMessage(`Unable to require file: ${jsFilePath}. Error is: ${err}.`, FileLogMessageType.Error); + } +})(); +/* tslint:enable:no-floating-promises */ diff --git a/lib/detached-processes/cleanup-process-definitions.d.ts b/lib/detached-processes/cleanup-process-definitions.d.ts index 72ed39579a..24eab593a4 100644 --- a/lib/detached-processes/cleanup-process-definitions.d.ts +++ b/lib/detached-processes/cleanup-process-definitions.d.ts @@ -1,4 +1,18 @@ -interface ISpawnCommandInfo { +interface ITimeout { + /** + * Timeout to execute the action. + */ + timeout?: number; +} + +interface IFilePath { + /** + * Path to file/directory to be deleted or required + */ + filePath: string; +} + +interface ISpawnCommandInfo extends ITimeout { /** * Executable to be started. */ @@ -8,11 +22,6 @@ interface ISpawnCommandInfo { * Arguments that will be passed to the child process */ args: string[]; - - /** - * Timeout to execute the action. - */ - timeout?: number; } interface ICleanupMessageBase { @@ -29,9 +38,12 @@ interface ISpawnCommandCleanupMessage extends ICleanupMessageBase { commandInfo: ISpawnCommandInfo; } -interface IDeleteFileCleanupMessage extends ICleanupMessageBase { - /** - * Path to file/directory to be deleted. - */ - filePath: string; +interface IFileCleanupMessage extends ICleanupMessageBase, IFilePath { } + +interface IJSCommand extends ITimeout, IFilePath { + data: IDictionary; } + +interface IJSCleanupMessage extends ICleanupMessageBase { + jsCommand: IJSCommand; + } diff --git a/lib/detached-processes/cleanup-process.ts b/lib/detached-processes/cleanup-process.ts index 705c266c6f..825b48d613 100644 --- a/lib/detached-processes/cleanup-process.ts +++ b/lib/detached-processes/cleanup-process.ts @@ -19,9 +19,29 @@ fileLogService.logData({ message: "Initializing Cleanup process." }); const commandsInfos: ISpawnCommandInfo[] = []; const filesToDelete: string[] = []; +const jsCommands: IJSCommand[] = []; + +const executeJSCleanup = async (jsCommand: IJSCommand) => { + const $childProcess = $injector.resolve("childProcess"); + + try { + fileLogService.logData({ message: `Start executing action for file: ${jsCommand.filePath} and data ${JSON.stringify(jsCommand.data)}` }); + + await $childProcess.trySpawnFromCloseEvent(process.execPath, [path.join(__dirname, "cleanup-js-subprocess.js"), pathToBootstrap, logFile, jsCommand.filePath, JSON.stringify(jsCommand.data)], {}, { throwError: true, timeout: jsCommand.timeout || 3000 }); + fileLogService.logData({ message: `Finished xecuting action for file: ${jsCommand.filePath} and data ${JSON.stringify(jsCommand.data)}` }); + + } catch (err) { + fileLogService.logData({ message: `Unable to execute action for file ${jsCommand.filePath} with data ${JSON.stringify(jsCommand.data)}. Error is: ${err}.`, type: FileLogMessageType.Error }); + } +}; const executeCleanup = async () => { const $childProcess = $injector.resolve("childProcess"); + + for (const jsCommand of jsCommands) { + await executeJSCleanup(jsCommand); + } + for (const commandInfo of commandsInfos) { try { fileLogService.logData({ message: `Start executing command: ${JSON.stringify(commandInfo)}` }); @@ -29,13 +49,17 @@ const executeCleanup = async () => { await $childProcess.trySpawnFromCloseEvent(commandInfo.command, commandInfo.args, {}, { throwError: true, timeout: commandInfo.timeout || 3000 }); fileLogService.logData({ message: `Successfully executed command: ${JSON.stringify(commandInfo)}` }); } catch (err) { - fileLogService.logData({ message: `Unable to execute command: ${JSON.stringify(commandInfo)}`, type: FileLogMessageType.Error }); + fileLogService.logData({ message: `Unable to execute command: ${JSON.stringify(commandInfo)}. Error is: ${err}.`, type: FileLogMessageType.Error }); } } if (filesToDelete.length) { - fileLogService.logData({ message: `Deleting files ${filesToDelete.join(" ")}` }); - shelljs.rm("-Rf", filesToDelete); + try { + fileLogService.logData({ message: `Deleting files ${filesToDelete.join(" ")}` }); + shelljs.rm("-Rf", filesToDelete); + } catch (err) { + fileLogService.logData({ message: `Unable to delete files: ${JSON.stringify(filesToDelete)}. Error is: ${err}.`, type: FileLogMessageType.Error }); + } } fileLogService.logData({ message: `cleanup-process finished` }); @@ -56,7 +80,7 @@ const removeCleanupAction = (commandInfo: ISpawnCommandInfo): void => { _.remove(commandsInfos, currentCommandInfo => _.isEqual(currentCommandInfo, commandInfo)); fileLogService.logData({ message: `cleanup-process removed command for execution: ${JSON.stringify(commandInfo)}` }); } else { - fileLogService.logData({ message: `cleanup-process cannot remove command for execution as it has note been added before: ${JSON.stringify(commandInfo)}` }); + fileLogService.logData({ message: `cleanup-process cannot remove command for execution as it has not been added before: ${JSON.stringify(commandInfo)}` }); } }; @@ -82,6 +106,32 @@ const removeDeleteAction = (filePath: string): void => { } }; +const addJSFile = (jsCommand: IJSCommand): void => { + const fullPath = path.resolve(jsCommand.filePath); + + jsCommand.filePath = fullPath; + + if (_.some(jsCommands, currentJSCommand => _.isEqual(currentJSCommand, jsCommand))) { + fileLogService.logData({ message: `cleanup-process will not add JS file for execution as it has been added already: ${JSON.stringify(jsCommand)}` }); + } else { + fileLogService.logData({ message: `cleanup-process added JS file for execution: ${JSON.stringify(jsCommand)}` }); + jsCommands.push(jsCommand); + } +}; + +const removeJSFile = (jsCommand: IJSCommand): void => { + const fullPath = path.resolve(jsCommand.filePath); + + jsCommand.filePath = fullPath; + + if (_.some(jsCommands, currentJSCommand => _.isEqual(currentJSCommand, jsCommand))) { + _.remove(jsCommands, currentJSCommand => _.isEqual(currentJSCommand, jsCommand)); + fileLogService.logData({ message: `cleanup-process removed JS action for execution: ${JSON.stringify(jsCommand)}` }); + } else { + fileLogService.logData({ message: `cleanup-process cannot remove JS action for execution as it has not been added before: ${JSON.stringify(jsCommand)}` }); + } +}; + process.on("message", async (cleanupProcessMessage: ICleanupMessageBase) => { fileLogService.logData({ message: `cleanup-process received message of type: ${JSON.stringify(cleanupProcessMessage)}` }); @@ -93,10 +143,18 @@ process.on("message", async (cleanupProcessMessage: ICleanupMessageBase) => { removeCleanupAction((cleanupProcessMessage).commandInfo); break; case CleanupProcessMessage.AddDeleteFileAction: - addDeleteAction((cleanupProcessMessage).filePath); + addDeleteAction((cleanupProcessMessage).filePath); break; case CleanupProcessMessage.RemoveDeleteFileAction: - removeDeleteAction((cleanupProcessMessage).filePath); + removeDeleteAction((cleanupProcessMessage).filePath); + break; + case CleanupProcessMessage.AddJSFileToRequire: + const jsCleanupMessage = cleanupProcessMessage; + addJSFile(jsCleanupMessage.jsCommand); + break; + case CleanupProcessMessage.RemoveJSFileToRequire: + const msgToRemove = cleanupProcessMessage; + removeJSFile(msgToRemove.jsCommand); break; default: fileLogService.logData({ message: `Unable to handle message of type ${cleanupProcessMessage.messageType}. Full message is ${JSON.stringify(cleanupProcessMessage)}`, type: FileLogMessageType.Error }); diff --git a/lib/detached-processes/detached-process-enums.d.ts b/lib/detached-processes/detached-process-enums.d.ts index 1e3a16f5d6..c8e27edede 100644 --- a/lib/detached-processes/detached-process-enums.d.ts +++ b/lib/detached-processes/detached-process-enums.d.ts @@ -40,8 +40,17 @@ declare const enum CleanupProcessMessage { AddDeleteFileAction = "AddDeleteFileAction", /** - * This type of message defines the cleanup procedure should not delete previously specified file. + * This type of message defines that the cleanup procedure should not delete previously specified file. */ RemoveDeleteFileAction = "RemoveDeleteFileAction", + /** + * This type of message defines that the cleanup procedure will require the specified JS file, which should execute some action. + */ + AddJSFileToRequire = "AddJSFileToRequire", + + /** + * This type of message defines that the cleanup procedure will not require the previously specified JS file. + */ + RemoveJSFileToRequire = "RemoveJSFileToRequire", } diff --git a/lib/services/cleanup-service.ts b/lib/services/cleanup-service.ts index 671f6afed6..953f330e25 100644 --- a/lib/services/cleanup-service.ts +++ b/lib/services/cleanup-service.ts @@ -27,12 +27,22 @@ export class CleanupService implements ICleanupService { public async addCleanupDeleteAction(filePath: string): Promise { const cleanupProcess = await this.getCleanupProcess(); - cleanupProcess.send({ messageType: CleanupProcessMessage.AddDeleteFileAction, filePath }); + cleanupProcess.send({ messageType: CleanupProcessMessage.AddDeleteFileAction, filePath }); } public async removeCleanupDeleteAction(filePath: string): Promise { const cleanupProcess = await this.getCleanupProcess(); - cleanupProcess.send({ messageType: CleanupProcessMessage.RemoveDeleteFileAction, filePath }); + cleanupProcess.send({ messageType: CleanupProcessMessage.RemoveDeleteFileAction, filePath }); + } + + public async addCleanupJS(jsCommand: IJSCommand): Promise { + const cleanupProcess = await this.getCleanupProcess(); + cleanupProcess.send({ messageType: CleanupProcessMessage.AddJSFileToRequire, jsCommand }); + } + + public async removeCleanupJS(jsCommand: IJSCommand): Promise { + const cleanupProcess = await this.getCleanupProcess(); + cleanupProcess.send({ messageType: CleanupProcessMessage.RemoveJSFileToRequire, jsCommand}); } @exported("cleanupService")