Skip to content

Commit 5f6243b

Browse files
authored
Merge pull request #3980 from NativeScript/kddimitrov/restart-on-hmr-fail
Kddimitrov/restart on hmr fail
2 parents 47f07d5 + 4e94cb1 commit 5f6243b

16 files changed

+204
-32
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ $injector.requirePublic("assetsGenerationService", "./services/assets-generation
178178
$injector.require("filesHashService", "./services/files-hash-service");
179179
$injector.require("logParserService", "./services/log-parser-service");
180180
$injector.require("iOSDebuggerPortService", "./services/ios-debugger-port-service");
181+
$injector.require("hmrStatusService", "./services/hmr-status-service");
181182

182183
$injector.require("pacoteService", "./services/pacote-service");
183184
$injector.require("qrCodeTerminalService", "./services/qr-code-terminal-service");

lib/common/appbuilder/device-log-provider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class DeviceLogProvider extends DeviceLogProviderBase {
1616

1717
if (data) {
1818
this.emit('data', deviceIdentifier, data);
19-
this.emit(DEVICE_LOG_EVENT_NAME, data, deviceIdentifier);
19+
this.emit(DEVICE_LOG_EVENT_NAME, data, deviceIdentifier, platform);
2020
}
2121
}
2222

lib/common/constants.ts

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ export class LiveSyncConstants {
4848
static IOS_PROJECT_PATH = "/Documents/AppBuilder/LiveSync";
4949
}
5050

51+
export class HmrConstants {
52+
public static HMR_ERROR_STATUS = 3;
53+
public static HMR_SUCCESS_STATUS = 2;
54+
}
55+
5156
export class DeviceDiscoveryEventNames {
5257
static DEVICE_FOUND = "deviceFound";
5358
static DEVICE_LOST = "deviceLost";

lib/common/mobile/device-log-provider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class DeviceLogProvider extends DeviceLogProviderBase {
1212
const data = this.$logFilter.filterData(platform, lineText, loggingOptions);
1313
if (data) {
1414
this.$logger.write(data);
15-
this.emit(DEVICE_LOG_EVENT_NAME, lineText, deviceIdentifier);
15+
this.emit(DEVICE_LOG_EVENT_NAME, lineText, deviceIdentifier, platform);
1616
}
1717
}
1818

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface IHmrStatusService {
2+
getHmrStatus(deviceId: string, operationHash: string): Promise<number>;
3+
attachToHrmStatusEvent(): void;
4+
}

lib/definitions/preview-app-livesync.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { FilePayload, Device, FilesPayload } from "nativescript-preview-sdk";
2+
import { EventEmitter } from "events";
23

34
declare global {
45
interface IPreviewAppLiveSyncService {
@@ -9,7 +10,7 @@ declare global {
910

1011
interface IPreviewAppLiveSyncData extends IProjectDir, IAppFilesUpdaterOptionsComposition, IEnvOptions { }
1112

12-
interface IPreviewSdkService {
13+
interface IPreviewSdkService extends EventEmitter {
1314
getQrCodeUrl(options: IHasUseHotModuleReloadOption): string;
1415
connectedDevices: Device[];
1516
initialize(getInitialFiles: (device: Device) => Promise<FilesPayload>): void;

lib/services/hmr-status-service.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { cache } from "../common/decorators";
2+
import { HmrConstants } from "../common/constants";
3+
4+
export class HmrStatusService implements IHmrStatusService {
5+
public static HMR_STATUS_LOG_REGEX = /([a-z A-Z]*) hmr hash ([a-z0-9]*)\./;
6+
public static STARTED_MESSAGE = "Checking for updates to the bundle with";
7+
public static SUCCESS_MESSAGE = "Successfully applied update with";
8+
public static FAILED_MESSAGE = "Cannot apply update with";
9+
private hashOperationStatuses: IDictionary<any> = {};
10+
private intervals: IDictionary<any> = {};
11+
12+
constructor(private $logParserService: ILogParserService,
13+
private $processService: IProcessService,
14+
private $logger: ILogger) {
15+
this.$processService.attachToProcessExitSignals(this, this.dispose);
16+
}
17+
18+
public getHmrStatus(deviceId: string, operationHash: string): Promise<number> {
19+
return new Promise((resolve, reject) => {
20+
const key = `${deviceId}${operationHash}`;
21+
let retryCount = 40;
22+
23+
this.intervals[key] = setInterval(() => {
24+
const status = this.getStatusByKey(key);
25+
if (status || retryCount === 0) {
26+
clearInterval(this.intervals[key]);
27+
this.intervals[key] = null;
28+
resolve(status);
29+
} else {
30+
retryCount--;
31+
}
32+
}, 250);
33+
});
34+
}
35+
36+
@cache()
37+
public attachToHrmStatusEvent(): void {
38+
this.$logParserService.addParseRule({
39+
regex: HmrStatusService.HMR_STATUS_LOG_REGEX,
40+
handler: this.handleHmrStatusFound.bind(this),
41+
name: "hmrStatus"
42+
});
43+
}
44+
45+
private handleHmrStatusFound(matches: RegExpMatchArray, deviceId: string): void {
46+
const message = matches[1].trim();
47+
const hash = matches[2];
48+
let status;
49+
50+
switch (message) {
51+
case HmrStatusService.SUCCESS_MESSAGE: {
52+
status = HmrConstants.HMR_SUCCESS_STATUS;
53+
break;
54+
}
55+
case HmrStatusService.FAILED_MESSAGE: {
56+
status = HmrConstants.HMR_ERROR_STATUS;
57+
break;
58+
}
59+
default: {
60+
status = null;
61+
break;
62+
}
63+
}
64+
65+
this.$logger.trace("Found hmr status.", {status, hash});
66+
67+
if (status) {
68+
this.setData(status, hash, deviceId);
69+
}
70+
}
71+
72+
private getStatusByKey(key: string): number {
73+
if (this.hashOperationStatuses[key]) {
74+
return this.hashOperationStatuses[key].status;
75+
}
76+
77+
return null;
78+
}
79+
80+
private setData(status: Number, operationHash: string, deviceId: string): void {
81+
const key = `${deviceId}${operationHash}`;
82+
83+
if (!this.hashOperationStatuses[key]) {
84+
this.hashOperationStatuses[key] = <any>{};
85+
}
86+
87+
this.hashOperationStatuses[key].status = status;
88+
}
89+
90+
private dispose() {
91+
_.forEach(this.intervals, (value, key) => {
92+
clearInterval(value);
93+
this.intervals[key] = null;
94+
});
95+
}
96+
}
97+
98+
$injector.register("hmrStatusService", HmrStatusService);

lib/services/livesync/livesync-service.ts

+27-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { EOL } from "os";
44
import { EventEmitter } from "events";
55
import { hook } from "../../common/helpers";
66
import { PACKAGE_JSON_FILE_NAME, LiveSyncTrackActionNames, USER_INTERACTION_NEEDED_EVENT_NAME, DEBUGGER_ATTACHED_EVENT_NAME, DEBUGGER_DETACHED_EVENT_NAME, TrackActionNames } from "../../constants";
7-
import { DeviceTypes, DeviceDiscoveryEventNames } from "../../common/constants";
7+
import { DeviceTypes, DeviceDiscoveryEventNames, HmrConstants } from "../../common/constants";
88
import { cache } from "../../common/decorators";
99

1010
const deviceDescriptorPrimaryKey = "identifier";
@@ -38,6 +38,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
3838
private $analyticsService: IAnalyticsService,
3939
private $usbLiveSyncService: DeprecatedUsbLiveSyncService,
4040
private $previewAppLiveSyncService: IPreviewAppLiveSyncService,
41+
private $hmrStatusService: IHmrStatusService,
4142
private $injector: IInjector) {
4243
super();
4344
}
@@ -147,8 +148,6 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
147148
deviceIdentifier: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier,
148149
isFullSync: liveSyncResultInfo.isFullSync
149150
});
150-
151-
this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`);
152151
}
153152

154153
private async refreshApplicationWithDebug(projectData: IProjectData, liveSyncResultInfo: ILiveSyncResultInfo, debugOptions: IDebugOptions, outputPath?: string): Promise<IDebugInformation> {
@@ -518,6 +517,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
518517

519518
await this.$platformService.trackActionForPlatform({ action: "LiveSync", platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version });
520519
await this.refreshApplication(projectData, liveSyncResultInfo, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath);
520+
this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`);
521521

522522
this.emitLivesyncEvent(LiveSyncEvents.liveSyncStarted, {
523523
projectDir: projectData.projectDir,
@@ -564,6 +564,10 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
564564
const platforms = _(devices).map(device => device.deviceInfo.platform).uniq().value();
565565
const patterns = await this.getWatcherPatterns(liveSyncData, projectData, platforms);
566566

567+
if (liveSyncData.useHotModuleReload) {
568+
this.$hmrStatusService.attachToHrmStatusEvent();
569+
}
570+
567571
if (liveSyncData.watchAllFiles) {
568572
const productionDependencies = this.$nodeModulesDependenciesBuilder.getProductionDependencies(projectData.projectDir);
569573
patterns.push(PACKAGE_JSON_FILE_NAME);
@@ -582,6 +586,10 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
582586
}
583587

584588
let filesToSync: string[] = [];
589+
const hmrData: { hash: string; fallbackFiles: IDictionary<string[]> } = {
590+
hash: "",
591+
fallbackFiles: {}
592+
};
585593
const filesToSyncMap: IDictionary<string[]> = {};
586594
let filesToRemove: string[] = [];
587595
let timeoutTimer: NodeJS.Timer;
@@ -610,6 +618,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
610618
if (filesToSync.length || filesToRemove.length) {
611619
try {
612620
const currentFilesToSync = _.cloneDeep(filesToSync);
621+
const currentHmrData = _.cloneDeep(hmrData);
613622
filesToSync.splice(0, filesToSync.length);
614623

615624
const currentFilesToRemove = _.cloneDeep(filesToRemove);
@@ -655,8 +664,21 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
655664
force: liveSyncData.force
656665
};
657666

658-
const liveSyncResultInfo = await service.liveSyncWatchAction(device, settings);
667+
let liveSyncResultInfo = await service.liveSyncWatchAction(device, settings);
668+
659669
await this.refreshApplication(projectData, liveSyncResultInfo, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath);
670+
671+
if (liveSyncData.useHotModuleReload && currentHmrData.hash) {
672+
const status = await this.$hmrStatusService.getHmrStatus(device.deviceInfo.identifier, currentHmrData.hash);
673+
if (status === HmrConstants.HMR_ERROR_STATUS) {
674+
settings.filesToSync = currentHmrData.fallbackFiles[device.deviceInfo.platform];
675+
liveSyncResultInfo = await service.liveSyncWatchAction(device, settings);
676+
liveSyncResultInfo.isFullSync = true;
677+
await this.refreshApplication(projectData, liveSyncResultInfo, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath);
678+
}
679+
}
680+
681+
this.$logger.info(`Successfully synced application ${liveSyncResultInfo.deviceAppData.appIdentifier} on device ${liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier}.`);
660682
},
661683
(device: Mobile.IDevice) => {
662684
const liveSyncProcessInfo = this.liveSyncProcessesInfo[projectData.projectDir];
@@ -704,6 +726,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
704726
},
705727
filesToSync,
706728
filesToSyncMap,
729+
hmrData,
707730
filesToRemove,
708731
startSyncFilesTimeout: async (platform: string) => {
709732
if (platform) {

lib/services/livesync/playground/preview-app-livesync-service.ts

+30-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as path from "path";
22
import { FilePayload, Device, FilesPayload } from "nativescript-preview-sdk";
33
import { PreviewSdkEventNames } from "./preview-app-constants";
44
import { APP_FOLDER_NAME, APP_RESOURCES_FOLDER_NAME, TNS_MODULES_FOLDER_NAME } from "../../../constants";
5+
import { HmrConstants } from "../../../common/constants";
56
const isTextOrBinary = require('istextorbinary');
67

78
export class PreviewAppLiveSyncService implements IPreviewAppLiveSyncService {
@@ -19,6 +20,7 @@ export class PreviewAppLiveSyncService implements IPreviewAppLiveSyncService {
1920
private $previewSdkService: IPreviewSdkService,
2021
private $previewAppPluginsService: IPreviewAppPluginsService,
2122
private $projectFilesManager: IProjectFilesManager,
23+
private $hmrStatusService: IHmrStatusService,
2224
private $projectFilesProvider: IProjectFilesProvider) { }
2325

2426
public async initialize(data: IPreviewAppLiveSyncData): Promise<void> {
@@ -43,19 +45,38 @@ export class PreviewAppLiveSyncService implements IPreviewAppLiveSyncService {
4345

4446
private async initializePreviewForDevice(data: IPreviewAppLiveSyncData, device: Device): Promise<FilesPayload> {
4547
const filesToSyncMap: IDictionary<string[]> = {};
48+
const hmrData: { hash: string; fallbackFiles: IDictionary<string[]> } = {
49+
hash: "",
50+
fallbackFiles: {}
51+
};
4652
let promise = Promise.resolve<FilesPayload>(null);
4753
const startSyncFilesTimeout = async (platform: string) => {
4854
await promise
4955
.then(async () => {
5056
const projectData = this.$projectDataService.getProjectData(data.projectDir);
51-
promise = this.applyChanges(this.$platformsData.getPlatformData(platform, projectData), projectData, filesToSyncMap[platform]);
57+
const platformData = this.$platformsData.getPlatformData(platform, projectData);
58+
const currentHmrData = _.cloneDeep(hmrData);
59+
const filesToSync = _.cloneDeep(filesToSyncMap[platform]);
60+
promise = this.applyChanges(platformData, projectData, filesToSync, { useHotModuleReload: data.appFilesUpdaterOptions.useHotModuleReload});
5261
await promise;
62+
63+
if (data.appFilesUpdaterOptions.useHotModuleReload && currentHmrData.hash) {
64+
const devices = _.filter(this.$previewSdkService.connectedDevices, { platform: platform.toLowerCase() });
65+
66+
await Promise.all(_.map(devices, async (previewDevice: Device) => {
67+
const status = await this.$hmrStatusService.getHmrStatus(previewDevice.id, currentHmrData.hash);
68+
if (status === HmrConstants.HMR_ERROR_STATUS) {
69+
await this.applyChanges(platformData, projectData, currentHmrData.fallbackFiles[platform], { useHotModuleReload: false }, previewDevice.id);
70+
}
71+
}));
72+
}
5373
});
5474
filesToSyncMap[platform] = [];
5575
};
5676
await this.$hooksService.executeBeforeHooks("preview-sync", {
5777
hookArgs: {
5878
projectData: this.$projectDataService.getProjectData(data.projectDir),
79+
hmrData,
5980
config: {
6081
env: data.env,
6182
platform: device.platform,
@@ -104,10 +125,11 @@ export class PreviewAppLiveSyncService implements IPreviewAppLiveSyncService {
104125

105126
let result: FilesPayload = null;
106127
if (files && files.length) {
107-
result = await this.applyChanges(platformData, projectData, files);
128+
result = await this.applyChanges(platformData, projectData, files, { useHotModuleReload: data.appFilesUpdaterOptions.useHotModuleReload});
108129
this.$logger.info(`Successfully synced ${result.files.map(filePayload => filePayload.file.yellow)} for platform ${platform}.`);
109130
} else {
110-
result = await this.getFilesPayload(platformData, projectData);
131+
const hmrMode = data.appFilesUpdaterOptions.useHotModuleReload ? 1 : 0;
132+
result = await this.getFilesPayload(platformData, projectData, hmrMode);
111133
this.$logger.info(`Successfully synced changes for platform ${platform}.`);
112134
}
113135

@@ -117,14 +139,15 @@ export class PreviewAppLiveSyncService implements IPreviewAppLiveSyncService {
117139
}
118140
}
119141

120-
private async applyChanges(platformData: IPlatformData, projectData: IProjectData, files: string[]): Promise<FilesPayload> {
121-
const payloads = this.getFilesPayload(platformData, projectData, _(files).uniq().value());
142+
private async applyChanges(platformData: IPlatformData, projectData: IProjectData, files: string[], { useHotModuleReload }: {useHotModuleReload: Boolean}, deviceId?: string): Promise<FilesPayload> {
143+
const hmrMode = useHotModuleReload ? 1 : 0;
144+
const payloads = this.getFilesPayload(platformData, projectData, hmrMode, _(files).uniq().value(), deviceId);
122145
await this.$previewSdkService.applyChanges(payloads);
123146

124147
return payloads;
125148
}
126149

127-
private getFilesPayload(platformData: IPlatformData, projectData: IProjectData, files?: string[]): FilesPayload {
150+
private getFilesPayload(platformData: IPlatformData, projectData: IProjectData, hmrMode: number, files?: string[], deviceId?: string): FilesPayload {
128151
const platformsAppFolderPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME);
129152

130153
if (files && files.length) {
@@ -163,7 +186,7 @@ export class PreviewAppLiveSyncService implements IPreviewAppLiveSyncService {
163186
return filePayload;
164187
});
165188

166-
return { files: payloads, platform: platformData.normalizedPlatformName.toLowerCase() };
189+
return { files: payloads, platform: platformData.normalizedPlatformName.toLowerCase(), hmrMode, deviceId};
167190
}
168191

169192
private async preparePlatform(platform: string, appFilesUpdaterOptions: IAppFilesUpdaterOptions, env: Object, projectData: IProjectData): Promise<void> {

lib/services/livesync/playground/preview-sdk-service.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { MessagingService, Config, Device, DeviceConnectedMessage, SdkCallbacks, ConnectedDevices, FilesPayload } from "nativescript-preview-sdk";
22
import { PubnubKeys } from "./preview-app-constants";
3+
import { DEVICE_LOG_EVENT_NAME } from "../../../common/constants";
4+
import { EventEmitter } from "events";
35
const pako = require("pako");
46

5-
export class PreviewSdkService implements IPreviewSdkService {
7+
export class PreviewSdkService extends EventEmitter implements IPreviewSdkService {
68
private static MAX_FILES_UPLOAD_BYTE_LENGTH = 15 * 1024 * 1024; // In MBs
79
private messagingService: MessagingService = null;
810
private instanceId: string = null;
@@ -11,6 +13,7 @@ export class PreviewSdkService implements IPreviewSdkService {
1113
constructor(private $logger: ILogger,
1214
private $httpClient: Server.IHttpClient,
1315
private $config: IConfiguration) {
16+
super();
1417
}
1518

1619
public getQrCodeUrl(options: IHasUseHotModuleReloadOption): string {
@@ -58,7 +61,9 @@ export class PreviewSdkService implements IPreviewSdkService {
5861
this.$logger.trace("Received onLogSdkMessage message: ", log);
5962
},
6063
onConnectedDevicesChange: (connectedDevices: ConnectedDevices) => ({ }),
61-
onLogMessage: (log: string, deviceName: string) => {
64+
onLogMessage: (log: string, deviceName: string, deviceId: string) => {
65+
const device = _.find(this.connectedDevices, { id: deviceId});
66+
this.emit(DEVICE_LOG_EVENT_NAME, log, deviceId, device.platform);
6267
this.$logger.info(`LOG from device ${deviceName}: ${log}`);
6368
},
6469
onRestartMessage: () => {
@@ -69,7 +74,7 @@ export class PreviewSdkService implements IPreviewSdkService {
6974
},
7075
onDeviceConnectedMessage: (deviceConnectedMessage: DeviceConnectedMessage) => ({ }),
7176
onDeviceConnected: (device: Device) => {
72-
if (!_.includes(this.connectedDevices, device)) {
77+
if (!_.find(this.connectedDevices, {id: device.id})) {
7378
this.connectedDevices.push(device);
7479
}
7580
},

0 commit comments

Comments
 (0)