Skip to content

Commit 59a7310

Browse files
Merge pull request #4533 from NativeScript/vladimirov/fix-ctrl-c
feat: introduce better way for handling Ctrl+C
2 parents 994fddc + 2d32053 commit 59a7310

File tree

67 files changed

+585
-489
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+585
-489
lines changed

PublicAPI.md

+27
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ const tns = require("nativescript");
7070
* [deviceLog](#devicelog)
7171
* [previewQrCodeService](#previewqrcodeservice)
7272
* [getPlaygroundAppQrCode](#getplaygroundappqrcode)
73+
* [cleanupService](#cleanupservice)
74+
* [setCleanupLogFile](#setcleanuplogfile)
7375

7476
## Module projectService
7577

@@ -1487,6 +1489,31 @@ tns.previewQrCodeService.getPlaygroundAppQrCode()
14871489
});
14881490
```
14891491
1492+
## cleanupService
1493+
The `cleanupService` is used to handle actions that should be executed after CLI's process had exited. This is an internal service, that runs detached childProcess in which it executes CLI's cleanup actions once CLI is dead. As the process is detached, logs from it are not shown anywhere, so the service exposes a way to add log file in which the child process will write its logs.
1494+
1495+
### setCleanupLogFile
1496+
Defines the log file location where the child cleanup process will write its logs.
1497+
1498+
> NOTE: You must call this method immediately after requiring NativeScript CLI. In case you call it after the cleanup process had started, it will not use the passed log file.
1499+
1500+
* Definition
1501+
```TypeScript
1502+
/**
1503+
* Sets the file in which the cleanup process will write its logs.
1504+
* This method must be called before starting the cleanup process, i.e. when CLI is initialized.
1505+
* @param {string} filePath Path to file where the logs will be written. The logs are appended to the passed file.
1506+
* @returns {void}
1507+
*/
1508+
setCleanupLogFile(filePath: string): void;
1509+
```
1510+
1511+
* Usage
1512+
```JavaScript
1513+
const tns = require("nativescript");
1514+
tns.cleanupService.setCleanupLogFile("/Users/username/cleanup-logs.txt");
1515+
```
1516+
14901517
## How to add a new method to Public API
14911518
CLI is designed as command line tool and when it is used as a library, it does not give you access to all of the methods. This is mainly implementation detail. Most of the CLI's code is created to work in command line, not as a library, so before adding method to public API, most probably it will require some modification.
14921519
For example the `$options` injected module contains information about all `--` options passed on the terminal. When the CLI is used as a library, the options are not populated. Before adding method to public API, make sure its implementation does not rely on `$options`.

lib/bootstrap.ts

+1
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/commands/debug.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,16 @@ export class DebugIOSCommand implements ICommand {
9191
private $sysInfo: ISysInfo,
9292
private $projectData: IProjectData,
9393
$iosDeviceOperations: IIOSDeviceOperations,
94-
$iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider) {
94+
$iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider,
95+
$cleanupService: ICleanupService) {
9596
this.$projectData.initializeProjectData();
9697
// Do not dispose ios-device-lib, so the process will remain alive and the debug application (NativeScript Inspector or Chrome DevTools) will be able to connect to the socket.
9798
// In case we dispose ios-device-lib, the socket will be closed and the code will fail when the debug application tries to read/send data to device socket.
9899
// That's why the `$ tns debug ios --justlaunch` command will not release the terminal.
99100
// In case we do not set it to false, the dispose will be called once the command finishes its execution, which will prevent the debugging.
100101
$iosDeviceOperations.setShouldDispose(false);
101102
$iOSSimulatorLogProvider.setShouldDispose(false);
103+
$cleanupService.setShouldDispose(false);
102104
}
103105

104106
public execute(args: string[]): Promise<void> {

lib/commands/preview.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ export class PreviewCommand implements ICommand {
1313
private $projectData: IProjectData,
1414
private $options: IOptions,
1515
private $previewAppLogProvider: IPreviewAppLogProvider,
16-
private $previewQrCodeService: IPreviewQrCodeService) {
17-
this.$analyticsService.setShouldDispose(this.$options.justlaunch || !this.$options.watch);
16+
private $previewQrCodeService: IPreviewQrCodeService,
17+
$cleanupService: ICleanupService) {
18+
this.$analyticsService.setShouldDispose(false);
19+
$cleanupService.setShouldDispose(false);
1820
}
1921

2022
public async execute(): Promise<void> {

lib/commands/test.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ abstract class TestCommandBase {
1010
protected abstract $options: IOptions;
1111
protected abstract $platformEnvironmentRequirements: IPlatformEnvironmentRequirements;
1212
protected abstract $errors: IErrors;
13+
protected abstract $cleanupService: ICleanupService;
1314

1415
async execute(args: string[]): Promise<void> {
1516
await this.$testExecutionService.startKarmaServer(this.platform, this.$projectData, this.projectFilesConfig);
@@ -18,6 +19,7 @@ abstract class TestCommandBase {
1819
async canExecute(args: string[]): Promise<boolean | ICanExecuteCommandOutput> {
1920
this.$projectData.initializeProjectData();
2021
this.$analyticsService.setShouldDispose(this.$options.justlaunch || !this.$options.watch);
22+
this.$cleanupService.setShouldDispose(this.$options.justlaunch || !this.$options.watch);
2123
this.projectFilesConfig = helpers.getProjectFilesConfig({ isReleaseBuild: this.$options.release });
2224

2325
const output = await this.$platformEnvironmentRequirements.checkEnvironmentRequirements({
@@ -51,7 +53,8 @@ class TestAndroidCommand extends TestCommandBase implements ICommand {
5153
protected $analyticsService: IAnalyticsService,
5254
protected $options: IOptions,
5355
protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements,
54-
protected $errors: IErrors) {
56+
protected $errors: IErrors,
57+
protected $cleanupService: ICleanupService) {
5558
super();
5659
}
5760

@@ -65,7 +68,8 @@ class TestIosCommand extends TestCommandBase implements ICommand {
6568
protected $analyticsService: IAnalyticsService,
6669
protected $options: IOptions,
6770
protected $platformEnvironmentRequirements: IPlatformEnvironmentRequirements,
68-
protected $errors: IErrors) {
71+
protected $errors: IErrors,
72+
protected $cleanupService: ICleanupService) {
6973
super();
7074
}
7175

lib/common/bootstrap.ts

-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ $injector.require("errors", "./errors");
66
$injector.requirePublic("fs", "./file-system");
77
$injector.require("hostInfo", "./host-info");
88
$injector.require("osInfo", "./os-info");
9-
$injector.require("timers", "./timers");
109

1110
$injector.require("dispatcher", "./dispatchers");
1211
$injector.require("commandDispatcher", "./dispatchers");
@@ -85,7 +84,6 @@ $injector.require("iOSEmulatorServices", "./mobile/ios/simulator/ios-emulator-se
8584
$injector.require("wp8EmulatorServices", "./mobile/wp8/wp8-emulator-services");
8685

8786
$injector.require("autoCompletionService", "./services/auto-completion-service");
88-
$injector.require("processService", "./services/process-service");
8987
$injector.requirePublic("settingsService", "./services/settings-service");
9088
$injector.require("opener", "./opener");
9189
$injector.require("microTemplateService", "./services/micro-templating-service");

lib/common/child-process.ts

+24-5
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,15 @@ export class ChildProcess extends EventEmitter implements IChildProcess {
6262
let isResolved = false;
6363
let capturedOut = "";
6464
let capturedErr = "";
65-
65+
let killTimer: NodeJS.Timer = null;
66+
67+
if (spawnFromEventOptions && spawnFromEventOptions.timeout) {
68+
this.$logger.trace(`Setting maximum time for execution of current child process to ${spawnFromEventOptions.timeout}`);
69+
killTimer = setTimeout(() => {
70+
this.$logger.trace(`Sending SIGTERM to current child process as maximum time for execution ${spawnFromEventOptions.timeout} had passed.`);
71+
childProcess.kill('SIGTERM');
72+
}, spawnFromEventOptions.timeout);
73+
}
6674
if (childProcess.stdout) {
6775
childProcess.stdout.on("data", (data: string) => {
6876
if (spawnFromEventOptions && spawnFromEventOptions.emitOptions && spawnFromEventOptions.emitOptions.eventName) {
@@ -91,17 +99,27 @@ export class ChildProcess extends EventEmitter implements IChildProcess {
9199
exitCode: exitCode
92100
};
93101

102+
const clearKillTimer = () => {
103+
if (killTimer) {
104+
clearTimeout(killTimer);
105+
}
106+
};
107+
108+
const resolveAction = () => {
109+
isResolved = true;
110+
resolve(result);
111+
clearKillTimer();
112+
};
113+
94114
if (spawnFromEventOptions && spawnFromEventOptions.throwError === false) {
95115
if (!isResolved) {
96116
this.$logger.trace("Result when throw error is false:");
97117
this.$logger.trace(result);
98-
isResolved = true;
99-
resolve(result);
118+
resolveAction();
100119
}
101120
} else {
102121
if (exitCode === 0) {
103-
isResolved = true;
104-
resolve(result);
122+
resolveAction();
105123
} else {
106124
let errorMessage = `Command ${command} failed with exit code ${exitCode}`;
107125
if (capturedErr) {
@@ -111,6 +129,7 @@ export class ChildProcess extends EventEmitter implements IChildProcess {
111129
if (!isResolved) {
112130
isResolved = true;
113131
reject(new Error(errorMessage));
132+
clearKillTimer();
114133
}
115134
}
116135
}

lib/common/commands/device/device-log-stream.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ export class OpenDeviceLogStreamCommand implements ICommand {
77
private $options: IOptions,
88
private $deviceLogProvider: Mobile.IDeviceLogProvider,
99
private $loggingLevels: Mobile.ILoggingLevels,
10-
$iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider) {
10+
$iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider,
11+
$cleanupService: ICleanupService) {
1112
$iOSSimulatorLogProvider.setShouldDispose(false);
13+
$cleanupService.setShouldDispose(false);
1214
}
1315

1416
allowedParameters: ICommandParameter[] = [];

lib/common/declarations.d.ts

+2-11
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,6 @@ declare const enum TrackingTypes {
207207
* Defines that the broker process should get and track the data from preview app to Google Analytics
208208
*/
209209
PreviewAppData = "PreviewAppData",
210-
211-
/**
212-
* Defines that all information has been sent and no more data will be tracked in current session.
213-
*/
214-
Finish = "finish"
215210
}
216211

217212
/**
@@ -653,7 +648,8 @@ interface ISpawnFromEventOptions {
653648
throwError: boolean;
654649
emitOptions?: {
655650
eventName: string;
656-
}
651+
},
652+
timeout?: number;
657653
}
658654

659655
interface IProjectDir {
@@ -1471,11 +1467,6 @@ interface INet {
14711467
waitForPortToListen(waitForPortListenData: IWaitForPortListenData): Promise<boolean>;
14721468
}
14731469

1474-
interface IProcessService {
1475-
listenersCount: number;
1476-
attachToProcessExitSignals(context: any, callback: () => void): void;
1477-
}
1478-
14791470
interface IDependencyInformation {
14801471
name: string;
14811472
version?: string;

lib/common/definitions/timers.d.ts

-4
This file was deleted.

lib/common/http-client.ts

+1-10
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,11 @@ export class HttpClient implements Server.IHttpClient {
1515
// We receive multiple response packets every ms but we don't need to be very aggressive here.
1616
private static STUCK_RESPONSE_CHECK_INTERVAL = 10000;
1717

18-
private defaultUserAgent: string;
19-
private cleanupData: ICleanupRequestData[];
18+
private defaultUserAgent: string;
2019

2120
constructor(private $logger: ILogger,
22-
private $processService: IProcessService,
2321
private $proxyService: IProxyService,
2422
private $staticConfig: Config.IStaticConfig) {
25-
this.cleanupData = [];
26-
this.$processService.attachToProcessExitSignals(this, () => {
27-
this.cleanupData.forEach(d => {
28-
this.cleanupAfterRequest(d);
29-
});
30-
});
3123
}
3224

3325
public async httpRequest(options: any, proxySettings?: IProxySettings): Promise<Server.IResponse> {
@@ -107,7 +99,6 @@ export class HttpClient implements Server.IHttpClient {
10799
const result = new Promise<Server.IResponse>((resolve, reject) => {
108100
let timerId: NodeJS.Timer;
109101
const cleanupRequestData: ICleanupRequestData = Object.create({ timers: [] });
110-
this.cleanupData.push(cleanupRequestData);
111102

112103
const promiseActions: IPromiseActions<Server.IResponse> = {
113104
resolve,

lib/common/mobile/android/logcat-helper.ts

-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export class LogcatHelper implements Mobile.ILogcatHelper {
1616
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
1717
private $logger: ILogger,
1818
private $injector: IInjector,
19-
private $processService: IProcessService,
2019
private $devicesService: Mobile.IDevicesService) {
2120
this.mapDevicesLoggingData = Object.create(null);
2221
}
@@ -53,8 +52,6 @@ export class LogcatHelper implements Mobile.ILogcatHelper {
5352
const lineText = line.toString();
5453
this.$deviceLogProvider.logData(lineText, this.$devicePlatformsConstants.Android, deviceIdentifier);
5554
});
56-
57-
this.$processService.attachToProcessExitSignals(this, logcatStream.kill);
5855
}
5956
}
6057

@@ -72,8 +69,6 @@ export class LogcatHelper implements Mobile.ILogcatHelper {
7269
logcatDumpStream.removeAllListeners();
7370
lineStream.removeAllListeners();
7471
});
75-
76-
this.$processService.attachToProcessExitSignals(this, logcatDumpStream.kill);
7772
}
7873

7974
/**

lib/common/mobile/ios/device/ios-device-operations.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,11 @@ export class IOSDeviceOperations extends EventEmitter implements IIOSDeviceOpera
99
public shouldDispose: boolean;
1010
private deviceLib: IOSDeviceLib.IOSDeviceLib;
1111

12-
constructor(private $logger: ILogger,
13-
private $processService: IProcessService) {
12+
constructor(private $logger: ILogger) {
1413
super();
1514

1615
this.isInitialized = false;
1716
this.shouldDispose = true;
18-
this.$processService.attachToProcessExitSignals(this, () => {
19-
this.setShouldDispose(true);
20-
this.dispose();
21-
});
2217
}
2318

2419
public async install(ipaPath: string, deviceIdentifiers: string[], errorHandler: DeviceOperationErrorHandler): Promise<IOSDeviceResponse> {

lib/common/mobile/ios/device/ios-device.ts

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export class IOSDevice extends IOSDeviceBase {
2020
protected $deviceLogProvider: Mobile.IDeviceLogProvider,
2121
protected $lockService: ILockService,
2222
private $iOSSocketRequestExecutor: IiOSSocketRequestExecutor,
23-
protected $processService: IProcessService,
2423
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
2524
private $iOSDeviceProductNameMapper: Mobile.IiOSDeviceProductNameMapper,
2625
private $iosDeviceOperations: IIOSDeviceOperations,

lib/common/mobile/ios/ios-device-base.ts

-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export abstract class IOSDeviceBase implements Mobile.IiOSDevice {
66
protected abstract $errors: IErrors;
77
protected abstract $deviceLogProvider: Mobile.IDeviceLogProvider;
88
protected abstract $iOSDebuggerPortService: IIOSDebuggerPortService;
9-
protected abstract $processService: IProcessService;
109
protected abstract $lockService: ILockService;
1110
abstract deviceInfo: Mobile.IDeviceInfo;
1211
abstract applicationManager: Mobile.IDeviceApplicationManager;
@@ -33,8 +32,6 @@ export abstract class IOSDeviceBase implements Mobile.IiOSDevice {
3332
this.cachedSockets[appId].on("close", async () => {
3433
await this.destroyDebugSocket(appId);
3534
});
36-
37-
this.$processService.attachToProcessExitSignals(this, () => this.destroyDebugSocket(appId));
3835
}
3936

4037
return this.cachedSockets[appId];

lib/common/mobile/ios/simulator/ios-simulator-application-manager.ts

-8
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,10 @@ export class IOSSimulatorApplicationManager extends ApplicationManagerBase {
1515
private device: Mobile.IiOSDevice,
1616
private $options: IOptions,
1717
private $fs: IFileSystem,
18-
private $processService: IProcessService,
1918
private $deviceLogProvider: Mobile.IDeviceLogProvider,
2019
$logger: ILogger,
2120
$hooksService: IHooksService) {
2221
super($logger, $hooksService);
23-
this.$processService.attachToProcessExitSignals(this, () => {
24-
for (const appId in this._lldbProcesses) {
25-
/* tslint:disable:no-floating-promises */
26-
this.detachNativeDebugger(appId);
27-
/* tslint:enable:no-floating-promises */
28-
}
29-
});
3022
}
3123

3224
public async getInstalledApplications(): Promise<string[]> {

lib/common/mobile/ios/simulator/ios-simulator-device.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ export class IOSSimulator extends IOSDeviceBase implements Mobile.IiOSDevice {
2222
private $iOSEmulatorServices: Mobile.IiOSSimulatorService,
2323
private $iOSNotification: IiOSNotification,
2424
private $iOSSimulatorLogProvider: Mobile.IiOSSimulatorLogProvider,
25-
private $logger: ILogger,
26-
protected $processService: IProcessService) {
25+
private $logger: ILogger) {
2726
super();
2827
this.applicationManager = this.$injector.resolve(applicationManagerPath.IOSSimulatorApplicationManager, { iosSim: this.$iOSSimResolver.iOSSim, device: this });
2928
this.fileSystem = this.$injector.resolve(fileSystemPath.IOSSimulatorFileSystem, { iosSim: this.$iOSSimResolver.iOSSim });

lib/common/mobile/ios/simulator/ios-simulator-log-provider.ts

-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export class IOSSimulatorLogProvider extends EventEmitter implements Mobile.IiOS
88

99
constructor(private $iOSSimResolver: Mobile.IiOSSimResolver,
1010
private $logger: ILogger,
11-
private $processService: IProcessService,
1211
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
1312
private $deviceLogProvider: Mobile.IDeviceLogProvider) {
1413
super();
@@ -47,8 +46,6 @@ export class IOSSimulatorLogProvider extends EventEmitter implements Mobile.IiOS
4746
deviceLogChildProcess.stderr.on("data", action.bind(this));
4847
}
4948

50-
this.$processService.attachToProcessExitSignals(this, deviceLogChildProcess.kill);
51-
5249
this.simulatorsLoggingEnabled[deviceId] = true;
5350
this.simulatorsLogProcess[deviceId] = deviceLogChildProcess;
5451
}

0 commit comments

Comments
 (0)