Skip to content

Start livesync watcher before preparing the project #3576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,5 @@ $injector.require("nativeScriptCloudExtensionService", "./services/nativescript-
$injector.requireCommand("resources|generate|icons", "./commands/generate-assets");
$injector.requireCommand("resources|generate|splashes", "./commands/generate-assets");
$injector.requirePublic("assetsGenerationService", "./services/assets-generation/assets-generation-service");

$injector.require("filesHashService", "./services/files-hash-service");
4 changes: 4 additions & 0 deletions lib/definitions/files-hash-service.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface IFilesHashService {
generateHashes(files: string[]): Promise<IStringDictionary>;
getChanges(files: string[], oldHashes: IStringDictionary): Promise<IStringDictionary>;
}
4 changes: 2 additions & 2 deletions lib/definitions/platform.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ interface IPlatformService extends IBuildPlatformAction, NodeJS.EventEmitter {
* @param {IShouldPrepareInfo} shouldPrepareInfo Options needed to decide whether to prepare.
* @returns {Promise<boolean>} true indicates that the project should be prepared.
*/
shouldPrepare(shouldPrepareInfo: IShouldPrepareInfo): Promise<boolean>
shouldPrepare(shouldPrepareInfo: IShouldPrepareInfo): Promise<boolean>;

/**
* Installs the application on specified device.
Expand Down Expand Up @@ -213,7 +213,7 @@ interface IPlatformService extends IBuildPlatformAction, NodeJS.EventEmitter {
* @param {string} buildInfoFileDirname The directory where the build file should be written to.
* @returns {void}
*/
saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void
saveBuildInfoFile(platform: string, projectDir: string, buildInfoFileDirname: string): void;
}

interface IPlatformOptions extends IPlatformSpecificData, ICreateProjectOptions { }
Expand Down
6 changes: 5 additions & 1 deletion lib/definitions/project-changes.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
interface IPrepareInfo extends IAddedNativePlatform {
interface IAppFilesHashes {
appFilesHashes: IStringDictionary;
}

interface IPrepareInfo extends IAddedNativePlatform, IAppFilesHashes {
time: string;
bundle: boolean;
release: boolean;
Expand Down
32 changes: 32 additions & 0 deletions lib/services/files-hash-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { executeActionByChunks } from "../common/helpers";
import { DEFAULT_CHUNK_SIZE } from "../common/constants";

export class FilesHashService implements IFilesHashService {
constructor(private $fs: IFileSystem,
private $logger: ILogger) { }

public async generateHashes(files: string[]): Promise<IStringDictionary> {
const result: IStringDictionary = {};

const action = async (file: string) => {
try {
const isFile = this.$fs.getFsStats(file).isFile();
if (isFile) {
result[file] = await this.$fs.getFileShasum(file);
}
} catch (err) {
this.$logger.trace(`Unable to generate hash for file ${file}. Error is: ${err}`);
}
};

await executeActionByChunks(files, DEFAULT_CHUNK_SIZE, action);

return result;
}

public async getChanges(files: string[], oldHashes: IStringDictionary): Promise<IStringDictionary> {
const newHashes = await this.generateHashes(files);
return _.omitBy(newHashes, (hash: string, pathToFile: string) => !!_.find(oldHashes, (oldHash: string, oldPath: string) => pathToFile === oldPath && hash === oldHash));
}
}
$injector.register("filesHashService", FilesHashService);
34 changes: 18 additions & 16 deletions lib/services/livesync/livesync-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
super();
}

public async liveSync(deviceDescriptors: ILiveSyncDeviceInfo[],
liveSyncData: ILiveSyncInfo): Promise<void> {
public async liveSync(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise<void> {
const projectData = this.$projectDataService.getProjectData(liveSyncData.projectDir);
await this.$pluginsService.ensureAllDependenciesAreInstalled(projectData);
await this.liveSyncOperation(deviceDescriptors, liveSyncData, projectData);
Expand Down Expand Up @@ -318,27 +317,19 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
}

@hook("liveSync")
private async liveSyncOperation(deviceDescriptors: ILiveSyncDeviceInfo[],
liveSyncData: ILiveSyncInfo, projectData: IProjectData): Promise<void> {
private async liveSyncOperation(deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo, projectData: IProjectData): Promise<void> {
// In case liveSync is called for a second time for the same projectDir.
const isAlreadyLiveSyncing = this.liveSyncProcessesInfo[projectData.projectDir] && !this.liveSyncProcessesInfo[projectData.projectDir].isStopped;

// Prevent cases where liveSync is called consecutive times with the same device, for example [ A, B, C ] and then [ A, B, D ] - we want to execute initialSync only for D.
const currentlyRunningDeviceDescriptors = this.getLiveSyncDeviceDescriptors(projectData.projectDir);
const deviceDescriptorsForInitialSync = isAlreadyLiveSyncing ? _.differenceBy(deviceDescriptors, currentlyRunningDeviceDescriptors, deviceDescriptorPrimaryKey) : deviceDescriptors;
this.setLiveSyncProcessInfo(liveSyncData.projectDir, deviceDescriptors);

await this.initialSync(projectData, deviceDescriptorsForInitialSync, liveSyncData);

if (!liveSyncData.skipWatcher && this.liveSyncProcessesInfo[projectData.projectDir].deviceDescriptors.length) {
// Should be set after prepare
this.$usbLiveSyncService.isInitialized = true;

const devicesIds = deviceDescriptors.map(dd => dd.identifier);
const devices = _.filter(this.$devicesService.getDeviceInstances(), device => _.includes(devicesIds, device.deviceInfo.identifier));
const platforms = _(devices).map(device => device.deviceInfo.platform).uniq().value();
await this.startWatcher(projectData, liveSyncData, platforms);
await this.startWatcher(projectData, liveSyncData, deviceDescriptors, { isAlreadyLiveSyncing });
}

await this.initialSync(projectData, liveSyncData, deviceDescriptors, { isAlreadyLiveSyncing });
}

private setLiveSyncProcessInfo(projectDir: string, deviceDescriptors: ILiveSyncDeviceInfo[]): void {
Expand All @@ -351,6 +342,13 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
this.liveSyncProcessesInfo[projectDir].deviceDescriptors = _.uniqBy(currentDeviceDescriptors.concat(deviceDescriptors), deviceDescriptorPrimaryKey);
}

private async initialSync(projectData: IProjectData, liveSyncData: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[], options: { isAlreadyLiveSyncing: boolean }): Promise<void> {
// Prevent cases where liveSync is called consecutive times with the same device, for example [ A, B, C ] and then [ A, B, D ] - we want to execute initialSync only for D.
const currentlyRunningDeviceDescriptors = this.getLiveSyncDeviceDescriptors(projectData.projectDir);
const deviceDescriptorsForInitialSync = options.isAlreadyLiveSyncing ? _.differenceBy(deviceDescriptors, currentlyRunningDeviceDescriptors, deviceDescriptorPrimaryKey) : deviceDescriptors;
await this.initialSyncCore(projectData, deviceDescriptorsForInitialSync, liveSyncData);
}

private getLiveSyncService(platform: string): IPlatformLiveSyncService {
if (this.$mobileHelper.isiOSPlatform(platform)) {
return this.$injector.resolve("iOSLiveSyncService");
Expand Down Expand Up @@ -452,7 +450,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
return null;
}

private async initialSync(projectData: IProjectData, deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise<void> {
private async initialSyncCore(projectData: IProjectData, deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise<void> {
const preparedPlatforms: string[] = [];
const rebuiltInformation: ILiveSyncBuildInfo[] = [];

Expand Down Expand Up @@ -483,6 +481,7 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
useLiveEdit: liveSyncData.useLiveEdit,
watch: !liveSyncData.skipWatcher
});

await this.$platformService.trackActionForPlatform({ action: "LiveSync", platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version });
await this.refreshApplication(projectData, liveSyncResultInfo, deviceBuildInfoDescriptor.debugOptions, deviceBuildInfoDescriptor.outputPath);

Expand Down Expand Up @@ -525,7 +524,10 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi
};
}

private async startWatcher(projectData: IProjectData, liveSyncData: ILiveSyncInfo, platforms: string[]): Promise<void> {
private async startWatcher(projectData: IProjectData, liveSyncData: ILiveSyncInfo, deviceDescriptors: ILiveSyncDeviceInfo[], options: { isAlreadyLiveSyncing: boolean }): Promise<void> {
const devicesIds = deviceDescriptors.map(dd => dd.identifier);
const devices = _.filter(this.$devicesService.getDeviceInstances(), device => _.includes(devicesIds, device.deviceInfo.identifier));
const platforms = _(devices).map(device => device.deviceInfo.platform).uniq().value();
const patterns = await this.getWatcherPatterns(liveSyncData, projectData, platforms);

if (liveSyncData.watchAllFiles) {
Expand Down
30 changes: 25 additions & 5 deletions lib/services/project-changes-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export class ProjectChangesService implements IProjectChangesService {
constructor(
private $platformsData: IPlatformsData,
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
private $fs: IFileSystem) {
private $fs: IFileSystem,
private $filesHashService: IFilesHashService) {
}

public get currentChanges(): IProjectChangesInfo {
Expand All @@ -58,9 +59,12 @@ export class ProjectChangesService implements IProjectChangesService {
public async checkForChanges(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): Promise<IProjectChangesInfo> {
const platformData = this.$platformsData.getPlatformData(platform, projectData);
this._changesInfo = new ProjectChangesInfo();
if (!this.ensurePrepareInfo(platform, projectData, projectChangesOptions)) {
const isNewPrepareInfo = await this.ensurePrepareInfo(platform, projectData, projectChangesOptions);
if (!isNewPrepareInfo) {
this._newFiles = 0;
this._changesInfo.appFilesChanged = this.containsNewerFiles(projectData.appDirectoryPath, projectData.appResourcesDirectoryPath, projectData);

this._changesInfo.appFilesChanged = await this.hasChangedAppFiles(projectData);

this._changesInfo.packageChanged = this.isProjectFileChanged(projectData, platform);
this._changesInfo.appResourcesChanged = this.containsNewerFiles(projectData.appResourcesDirectoryPath, null, projectData);
/*done because currently all node_modules are traversed, a possible improvement could be traversing only the production dependencies*/
Expand Down Expand Up @@ -152,7 +156,7 @@ export class ProjectChangesService implements IProjectChangesService {
}
}

private ensurePrepareInfo(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): boolean {
private async ensurePrepareInfo(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): Promise<boolean> {
this._prepareInfo = this.getPrepareInfo(platform, projectData);
if (this._prepareInfo) {
this._prepareInfo.nativePlatformStatus = this._prepareInfo.nativePlatformStatus && this._prepareInfo.nativePlatformStatus < projectChangesOptions.nativePlatformStatus ?
Expand All @@ -173,7 +177,8 @@ export class ProjectChangesService implements IProjectChangesService {
release: projectChangesOptions.release,
changesRequireBuild: true,
projectFileHash: this.getProjectFileStrippedHash(projectData, platform),
changesRequireBuildTime: null
changesRequireBuildTime: null,
appFilesHashes: await this.$filesHashService.generateHashes(this.getAppFiles(projectData))
};

this._outputProjectMtime = 0;
Expand Down Expand Up @@ -300,5 +305,20 @@ export class ProjectChangesService implements IProjectChangesService {
}
return false;
}

private getAppFiles(projectData: IProjectData): string[] {
return this.$fs.enumerateFilesInDirectorySync(projectData.appDirectoryPath, (filePath: string, stat: IFsStats) => filePath !== projectData.appResourcesDirectoryPath);
}

private async hasChangedAppFiles(projectData: IProjectData): Promise<boolean> {
const files = this.getAppFiles(projectData);
const changedFiles = await this.$filesHashService.getChanges(files, this._prepareInfo.appFilesHashes || {});
const hasChanges = changedFiles && _.keys(changedFiles).length > 0;
if (hasChanges) {
this._prepareInfo.appFilesHashes = await this.$filesHashService.generateHashes(files);
}

return hasChanges;
}
}
$injector.register("projectChangesService", ProjectChangesService);
4 changes: 4 additions & 0 deletions test/npm-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ function createTestInjector(): IInjector {
});
testInjector.register("httpClient", {});
testInjector.register("androidResourcesMigrationService", stubs.AndroidResourcesMigrationServiceStub);
testInjector.register("filesHashService", {
getChanges: () => Promise.resolve({}),
generateHashes: () => Promise.resolve()
});

return testInjector;
}
Expand Down
1 change: 1 addition & 0 deletions test/platform-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ function createTestInjector() {
testInjector.register("analyticsSettingsService", {
getPlaygroundInfo: () => Promise.resolve(null)
});
testInjector.register("filesHashService", {});

return testInjector;
}
Expand Down
4 changes: 4 additions & 0 deletions test/platform-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ function createTestInjector() {
})
});
testInjector.register("androidResourcesMigrationService", stubs.AndroidResourcesMigrationServiceStub);
testInjector.register("filesHashService", {
generateHashes: () => Promise.resolve(),
getChanges: () => Promise.resolve({test: "testHash"})
});

return testInjector;
}
Expand Down
9 changes: 8 additions & 1 deletion test/project-changes-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ class ProjectChangesServiceTest extends BaseServiceTest {
this.injector.register("devicePlatformsConstants", {});
this.injector.register("devicePlatformsConstants", {});
this.injector.register("projectChangesService", ProjectChangesService);
this.injector.register("filesHashService", {
generateHashes: () => Promise.resolve({})
});
this.injector.register("logger", {
warn: () => ({})
});

const fs = this.injector.resolve<IFileSystem>("fs");
fs.writeJson(path.join(this.projectDir, Constants.PACKAGE_JSON_FILE_NAME), {
Expand Down Expand Up @@ -127,7 +133,8 @@ describe("Project Changes Service Tests", () => {
changesRequireBuildTime: new Date().toString(),
iOSProvisioningProfileUUID: "provisioning_profile_test",
projectFileHash: "",
nativePlatformStatus: Constants.NativePlatformStatus.requiresPlatformAdd
nativePlatformStatus: Constants.NativePlatformStatus.requiresPlatformAdd,
appFilesHashes: {}
};
fs.writeJson(prepareInfoPath, expectedPrepareInfo);

Expand Down