Skip to content

Commit 4d848ab

Browse files
feat: introduce cleanup child process
In case CLI uses non-Nodejs resources, there's nowhere to clean them as CLI does not handle the SIGINT (Ctrl+C) signal. For example, when we debug on Android, we setup port forward with `adb` command. Once CLI process is about to stop, the adb forward should be removed, but currently there's nowhere to do it. So, to handle this, introduce a new cleanup child process. It is detached and unrefed. As we have IPC communication with it, when CLI exits gracefully (i.e. when we are not in a long living command which usually exits with Ctrl+C), we should manually disconnect the process. Whenever some external resources must be cleaned, the actual cleanup commands that should be executed are sent to the new process. It caches them and once the CLI finishes its work, the cleanup process will receive `disconnect` event. At this point the cleanup process will execute all cleanup actions and after that it will exit. The idea of the process is very similar to the process used for analytics tracking, so several interfaces were reused. They are just renamed to have a better meaning for the new used cases. Introduce `--cleanupLogFile` option, which allows the user to specify path to file, where information about all actions will be logged. Also expose a method in CLI's public API that allows setting this file when using CLI as a library.
1 parent b820bc5 commit 4d848ab

23 files changed

+314
-75
lines changed

lib/bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,4 @@ $injector.require("qrCodeTerminalService", "./services/qr-code-terminal-service"
192192
$injector.require("testInitializationService", "./services/test-initialization-service");
193193

194194
$injector.require("networkConnectivityValidator", "./helpers/network-connectivity-validator");
195+
$injector.requirePublic("cleanupService", "./services/cleanup-service");

lib/common/services/commands-service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export class CommandsService implements ICommandsService {
2828
private $helpService: IHelpService,
2929
private $extensibilityService: IExtensibilityService,
3030
private $optionsTracker: IOptionsTracker,
31-
private $projectDataService: IProjectDataService) {
31+
private $projectDataService: IProjectDataService,
32+
private $cleanupService: ICleanupService) {
3233
let projectData = null;
3334
try {
3435
projectData = this.$projectDataService.getProjectData();
@@ -37,6 +38,7 @@ export class CommandsService implements ICommandsService {
3738
}
3839

3940
this.$options.setupOptions(projectData);
41+
this.$cleanupService.setShouldDispose(this.$options.justlaunch || !this.$options.watch);
4042
}
4143

4244
public allCommands(opts: { includeDevCommands: boolean }): string[] {

lib/declarations.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai
570570
analyticsLogFile: string;
571571
performance: Object;
572572
setupOptions(projectData: IProjectData): void;
573+
cleanupLogFile: string;
573574
}
574575

575576
interface IEnvOptions {

lib/definitions/cleanup-service.d.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Descibes the cleanup service which allows scheduling cleanup actions
3+
* The actions will be executed once CLI process exits.
4+
*/
5+
interface ICleanupService extends IShouldDispose, IDisposable {
6+
/**
7+
* Add new action to be executed when CLI process exits.
8+
* @param {ICleanupAction} action The action that should be executed, including command and args.
9+
* @returns {Promise<void>}
10+
*/
11+
addCleanupAction(action: ICleanupAction): Promise<void>;
12+
13+
/**
14+
* Sets the file in which the cleanup process will write its logs.
15+
* This method must be called before starting the cleanup process, i.e. when CLI is initialized.
16+
* @param {string} filePath Path to file where the logs will be written. The logs are appended to the passed file.
17+
* @returns {void}
18+
*/
19+
setCleanupLogFile(filePath: string): void
20+
}

lib/definitions/file-log-service.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Describes message that needs to be logged in the analytics logging file.
3+
*/
4+
interface IFileLogMessage {
5+
message: string;
6+
type?: FileLogMessageType;
7+
}
8+
9+
/**
10+
* Describes methods to get local logs from analytics tracking.
11+
*/
12+
interface IFileLogService {
13+
/**
14+
* Logs specified message to the previously specified file.
15+
* @param {IFileLogMessage} fileLogMessage The message that has to be written to the logs file.
16+
* @returns {void}
17+
*/
18+
logData(fileLogMessage: IFileLogMessage): void;
19+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
interface ICleanupAction {
2+
/**
3+
* Executable to be started.
4+
*/
5+
command: string;
6+
7+
/**
8+
* Arguments that will be passed to the child process
9+
*/
10+
args: string[];
11+
/**
12+
* Timeout to execute the action.
13+
*/
14+
timeout?: number;
15+
}
16+
17+
interface ICleanupProcessMessage {
18+
/**
19+
* Type of the action
20+
*/
21+
actionType: CleanupProcessMessageType;
22+
23+
/**
24+
* Describes the action that must be executed
25+
*/
26+
action: ICleanupAction;
27+
}
28+
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// NOTE: This file is used to clean up resources used by CLI, when the CLI is killed.
2+
// The instances here are not shared with the ones in main CLI process.
3+
import * as fs from "fs";
4+
import { FileLogService } from "./file-log-service";
5+
6+
const pathToBootstrap = process.argv[2];
7+
if (!pathToBootstrap || !fs.existsSync(pathToBootstrap)) {
8+
throw new Error("Invalid path to bootstrap.");
9+
}
10+
11+
const logFile = process.argv[3];
12+
// After requiring the bootstrap we can use $injector
13+
require(pathToBootstrap);
14+
15+
const fileLogService = $injector.resolve<IFileLogService>(FileLogService, { logFile });
16+
fileLogService.logData({ message: "Initializing Cleanup process." });
17+
18+
const actionsToExecute: ICleanupAction[] = [];
19+
20+
const executeCleanup = async () => {
21+
const $childProcess = $injector.resolve<IChildProcess>("childProcess");
22+
for (const action of actionsToExecute) {
23+
try {
24+
fileLogService.logData({ message: `Start executing action: ${JSON.stringify(action)}` });
25+
26+
// TODO: Add timeout for each action here
27+
await $childProcess.trySpawnFromCloseEvent(action.command, action.args);
28+
fileLogService.logData({ message: `Successfully executed action: ${JSON.stringify(action)}` });
29+
} catch (err) {
30+
fileLogService.logData({ message: `Unable to execute action: ${JSON.stringify(action)}`, type: FileLogMessageType.Error });
31+
}
32+
}
33+
34+
fileLogService.logData({ message: `cleanup-process finished` });
35+
process.exit();
36+
};
37+
38+
const addCleanupAction = (newAction: ICleanupAction): void => {
39+
if (!_.some(actionsToExecute, currentAction => _.isEqual(currentAction, newAction))) {
40+
fileLogService.logData({ message: `cleanup-process added action for execution: ${JSON.stringify(newAction)}` });
41+
actionsToExecute.push(newAction);
42+
} else {
43+
fileLogService.logData({ message: `cleanup-process will not add action for execution as it has been added already: ${JSON.stringify(newAction)}` });
44+
}
45+
};
46+
47+
process.on("message", async (cleanupProcessMessage: ICleanupProcessMessage) => {
48+
fileLogService.logData({ message: `cleanup-process received message of type: ${JSON.stringify(cleanupProcessMessage)}` });
49+
50+
switch (cleanupProcessMessage.actionType) {
51+
case CleanupProcessMessageType.AddCleanAction:
52+
addCleanupAction(cleanupProcessMessage.action);
53+
break;
54+
default:
55+
fileLogService.logData({ message: `Unable to handle message of type ${cleanupProcessMessage.actionType}. Full message is ${JSON.stringify(cleanupProcessMessage)}`, type: FileLogMessageType.Error });
56+
break;
57+
}
58+
59+
});
60+
61+
process.on("disconnect", async () => {
62+
fileLogService.logData({ message: "cleanup-process received process.disconnect event" });
63+
await executeCleanup();
64+
});
65+
66+
fileLogService.logData({ message: `cleanup-process will send ${DetachedProcessMessages.ProcessReadyToReceive} message` });
67+
68+
process.send(DetachedProcessMessages.ProcessReadyToReceive);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Defines messages used in communication between CLI's process and analytics subprocesses.
3+
*/
4+
declare const enum DetachedProcessMessages {
5+
/**
6+
* The detached process is initialized and is ready to receive information for tracking.
7+
*/
8+
ProcessReadyToReceive = "ProcessReadyToReceive"
9+
}
10+
11+
/**
12+
* Defines the type of the messages that should be written in the local analyitcs log file (in case such is specified).
13+
*/
14+
declare const enum FileLogMessageType {
15+
/**
16+
* Information message. This is the default value in case type is not specified.
17+
*/
18+
Info = "Info",
19+
20+
/**
21+
* Error message - used to indicate that some action did not succeed.
22+
*/
23+
Error = "Error"
24+
}
25+
26+
declare const enum CleanupProcessMessageType {
27+
AddCleanAction = "AddCleanAction",
28+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { EOL } from "os";
2+
import { getFixedLengthDateString } from "../common/helpers";
3+
4+
export class FileLogService implements IFileLogService {
5+
constructor(private $fs: IFileSystem,
6+
private logFile: string) { }
7+
8+
public logData(fileLoggingMessage: IFileLogMessage): void {
9+
if (this.logFile && fileLoggingMessage && fileLoggingMessage.message) {
10+
fileLoggingMessage.type = fileLoggingMessage.type || FileLogMessageType.Info;
11+
const formattedDate = getFixedLengthDateString();
12+
this.$fs.appendFile(this.logFile, `[${formattedDate}] [${fileLoggingMessage.type}] ${fileLoggingMessage.message}${EOL}`);
13+
}
14+
}
15+
}

lib/options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export class Options {
147147
default: { type: OptionType.Boolean, hasSensitiveValue: false },
148148
count: { type: OptionType.Number, hasSensitiveValue: false },
149149
analyticsLogFile: { type: OptionType.String, hasSensitiveValue: true },
150+
cleanupLogFile: { type: OptionType.String, hasSensitiveValue: true },
150151
hooks: { type: OptionType.Boolean, default: true, hasSensitiveValue: false },
151152
link: { type: OptionType.Boolean, default: false, hasSensitiveValue: false },
152153
aab: { type: OptionType.Boolean, hasSensitiveValue: false },

0 commit comments

Comments
 (0)