Skip to content

Commit e8e126a

Browse files
author
Dimitar Tachev
authored
Merge pull request #4114 from NativeScript/tachev/debug-without-restart
Allow `tns debug` with Hot Module Replacement (HMR)
2 parents 0dd8819 + 4bd9408 commit e8e126a

32 files changed

+626
-566
lines changed

lib/bootstrap.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ $injector.require("preparePlatformNativeService", "./services/prepare-platform-n
2929

3030
$injector.require("debugDataService", "./services/debug-data-service");
3131
$injector.requirePublicClass("debugService", "./services/debug-service");
32-
$injector.require("iOSDebugService", "./services/ios-debug-service");
33-
$injector.require("androidDebugService", "./services/android-debug-service");
32+
$injector.require("iOSDeviceDebugService", "./services/ios-device-debug-service");
33+
$injector.require("androidDeviceDebugService", "./services/android-device-debug-service");
3434

3535
$injector.require("userSettingsService", "./services/user-settings-service");
3636
$injector.requirePublic("analyticsSettingsService", "./services/analytics-settings-service");
@@ -146,7 +146,7 @@ $injector.requirePublic("previewQrCodeService", "./services/livesync/playground/
146146
$injector.requirePublic("sysInfo", "./sys-info");
147147

148148
$injector.require("iOSNotificationService", "./services/ios-notification-service");
149-
$injector.require("socketProxyFactory", "./device-sockets/ios/socket-proxy-factory");
149+
$injector.require("appDebugSocketProxyFactory", "./device-sockets/ios/app-debug-socket-proxy-factory");
150150
$injector.require("iOSNotification", "./device-sockets/ios/notification");
151151
$injector.require("iOSSocketRequestExecutor", "./device-sockets/ios/socket-request-executor");
152152
$injector.require("messages", "./common/messages/messages");

lib/commands/debug.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { isInteractive } from "../common/helpers";
33
import { cache } from "../common/decorators";
44
import { DebugCommandErrors } from "../constants";
55
import { ValidatePlatformCommandBase } from "./command-base";
6+
import { LiveSyncCommandHelper } from "../helpers/livesync-command-helper";
67

78
export class DebugPlatformCommand extends ValidatePlatformCommandBase implements ICommand {
89
public allowedParameters: ICommandParameter[] = [];
910

1011
constructor(private platform: string,
12+
private $bundleValidatorHelper: IBundleValidatorHelper,
1113
private $debugService: IDebugService,
1214
protected $devicesService: Mobile.IDevicesService,
1315
$platformService: IPlatformService,
@@ -21,7 +23,7 @@ export class DebugPlatformCommand extends ValidatePlatformCommandBase implements
2123
private $prompter: IPrompter,
2224
private $liveSyncCommandHelper: ILiveSyncCommandHelper,
2325
private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper) {
24-
super($options, $platformsData, $platformService, $projectData);
26+
super($options, $platformsData, $platformService, $projectData);
2527
}
2628

2729
public async execute(args: string[]): Promise<void> {
@@ -36,7 +38,7 @@ export class DebugPlatformCommand extends ValidatePlatformCommandBase implements
3638

3739
const selectedDeviceForDebug = await this.getDeviceForDebug();
3840

39-
const debugData = this.$debugDataService.createDebugData(this.$projectData, {device: selectedDeviceForDebug.deviceInfo.identifier});
41+
const debugData = this.$debugDataService.createDebugData(this.$projectData, { device: selectedDeviceForDebug.deviceInfo.identifier });
4042

4143
if (this.$options.start) {
4244
await this.$liveSyncService.printDebugInformation(await this.$debugService.debug(debugData, debugOptions));
@@ -118,7 +120,14 @@ export class DebugPlatformCommand extends ValidatePlatformCommandBase implements
118120
this.$errors.fail("--release flag is not applicable to this command");
119121
}
120122

121-
const result = await super.canExecuteCommandBase(this.platform, { validateOptions: true, notConfiguredEnvOptions: { hideCloudBuildOption: true }});
123+
if (this.$options.hmr && this.$options.debugBrk) {
124+
this.$errors.fail("--debug-brk and --hmr flags cannot be combined");
125+
}
126+
127+
const minSupportedWebpackVersion = this.$options.hmr ? LiveSyncCommandHelper.MIN_SUPPORTED_WEBPACK_VERSION_WITH_HMR : null;
128+
this.$bundleValidatorHelper.validate(minSupportedWebpackVersion);
129+
130+
const result = await super.canExecuteCommandBase(this.platform, { validateOptions: true, notConfiguredEnvOptions: { hideCloudBuildOption: true, hideSyncToPreviewAppOption: true } });
122131
return result;
123132
}
124133
}
@@ -209,7 +218,7 @@ export class DebugAndroidCommand implements ICommand {
209218
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
210219
private $injector: IInjector,
211220
private $projectData: IProjectData) {
212-
this.$projectData.initializeProjectData();
221+
this.$projectData.initializeProjectData();
213222
}
214223

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

lib/common/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,7 @@ export class AndroidVirtualDevice {
156156
static GENYMOTION_DEFAULT_STDERR_STRING = "Logging activities to file";
157157

158158
static UNABLE_TO_START_EMULATOR_MESSAGE = "Cannot run the app in the selected native emulator. Try to restart the adb server by running the `adb kill-server` command in the Command Prompt, or increase the allocated RAM of the virtual device through the Android Virtual Device manager. NativeScript CLI users can try to increase the timeout of the operation by adding the `--timeout` flag.";
159+
159160
}
161+
162+
export const SOCKET_CONNECTION_TIMEOUT_MS = 30000;

lib/common/definitions/mobile.d.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,14 @@ declare module Mobile {
111111
}
112112

113113
interface IiOSDevice extends IDevice {
114-
connectToPort(port: number): Promise<any>;
114+
getLiveSyncSocket(appId: string): Promise<any>;
115+
destroyLiveSyncSocket(appId: string): void;
116+
117+
getDebugSocket(appId: string): Promise<any>;
118+
destroyDebugSocket(appId: string): void;
119+
115120
openDeviceLogStream(options?: IiOSLogStreamOptions): Promise<void>;
121+
destroyAllSockets(): void;
116122
}
117123

118124
interface IAndroidDevice extends IDevice {
@@ -125,8 +131,6 @@ declare module Mobile {
125131
getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService;
126132
}
127133

128-
interface IiOSSimulator extends IDevice { }
129-
130134
/**
131135
* Describes log stream options
132136
*/
Lines changed: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,41 @@
11
import * as applicationManagerPath from "./ios-application-manager";
22
import * as fileSystemPath from "./ios-device-file-system";
3-
import * as constants from "../../../constants";
3+
import * as commonConstants from "../../../constants";
4+
import * as constants from "../../../../constants";
45
import * as net from "net";
56
import { cache } from "../../../decorators";
7+
import * as helpers from "../../../../common/helpers";
8+
import { IOSDeviceBase } from "../ios-device-base";
69

7-
export class IOSDevice implements Mobile.IiOSDevice {
10+
export class IOSDevice extends IOSDeviceBase {
811
public applicationManager: Mobile.IDeviceApplicationManager;
912
public fileSystem: Mobile.IDeviceFileSystem;
1013
public deviceInfo: Mobile.IDeviceInfo;
11-
12-
private _socket: net.Socket;
1314
private _deviceLogHandler: (...args: any[]) => void;
1415

1516
constructor(private deviceActionInfo: IOSDeviceLib.IDeviceActionInfo,
17+
protected $errors: IErrors,
1618
private $injector: IInjector,
17-
private $processService: IProcessService,
19+
protected $iOSDebuggerPortService: IIOSDebuggerPortService,
20+
private $iOSSocketRequestExecutor: IiOSSocketRequestExecutor,
21+
protected $processService: IProcessService,
1822
private $deviceLogProvider: Mobile.IDeviceLogProvider,
1923
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
2024
private $iOSDeviceProductNameMapper: Mobile.IiOSDeviceProductNameMapper,
2125
private $iosDeviceOperations: IIOSDeviceOperations,
2226
private $mobileHelper: Mobile.IMobileHelper) {
23-
27+
super();
2428
this.applicationManager = this.$injector.resolve(applicationManagerPath.IOSApplicationManager, { device: this, devicePointer: this.deviceActionInfo });
2529
this.fileSystem = this.$injector.resolve(fileSystemPath.IOSDeviceFileSystem, { device: this, devicePointer: this.deviceActionInfo });
26-
2730
const productType = deviceActionInfo.productType;
2831
const isTablet = this.$mobileHelper.isiOSTablet(productType);
29-
const deviceStatus = deviceActionInfo.status || constants.UNREACHABLE_STATUS;
32+
const deviceStatus = deviceActionInfo.status || commonConstants.UNREACHABLE_STATUS;
3033
this.deviceInfo = {
3134
identifier: deviceActionInfo.deviceId,
3235
vendor: "Apple",
3336
platform: this.$devicePlatformsConstants.iOS,
3437
status: deviceStatus,
35-
errorHelp: deviceStatus === constants.UNREACHABLE_STATUS ? `Device ${deviceActionInfo.deviceId} is ${constants.UNREACHABLE_STATUS}` : null,
38+
errorHelp: deviceStatus === commonConstants.UNREACHABLE_STATUS ? `Device ${deviceActionInfo.deviceId} is ${commonConstants.UNREACHABLE_STATUS}` : null,
3639
type: "Device",
3740
isTablet: isTablet,
3841
displayName: this.$iOSDeviceProductNameMapper.resolveProductName(deviceActionInfo.deviceName) || deviceActionInfo.deviceName,
@@ -47,8 +50,29 @@ export class IOSDevice implements Mobile.IiOSDevice {
4750
return false;
4851
}
4952

50-
public getApplicationInfo(applicationIdentifier: string): Promise<Mobile.IApplicationInfo> {
51-
return this.applicationManager.getApplicationInfo(applicationIdentifier);
53+
@cache()
54+
public async openDeviceLogStream(): Promise<void> {
55+
if (this.deviceInfo.status !== commonConstants.UNREACHABLE_STATUS) {
56+
this._deviceLogHandler = this.actionOnDeviceLog.bind(this);
57+
this.$iosDeviceOperations.on(commonConstants.DEVICE_LOG_EVENT_NAME, this._deviceLogHandler);
58+
this.$iosDeviceOperations.startDeviceLog(this.deviceInfo.identifier);
59+
}
60+
}
61+
62+
protected async getSocketCore(appId: string): Promise<net.Socket> {
63+
await this.$iOSSocketRequestExecutor.executeAttachRequest(this, constants.AWAIT_NOTIFICATION_TIMEOUT_SECONDS, appId);
64+
const port = await this.getDebuggerPort(appId);
65+
const deviceId = this.deviceInfo.identifier;
66+
const socket = await helpers.connectEventuallyUntilTimeout(
67+
async () => {
68+
const deviceResponse = _.first((await this.$iosDeviceOperations.connectToPort([{ deviceId: deviceId, port: port }]))[deviceId]);
69+
const _socket = new net.Socket();
70+
_socket.connect(deviceResponse.port, deviceResponse.host);
71+
return _socket;
72+
},
73+
commonConstants.SOCKET_CONNECTION_TIMEOUT_MS);
74+
75+
return socket;
5276
}
5377

5478
private actionOnDeviceLog(response: IOSDeviceLib.IDeviceLogData): void {
@@ -57,34 +81,12 @@ export class IOSDevice implements Mobile.IiOSDevice {
5781
}
5882
}
5983

60-
@cache()
61-
public async openDeviceLogStream(): Promise<void> {
62-
if (this.deviceInfo.status !== constants.UNREACHABLE_STATUS) {
63-
this._deviceLogHandler = this.actionOnDeviceLog.bind(this);
64-
this.$iosDeviceOperations.on(constants.DEVICE_LOG_EVENT_NAME, this._deviceLogHandler);
65-
this.$iosDeviceOperations.startDeviceLog(this.deviceInfo.identifier);
66-
}
67-
}
68-
6984
public detach(): void {
7085
if (this._deviceLogHandler) {
71-
this.$iosDeviceOperations.removeListener(constants.DEVICE_LOG_EVENT_NAME, this._deviceLogHandler);
86+
this.$iosDeviceOperations.removeListener(commonConstants.DEVICE_LOG_EVENT_NAME, this._deviceLogHandler);
7287
}
7388
}
7489

75-
// This function works only on OSX
76-
public async connectToPort(port: number): Promise<net.Socket> {
77-
const deviceId = this.deviceInfo.identifier;
78-
const deviceResponse = _.first((await this.$iosDeviceOperations.connectToPort([{ deviceId: deviceId, port: port }]))[deviceId]);
79-
80-
const socket = new net.Socket();
81-
socket.connect(deviceResponse.port, deviceResponse.host);
82-
this._socket = socket;
83-
84-
this.$processService.attachToProcessExitSignals(this, this.destroySocket);
85-
return this._socket;
86-
}
87-
8890
private getActiveArchitecture(productType: string): string {
8991
let activeArchitecture = "";
9092
if (productType) {
@@ -106,13 +108,6 @@ export class IOSDevice implements Mobile.IiOSDevice {
106108

107109
return activeArchitecture;
108110
}
109-
110-
private destroySocket() {
111-
if (this._socket) {
112-
this._socket.destroy();
113-
this._socket = null;
114-
}
115-
}
116111
}
117112

118113
$injector.register("iOSDevice", IOSDevice);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import * as net from "net";
2+
3+
export abstract class IOSDeviceBase implements Mobile.IiOSDevice {
4+
private cachedSockets: IDictionary<net.Socket> = {};
5+
protected abstract $errors: IErrors;
6+
protected abstract $iOSDebuggerPortService: IIOSDebuggerPortService;
7+
protected abstract $processService: IProcessService;
8+
abstract deviceInfo: Mobile.IDeviceInfo;
9+
abstract applicationManager: Mobile.IDeviceApplicationManager;
10+
abstract fileSystem: Mobile.IDeviceFileSystem;
11+
abstract isEmulator: boolean;
12+
abstract openDeviceLogStream(): Promise<void>;
13+
14+
public getApplicationInfo(applicationIdentifier: string): Promise<Mobile.IApplicationInfo> {
15+
return this.applicationManager.getApplicationInfo(applicationIdentifier);
16+
}
17+
18+
public async getLiveSyncSocket(appId: string): Promise<net.Socket> {
19+
return this.getSocket(appId);
20+
}
21+
22+
public async getDebugSocket(appId: string): Promise<net.Socket> {
23+
return this.getSocket(appId);
24+
}
25+
26+
public async getSocket(appId: string): Promise<net.Socket> {
27+
if (this.cachedSockets[appId]) {
28+
return this.cachedSockets[appId];
29+
}
30+
31+
this.cachedSockets[appId] = await this.getSocketCore(appId);
32+
33+
if (this.cachedSockets[appId]) {
34+
this.cachedSockets[appId].on("close", () => {
35+
this.destroySocket(appId);
36+
});
37+
38+
this.$processService.attachToProcessExitSignals(this, () => this.destroySocket(appId));
39+
}
40+
41+
return this.cachedSockets[appId];
42+
}
43+
44+
public destroyLiveSyncSocket(appId: string) {
45+
this.destroySocket(appId);
46+
}
47+
48+
public destroyDebugSocket(appId: string) {
49+
this.destroySocket(appId);
50+
}
51+
52+
protected abstract async getSocketCore(appId: string): Promise<net.Socket>;
53+
54+
protected async getDebuggerPort(appId: string): Promise<number> {
55+
const port = await this.$iOSDebuggerPortService.getPort({ deviceId: this.deviceInfo.identifier, appId });
56+
if (!port) {
57+
this.$errors.failWithoutHelp("Device socket port cannot be found.");
58+
}
59+
60+
return port;
61+
}
62+
63+
public destroyAllSockets() {
64+
for (const appId in this.cachedSockets) {
65+
this.destroySocketSafe(this.cachedSockets[appId]);
66+
}
67+
68+
this.cachedSockets = {};
69+
}
70+
71+
private destroySocket(appId: string) {
72+
this.destroySocketSafe(this.cachedSockets[appId]);
73+
this.cachedSockets[appId] = null;
74+
}
75+
76+
private destroySocketSafe(socket: net.Socket) {
77+
if (socket && !socket.destroyed) {
78+
socket.destroy();
79+
}
80+
}
81+
}

lib/common/mobile/ios/simulator/ios-emulator-services.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import * as net from "net";
2-
import { connectEventuallyUntilTimeout } from "../../../helpers";
32
import { APPLE_VENDOR_NAME, DeviceTypes, RUNNING_EMULATOR_STATUS, NOT_RUNNING_EMULATOR_STATUS } from "../../../constants";
43

54
class IosEmulatorServices implements Mobile.IiOSSimulatorService {
6-
private static DEFAULT_TIMEOUT = 10000;
7-
85
constructor(private $logger: ILogger,
96
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
107
private $iOSSimResolver: Mobile.IiOSSimResolver,
@@ -72,7 +69,7 @@ class IosEmulatorServices implements Mobile.IiOSSimulatorService {
7269

7370
public async connectToPort(data: Mobile.IConnectToPortData): Promise<net.Socket> {
7471
try {
75-
const socket = await connectEventuallyUntilTimeout(async () => net.connect(data.port), data.timeout || IosEmulatorServices.DEFAULT_TIMEOUT);
72+
const socket = net.connect(data.port);
7673
return socket;
7774
} catch (e) {
7875
this.$logger.debug(e);
@@ -85,7 +82,7 @@ class IosEmulatorServices implements Mobile.IiOSSimulatorService {
8582

8683
const output = await this.tryGetiOSSimDevices();
8784
if (output.devices && output.devices.length) {
88-
devices = _(output.devices)
85+
devices = _(output.devices)
8986
.map(simDevice => this.convertSimDeviceToDeviceInfo(simDevice))
9087
.sortBy(deviceInfo => deviceInfo.version)
9188
.value();
@@ -102,7 +99,7 @@ class IosEmulatorServices implements Mobile.IiOSSimulatorService {
10299
return [];
103100
}
104101

105-
private async tryGetiOSSimDevices(): Promise<{devices: Mobile.IiSimDevice[], error: string}> {
102+
private async tryGetiOSSimDevices(): Promise<{ devices: Mobile.IiSimDevice[], error: string }> {
106103
let devices: Mobile.IiSimDevice[] = [];
107104
let error: string = null;
108105

0 commit comments

Comments
 (0)