Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions lib/definitions/cleanup-service.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,20 @@ interface ICleanupService extends IShouldDispose, IDisposable {
* @returns {Promise<void>}
*/
removeCleanupDeleteAction(filePath: string): Promise<void>;

/**
* 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<void>}
*/
addCleanupJS(jsCommand: IJSCommand): Promise<void>;

/**
* 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<void>}
*/
removeCleanupJS(jsCommand: IJSCommand): Promise<void>;
}
58 changes: 58 additions & 0 deletions lib/detached-processes/cleanup-js-subprocess.ts
Original file line number Diff line number Diff line change
@@ -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<IFileLogService>(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 */
34 changes: 23 additions & 11 deletions lib/detached-processes/cleanup-process-definitions.d.ts
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand All @@ -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 {
Expand All @@ -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<any>;
}

interface IJSCleanupMessage extends ICleanupMessageBase {
jsCommand: IJSCommand;
}
70 changes: 64 additions & 6 deletions lib/detached-processes/cleanup-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,47 @@ fileLogService.logData({ message: "Initializing Cleanup process." });

const commandsInfos: ISpawnCommandInfo[] = [];
const filesToDelete: string[] = [];
const jsCommands: IJSCommand[] = [];

const executeJSCleanup = async (jsCommand: IJSCommand) => {
const $childProcess = $injector.resolve<IChildProcess>("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<IChildProcess>("childProcess");

for (const jsCommand of jsCommands) {
await executeJSCleanup(jsCommand);
}

for (const commandInfo of commandsInfos) {
try {
fileLogService.logData({ message: `Start executing command: ${JSON.stringify(commandInfo)}` });

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` });
Expand All @@ -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)}` });
}
};

Expand All @@ -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)}` });

Expand All @@ -93,10 +143,18 @@ process.on("message", async (cleanupProcessMessage: ICleanupMessageBase) => {
removeCleanupAction((<ISpawnCommandCleanupMessage>cleanupProcessMessage).commandInfo);
break;
case CleanupProcessMessage.AddDeleteFileAction:
addDeleteAction((<IDeleteFileCleanupMessage>cleanupProcessMessage).filePath);
addDeleteAction((<IFileCleanupMessage>cleanupProcessMessage).filePath);
break;
case CleanupProcessMessage.RemoveDeleteFileAction:
removeDeleteAction((<IDeleteFileCleanupMessage>cleanupProcessMessage).filePath);
removeDeleteAction((<IFileCleanupMessage>cleanupProcessMessage).filePath);
break;
case CleanupProcessMessage.AddJSFileToRequire:
const jsCleanupMessage = <IJSCleanupMessage>cleanupProcessMessage;
addJSFile(jsCleanupMessage.jsCommand);
break;
case CleanupProcessMessage.RemoveJSFileToRequire:
const msgToRemove = <IJSCleanupMessage>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 });
Expand Down
11 changes: 10 additions & 1 deletion lib/detached-processes/detached-process-enums.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
14 changes: 12 additions & 2 deletions lib/services/cleanup-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,22 @@ export class CleanupService implements ICleanupService {

public async addCleanupDeleteAction(filePath: string): Promise<void> {
const cleanupProcess = await this.getCleanupProcess();
cleanupProcess.send(<IDeleteFileCleanupMessage>{ messageType: CleanupProcessMessage.AddDeleteFileAction, filePath });
cleanupProcess.send(<IFileCleanupMessage>{ messageType: CleanupProcessMessage.AddDeleteFileAction, filePath });
}

public async removeCleanupDeleteAction(filePath: string): Promise<void> {
const cleanupProcess = await this.getCleanupProcess();
cleanupProcess.send(<IDeleteFileCleanupMessage>{ messageType: CleanupProcessMessage.RemoveDeleteFileAction, filePath });
cleanupProcess.send(<IFileCleanupMessage>{ messageType: CleanupProcessMessage.RemoveDeleteFileAction, filePath });
}

public async addCleanupJS(jsCommand: IJSCommand): Promise<void> {
const cleanupProcess = await this.getCleanupProcess();
cleanupProcess.send(<IJSCleanupMessage>{ messageType: CleanupProcessMessage.AddJSFileToRequire, jsCommand });
}

public async removeCleanupJS(jsCommand: IJSCommand): Promise<void> {
const cleanupProcess = await this.getCleanupProcess();
cleanupProcess.send(<IJSCleanupMessage>{ messageType: CleanupProcessMessage.RemoveJSFileToRequire, jsCommand});
}

@exported("cleanupService")
Expand Down