Skip to content

Commit 3e586f2

Browse files
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.
1 parent 646f7a7 commit 3e586f2

File tree

6 files changed

+183
-20
lines changed

6 files changed

+183
-20
lines changed

lib/definitions/cleanup-service.d.ts

+16
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,20 @@ interface ICleanupService extends IShouldDispose, IDisposable {
4040
* @returns {Promise<void>}
4141
*/
4242
removeCleanupDeleteAction(filePath: string): Promise<void>;
43+
44+
/**
45+
* Adds JS file to be required and executed during cleanup.
46+
* NOTE: The JS file will be required in a new child process, so you can pass timeout for the execution.
47+
* In the child process you can use all injected dependencies of CLI.
48+
* @param {IJSCommand} jsCommand Information about the JS file to be required and the data that should be passed to it.
49+
* @returns {Promise<void>}
50+
*/
51+
addCleanupJS(jsCommand: IJSCommand): Promise<void>;
52+
53+
/**
54+
* Removes JS file to be required and executed during cleanup.
55+
* @param {IJSCommand} filePath jsCommand Information about the JS file to be required and the data that should not be passed to it.
56+
* @returns {Promise<void>}
57+
*/
58+
removeCleanupJS(jsCommand: IJSCommand): Promise<void>;
4359
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env node
2+
3+
// NOTE: This file is used to call JS functions when cleaning resources used by CLI, after the CLI is killed.
4+
// The instances here are not shared with the ones in main CLI process.
5+
import * as fs from "fs";
6+
import * as uuid from "uuid";
7+
import { FileLogService } from "./file-log-service";
8+
9+
const pathToBootstrap = process.argv[2];
10+
if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) {
11+
throw new Error("Invalid path to bootstrap.");
12+
}
13+
14+
// After requiring the bootstrap we can use $injector
15+
require(pathToBootstrap);
16+
17+
const logFile = process.argv[3];
18+
const jsFilePath = process.argv[4];
19+
20+
const fileLogService = $injector.resolve<IFileLogService>(FileLogService, { logFile });
21+
const uniqueId = uuid.v4();
22+
fileLogService.logData({ message: `Initializing Cleanup process for path: ${jsFilePath} Unique id: ${uniqueId}` });
23+
24+
if (!fs.existsSync(jsFilePath)) {
25+
throw new Error(`Unable to find file ${jsFilePath}. Ensure it exists.`);
26+
}
27+
28+
let data: any;
29+
try {
30+
data = process.argv[5] && JSON.parse(process.argv[5]);
31+
} catch (err) {
32+
throw new Error(`Unable to parse data from argv ${process.argv[5]}.`);
33+
}
34+
35+
const logMessage = (msg: string, type?: FileLogMessageType): void => {
36+
fileLogService.logData({ message: `[${uniqueId}] ${msg}`, type });
37+
};
38+
39+
/* tslint:disable:no-floating-promises */
40+
(async () => {
41+
try {
42+
logMessage(`Requiring file ${jsFilePath}`);
43+
44+
const func = require(jsFilePath);
45+
if (func && typeof func === "function") {
46+
try {
47+
logMessage(`Passing data: ${JSON.stringify(data)} to the default function exported by currently required file ${jsFilePath}`);
48+
await func(data);
49+
logMessage(`Finished execution with data: ${JSON.stringify(data)} to the default function exported by currently required file ${jsFilePath}`);
50+
} catch (err) {
51+
logMessage(`Unable to execute action of file ${jsFilePath} when passed data is ${JSON.stringify(data)}. Error is: ${err}.`, FileLogMessageType.Error);
52+
}
53+
}
54+
} catch (err) {
55+
logMessage(`Unable to require file: ${jsFilePath}. Error is: ${err}.`, FileLogMessageType.Error);
56+
}
57+
})();
58+
/* tslint:enable:no-floating-promises */
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
1-
interface ISpawnCommandInfo {
1+
interface ITimeout {
2+
/**
3+
* Timeout to execute the action.
4+
*/
5+
timeout?: number;
6+
}
7+
8+
interface IFilePath {
9+
/**
10+
* Path to file/directory to be deleted or required
11+
*/
12+
filePath: string;
13+
}
14+
15+
interface ISpawnCommandInfo extends ITimeout {
216
/**
317
* Executable to be started.
418
*/
@@ -8,11 +22,6 @@ interface ISpawnCommandInfo {
822
* Arguments that will be passed to the child process
923
*/
1024
args: string[];
11-
12-
/**
13-
* Timeout to execute the action.
14-
*/
15-
timeout?: number;
1625
}
1726

1827
interface ICleanupMessageBase {
@@ -29,9 +38,12 @@ interface ISpawnCommandCleanupMessage extends ICleanupMessageBase {
2938
commandInfo: ISpawnCommandInfo;
3039
}
3140

32-
interface IDeleteFileCleanupMessage extends ICleanupMessageBase {
33-
/**
34-
* Path to file/directory to be deleted.
35-
*/
36-
filePath: string;
41+
interface IFileCleanupMessage extends ICleanupMessageBase, IFilePath { }
42+
43+
interface IJSCommand extends ITimeout, IFilePath {
44+
data: IDictionary<any>;
3745
}
46+
47+
interface IJSCleanupMessage extends ICleanupMessageBase {
48+
jsCommand: IJSCommand;
49+
}

lib/detached-processes/cleanup-process.ts

+64-6
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,47 @@ fileLogService.logData({ message: "Initializing Cleanup process." });
1919

2020
const commandsInfos: ISpawnCommandInfo[] = [];
2121
const filesToDelete: string[] = [];
22+
const jsCommands: IJSCommand[] = [];
23+
24+
const executeJSCleanup = async (jsCommand: IJSCommand) => {
25+
const $childProcess = $injector.resolve<IChildProcess>("childProcess");
26+
27+
try {
28+
fileLogService.logData({ message: `Start executing action for file: ${jsCommand.filePath} and data ${JSON.stringify(jsCommand.data)}` });
29+
30+
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 });
31+
fileLogService.logData({ message: `Finished xecuting action for file: ${jsCommand.filePath} and data ${JSON.stringify(jsCommand.data)}` });
32+
33+
} catch (err) {
34+
fileLogService.logData({ message: `Unable to execute action for file ${jsCommand.filePath} with data ${JSON.stringify(jsCommand.data)}. Error is: ${err}.`, type: FileLogMessageType.Error });
35+
}
36+
};
2237

2338
const executeCleanup = async () => {
2439
const $childProcess = $injector.resolve<IChildProcess>("childProcess");
40+
41+
for (const jsCommand of jsCommands) {
42+
await executeJSCleanup(jsCommand);
43+
}
44+
2545
for (const commandInfo of commandsInfos) {
2646
try {
2747
fileLogService.logData({ message: `Start executing command: ${JSON.stringify(commandInfo)}` });
2848

2949
await $childProcess.trySpawnFromCloseEvent(commandInfo.command, commandInfo.args, {}, { throwError: true, timeout: commandInfo.timeout || 3000 });
3050
fileLogService.logData({ message: `Successfully executed command: ${JSON.stringify(commandInfo)}` });
3151
} catch (err) {
32-
fileLogService.logData({ message: `Unable to execute command: ${JSON.stringify(commandInfo)}`, type: FileLogMessageType.Error });
52+
fileLogService.logData({ message: `Unable to execute command: ${JSON.stringify(commandInfo)}. Error is: ${err}.`, type: FileLogMessageType.Error });
3353
}
3454
}
3555

3656
if (filesToDelete.length) {
37-
fileLogService.logData({ message: `Deleting files ${filesToDelete.join(" ")}` });
38-
shelljs.rm("-Rf", filesToDelete);
57+
try {
58+
fileLogService.logData({ message: `Deleting files ${filesToDelete.join(" ")}` });
59+
shelljs.rm("-Rf", filesToDelete);
60+
} catch (err) {
61+
fileLogService.logData({ message: `Unable to delete files: ${JSON.stringify(filesToDelete)}. Error is: ${err}.`, type: FileLogMessageType.Error });
62+
}
3963
}
4064

4165
fileLogService.logData({ message: `cleanup-process finished` });
@@ -56,7 +80,7 @@ const removeCleanupAction = (commandInfo: ISpawnCommandInfo): void => {
5680
_.remove(commandsInfos, currentCommandInfo => _.isEqual(currentCommandInfo, commandInfo));
5781
fileLogService.logData({ message: `cleanup-process removed command for execution: ${JSON.stringify(commandInfo)}` });
5882
} else {
59-
fileLogService.logData({ message: `cleanup-process cannot remove command for execution as it has note been added before: ${JSON.stringify(commandInfo)}` });
83+
fileLogService.logData({ message: `cleanup-process cannot remove command for execution as it has not been added before: ${JSON.stringify(commandInfo)}` });
6084
}
6185
};
6286

@@ -82,6 +106,32 @@ const removeDeleteAction = (filePath: string): void => {
82106
}
83107
};
84108

109+
const addJSFile = (jsCommand: IJSCommand): void => {
110+
const fullPath = path.resolve(jsCommand.filePath);
111+
112+
jsCommand.filePath = fullPath;
113+
114+
if (_.some(jsCommands, currentJSCommand => _.isEqual(currentJSCommand, jsCommand))) {
115+
fileLogService.logData({ message: `cleanup-process will not add JS file for execution as it has been added already: ${JSON.stringify(jsCommand)}` });
116+
} else {
117+
fileLogService.logData({ message: `cleanup-process added JS file for execution: ${JSON.stringify(jsCommand)}` });
118+
jsCommands.push(jsCommand);
119+
}
120+
};
121+
122+
const removeJSFile = (jsCommand: IJSCommand): void => {
123+
const fullPath = path.resolve(jsCommand.filePath);
124+
125+
jsCommand.filePath = fullPath;
126+
127+
if (_.some(jsCommands, currentJSCommand => _.isEqual(currentJSCommand, jsCommand))) {
128+
_.remove(jsCommands, currentJSCommand => _.isEqual(currentJSCommand, jsCommand));
129+
fileLogService.logData({ message: `cleanup-process removed JS action for execution: ${JSON.stringify(jsCommand)}` });
130+
} else {
131+
fileLogService.logData({ message: `cleanup-process cannot remove JS action for execution as it has not been added before: ${JSON.stringify(jsCommand)}` });
132+
}
133+
};
134+
85135
process.on("message", async (cleanupProcessMessage: ICleanupMessageBase) => {
86136
fileLogService.logData({ message: `cleanup-process received message of type: ${JSON.stringify(cleanupProcessMessage)}` });
87137

@@ -93,10 +143,18 @@ process.on("message", async (cleanupProcessMessage: ICleanupMessageBase) => {
93143
removeCleanupAction((<ISpawnCommandCleanupMessage>cleanupProcessMessage).commandInfo);
94144
break;
95145
case CleanupProcessMessage.AddDeleteFileAction:
96-
addDeleteAction((<IDeleteFileCleanupMessage>cleanupProcessMessage).filePath);
146+
addDeleteAction((<IFileCleanupMessage>cleanupProcessMessage).filePath);
97147
break;
98148
case CleanupProcessMessage.RemoveDeleteFileAction:
99-
removeDeleteAction((<IDeleteFileCleanupMessage>cleanupProcessMessage).filePath);
149+
removeDeleteAction((<IFileCleanupMessage>cleanupProcessMessage).filePath);
150+
break;
151+
case CleanupProcessMessage.AddJSFileToRequire:
152+
const jsCleanupMessage = <IJSCleanupMessage>cleanupProcessMessage;
153+
addJSFile(jsCleanupMessage.jsCommand);
154+
break;
155+
case CleanupProcessMessage.RemoveJSFileToRequire:
156+
const msgToRemove = <IJSCleanupMessage>cleanupProcessMessage;
157+
removeJSFile(msgToRemove.jsCommand);
100158
break;
101159
default:
102160
fileLogService.logData({ message: `Unable to handle message of type ${cleanupProcessMessage.messageType}. Full message is ${JSON.stringify(cleanupProcessMessage)}`, type: FileLogMessageType.Error });

lib/detached-processes/detached-process-enums.d.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,17 @@ declare const enum CleanupProcessMessage {
4040
AddDeleteFileAction = "AddDeleteFileAction",
4141

4242
/**
43-
* This type of message defines the cleanup procedure should not delete previously specified file.
43+
* This type of message defines that the cleanup procedure should not delete previously specified file.
4444
*/
4545
RemoveDeleteFileAction = "RemoveDeleteFileAction",
4646

47+
/**
48+
* This type of message defines that the cleanup procedure will require the specified JS file, which should execute some action.
49+
*/
50+
AddJSFileToRequire = "AddJSFileToRequire",
51+
52+
/**
53+
* This type of message defines that the cleanup procedure will not require the previously specified JS file.
54+
*/
55+
RemoveJSFileToRequire = "RemoveJSFileToRequire",
4756
}

lib/services/cleanup-service.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,22 @@ export class CleanupService implements ICleanupService {
2727

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

3333
public async removeCleanupDeleteAction(filePath: string): Promise<void> {
3434
const cleanupProcess = await this.getCleanupProcess();
35-
cleanupProcess.send(<IDeleteFileCleanupMessage>{ messageType: CleanupProcessMessage.RemoveDeleteFileAction, filePath });
35+
cleanupProcess.send(<IFileCleanupMessage>{ messageType: CleanupProcessMessage.RemoveDeleteFileAction, filePath });
36+
}
37+
38+
public async addCleanupJS(jsCommand: IJSCommand): Promise<void> {
39+
const cleanupProcess = await this.getCleanupProcess();
40+
cleanupProcess.send(<IJSCleanupMessage>{ messageType: CleanupProcessMessage.AddJSFileToRequire, jsCommand });
41+
}
42+
43+
public async removeCleanupJS(jsCommand: IJSCommand): Promise<void> {
44+
const cleanupProcess = await this.getCleanupProcess();
45+
cleanupProcess.send(<IJSCleanupMessage>{ messageType: CleanupProcessMessage.RemoveJSFileToRequire, jsCommand});
3646
}
3747

3848
@exported("cleanupService")

0 commit comments

Comments
 (0)