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")