diff --git a/lib/common b/lib/common index 3e9e870c06..c78d5bdf7b 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 3e9e870c06461285d1a3f3f6b8163a972208d4b0 +Subproject commit c78d5bdf7ba550fe19812e39f08e3690834149d5 diff --git a/lib/constants.ts b/lib/constants.ts index b1858470a0..aa0e764f84 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -39,6 +39,7 @@ export const CONFIG_NS_APP_RESOURCES_ENTRY = "appResourcesPath"; export const CONFIG_NS_APP_ENTRY = "appPath"; export const DEPENDENCIES_JSON_NAME = "dependencies.json"; export const APK_EXTENSION_NAME = ".apk"; +export const HASHES_FILE_NAME = ".nshashes"; export class PackageVersion { static NEXT = "next"; diff --git a/lib/definitions/files-hash-service.d.ts b/lib/definitions/files-hash-service.d.ts index 753d63d65a..e561119baa 100644 --- a/lib/definitions/files-hash-service.d.ts +++ b/lib/definitions/files-hash-service.d.ts @@ -1,5 +1,21 @@ interface IFilesHashService { generateHashes(files: string[]): Promise; + /** + * Generate hashes for all prepared files (all files from app folder under platforms folder). + * @param platformData - Current platform's data + * @returns {Promise} + * A map with key file's path and value - file's hash + */ + generateHashesForProject(platformData: IPlatformData): Promise; + /** + * Generates hashes for all prepared files (all files from app folder under platforms folder) + * and saves them in .nshashes file under `hashFileDirectory` directory. + * @param platformData - Current platform's data + * @param hashesFileDirectory - Path to directory containing the hash file. + * @returns {Promise} + */ + saveHashesForProject(platformData: IPlatformData, hashesFileDirectory: string): Promise; + saveHashes(hashes: IStringDictionary, hashesFileDirectory: string): void; getChanges(files: string[], oldHashes: IStringDictionary): Promise; hasChangesInShasums(oldHashes: IStringDictionary, newHashes: IStringDictionary): boolean; } diff --git a/lib/definitions/livesync.d.ts b/lib/definitions/livesync.d.ts index a2807ee1bf..b160c6034b 100644 --- a/lib/definitions/livesync.d.ts +++ b/lib/definitions/livesync.d.ts @@ -154,6 +154,13 @@ interface ILiveSyncInfo extends IProjectDir, IEnvOptions, IBundle, IRelease, IOp */ clean?: boolean; + /** + * Defines if initial sync will be forced. + * In case it is true, transfers all project's directory on device + * In case it is false, transfers only changed files. + */ + force?: boolean; + /** * Defines the timeout in seconds {N} CLI will wait to find the inspector socket port from device's logs. * If not provided, defaults to 10seconds. @@ -330,6 +337,8 @@ interface ILiveSyncWatchInfo extends IProjectDataComposition, IHasUseHotModuleRe filesToSync: string[]; isReinstalled: boolean; syncAllFiles: boolean; + liveSyncDeviceInfo: ILiveSyncDeviceInfo; + force?: boolean; } interface ILiveSyncResultInfo extends IHasUseHotModuleReloadOption { @@ -344,6 +353,13 @@ interface IFullSyncInfo extends IProjectDataComposition, IHasUseHotModuleReloadO device: Mobile.IDevice; watch: boolean; syncAllFiles: boolean; + liveSyncDeviceInfo: ILiveSyncDeviceInfo; + force?: boolean; +} + +interface ITransferFilesOptions { + isFullSync: boolean; + force?: boolean; } interface IPlatformLiveSyncService { @@ -383,7 +399,7 @@ interface INativeScriptDeviceLiveSyncService extends IDeviceLiveSyncServiceBase * @param {boolean} isFullSync Indicates if the operation is part of a fullSync * @return {Promise} Returns the ILocalToDevicePathData of all transfered files */ - transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise; + transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise; } interface IAndroidNativeScriptDeviceLiveSyncService extends INativeScriptDeviceLiveSyncService { diff --git a/lib/helpers/livesync-command-helper.ts b/lib/helpers/livesync-command-helper.ts index 25f3aa3e27..508ac83528 100644 --- a/lib/helpers/livesync-command-helper.ts +++ b/lib/helpers/livesync-command-helper.ts @@ -115,7 +115,8 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper { release: this.$options.release, env: this.$options.env, timeout: this.$options.timeout, - useHotModuleReload: this.$options.hmr + useHotModuleReload: this.$options.hmr, + force: this.$options.force }; await this.$liveSyncService.liveSync(deviceDescriptors, liveSyncInfo); diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index a336e71113..ba0fb98881 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -30,7 +30,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject private $npm: INodePackageManager, private $androidPluginBuildService: IAndroidPluginBuildService, private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, - private $androidResourcesMigrationService: IAndroidResourcesMigrationService) { + private $androidResourcesMigrationService: IAndroidResourcesMigrationService, + private $filesHashService: IFilesHashService) { super($fs, $projectDataService); this.isAndroidStudioTemplate = false; } @@ -340,6 +341,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject message: "Gradle build..." }) ); + + await this.$filesHashService.saveHashesForProject(this._platformData, this._platformData.deviceBuildOutputPath); } private getGradleBuildOptions(settings: IAndroidBuildOptionsSettings, projectData: IProjectData): Array { diff --git a/lib/services/files-hash-service.ts b/lib/services/files-hash-service.ts index 45f1603843..f8893e4d99 100644 --- a/lib/services/files-hash-service.ts +++ b/lib/services/files-hash-service.ts @@ -1,5 +1,7 @@ import { executeActionByChunks } from "../common/helpers"; import { DEFAULT_CHUNK_SIZE } from "../common/constants"; +import { APP_FOLDER_NAME, HASHES_FILE_NAME } from "../constants"; +import * as path from "path"; export class FilesHashService implements IFilesHashService { constructor(private $fs: IFileSystem, @@ -24,6 +26,19 @@ export class FilesHashService implements IFilesHashService { return result; } + public async generateHashesForProject(platformData: IPlatformData): Promise { + const appFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + const files = this.$fs.enumerateFilesInDirectorySync(appFilesPath); + const hashes = await this.generateHashes(files); + return hashes; + } + + public async saveHashesForProject(platformData: IPlatformData, hashesFileDirectory: string): Promise { + const hashes = await this.generateHashesForProject(platformData); + this.saveHashes(hashes, hashesFileDirectory); + return hashes; + } + public async getChanges(files: string[], oldHashes: IStringDictionary): Promise { const newHashes = await this.generateHashes(files); return this.getChangesInShasums(oldHashes, newHashes); @@ -33,6 +48,11 @@ export class FilesHashService implements IFilesHashService { return !!_.keys(this.getChangesInShasums(oldHashes, newHashes)).length; } + public saveHashes(hashes: IStringDictionary, hashesFileDirectory: string): void { + const hashesFilePath = path.join(hashesFileDirectory, HASHES_FILE_NAME); + this.$fs.writeJson(hashesFilePath, hashes); + } + private getChangesInShasums(oldHashes: IStringDictionary, newHashes: IStringDictionary): IStringDictionary { return _.omitBy(newHashes, (hash: string, pathToFile: string) => !!_.find(oldHashes, (oldHash: string, oldPath: string) => pathToFile === oldPath && hash === oldHash)); } diff --git a/lib/services/livesync/android-device-livesync-service-base.ts b/lib/services/livesync/android-device-livesync-service-base.ts new file mode 100644 index 0000000000..eeddc7c32f --- /dev/null +++ b/lib/services/livesync/android-device-livesync-service-base.ts @@ -0,0 +1,85 @@ +import { DeviceLiveSyncServiceBase } from './device-livesync-service-base'; +import { AndroidDeviceHashService } from "../../common/mobile/android/android-device-hash-service"; + +export abstract class AndroidDeviceLiveSyncServiceBase extends DeviceLiveSyncServiceBase { + private deviceHashServices: IDictionary; + + constructor(protected $injector: IInjector, + protected $platformsData: IPlatformsData, + protected $filesHashService: IFilesHashService, + protected $logger: ILogger, + protected device: Mobile.IAndroidDevice) { + super($platformsData, device); + this.deviceHashServices = {}; + } + + public abstract async transferFilesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise; + public abstract async transferDirectoryOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise; + + public getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService { + const key = `${this.device.deviceInfo.identifier}${appIdentifier}`; + if (!this.deviceHashServices[key]) { + const deviceHashService = this.$injector.resolve(AndroidDeviceHashService, { adb: this.device.adb, appIdentifier }); + this.deviceHashServices[key] = deviceHashService; + } + + return this.deviceHashServices[key]; + } + + public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise { + const transferredFiles = await this.transferFilesCore(deviceAppData, localToDevicePaths, projectFilesPath, options); + await this.updateHashes(deviceAppData, localToDevicePaths, projectData, liveSyncDeviceInfo); + return transferredFiles; + } + + private async transferFilesCore(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, options: ITransferFilesOptions): Promise { + if (options.force && options.isFullSync) { + const hashFileDevicePath = this.getDeviceHashService(deviceAppData.appIdentifier).hashFileDevicePath; + await this.device.fileSystem.deleteFile(hashFileDevicePath, deviceAppData.appIdentifier); + this.$logger.trace("Before transfer directory on device ", localToDevicePaths); + await this.transferDirectoryOnDevice(deviceAppData, localToDevicePaths, projectFilesPath); + return localToDevicePaths; + } + + const localToDevicePathsToTransfer = await this.getLocalToDevicePathsToTransfer(deviceAppData, localToDevicePaths, options); + this.$logger.trace("Files to transfer: ", localToDevicePathsToTransfer); + await this.transferFilesOnDevice(deviceAppData, localToDevicePathsToTransfer); + return localToDevicePathsToTransfer; + } + + private async getLocalToDevicePathsToTransfer(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], options: ITransferFilesOptions): Promise { + if (options.force || !options.isFullSync) { + return localToDevicePaths; + } + + const changedLocalToDevicePaths = await this.getChangedLocalToDevicePaths(deviceAppData.appIdentifier, localToDevicePaths); + return changedLocalToDevicePaths; + } + + private async getChangedLocalToDevicePaths(appIdentifier: string, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + const deviceHashService = this.getDeviceHashService(appIdentifier); + const currentHashes = await deviceHashService.generateHashesFromLocalToDevicePaths(localToDevicePaths); + const oldHashes = (await deviceHashService.getShasumsFromDevice()) || {}; + const changedHashes = deviceHashService.getChangedShasums(oldHashes, currentHashes); + const changedFiles = _.keys(changedHashes); + const changedLocalToDevicePaths = localToDevicePaths.filter(localToDevicePathData => changedFiles.indexOf(localToDevicePathData.getLocalPath()) >= 0); + return changedLocalToDevicePaths; + } + + private async updateHashes(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo): Promise { + const hashes = await this.updateHashesOnDevice(deviceAppData, localToDevicePaths, projectData, liveSyncDeviceInfo); + this.updateLocalHashes(hashes, deviceAppData, projectData, liveSyncDeviceInfo); + } + + private async updateHashesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo): Promise { + const deviceHashService = this.getDeviceHashService(deviceAppData.appIdentifier); + const currentHashes = await deviceHashService.generateHashesFromLocalToDevicePaths(localToDevicePaths); + await deviceHashService.uploadHashFileToDevice(currentHashes); + return currentHashes; + } + + private updateLocalHashes(hashes: IStringDictionary, deviceAppData: Mobile.IDeviceAppData, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo): void { + const hashFilePath = liveSyncDeviceInfo.outputPath || this.$platformsData.getPlatformData(deviceAppData.platform, projectData).deviceBuildOutputPath; + this.$filesHashService.saveHashes(hashes, hashFilePath); + } +} diff --git a/lib/services/livesync/android-device-livesync-service.ts b/lib/services/livesync/android-device-livesync-service.ts index c212124242..fc979eb591 100644 --- a/lib/services/livesync/android-device-livesync-service.ts +++ b/lib/services/livesync/android-device-livesync-service.ts @@ -1,23 +1,29 @@ -import { DeviceAndroidDebugBridge } from "../../common/mobile/android/device-android-debug-bridge"; -import { AndroidDeviceHashService } from "../../common/mobile/android/android-device-hash-service"; -import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base"; +import { AndroidDeviceLiveSyncServiceBase } from "./android-device-livesync-service-base"; import * as helpers from "../../common/helpers"; import { LiveSyncPaths } from "../../common/constants"; -import { cache } from "../../common/decorators"; import * as path from "path"; import * as net from "net"; -export class AndroidDeviceLiveSyncService extends DeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { +export class AndroidDeviceLiveSyncService extends AndroidDeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { private port: number; - constructor( - private $mobileHelper: Mobile.IMobileHelper, + constructor(private $mobileHelper: Mobile.IMobileHelper, private $devicePathProvider: IDevicePathProvider, - private $injector: IInjector, + $injector: IInjector, private $androidProcessService: Mobile.IAndroidProcessService, protected $platformsData: IPlatformsData, - protected device: Mobile.IAndroidDevice) { - super($platformsData, device); + protected device: Mobile.IAndroidDevice, + $filesHashService: IFilesHashService, + $logger: ILogger) { + super($injector, $platformsData, $filesHashService, $logger, device); + } + + public async transferFilesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + await this.device.fileSystem.transferFiles(deviceAppData, localToDevicePaths); + } + + public async transferDirectoryOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { + await this.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); } public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise { @@ -116,12 +122,6 @@ export class AndroidDeviceLiveSyncService extends DeviceLiveSyncServiceBase impl await this.getDeviceHashService(deviceAppData.appIdentifier).removeHashes(localToDevicePaths); } - @cache() - public getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService { - const adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); - return this.$injector.resolve(AndroidDeviceHashService, { adb, appIdentifier }); - } - private async awaitRuntimeReloadSuccessMessage(): Promise { return new Promise((resolve, reject) => { let isResolved = false; diff --git a/lib/services/livesync/android-device-livesync-sockets-service.ts b/lib/services/livesync/android-device-livesync-sockets-service.ts index aa9d27494b..dec5569300 100644 --- a/lib/services/livesync/android-device-livesync-sockets-service.ts +++ b/lib/services/livesync/android-device-livesync-sockets-service.ts @@ -1,6 +1,4 @@ -import { DeviceAndroidDebugBridge } from "../../common/mobile/android/device-android-debug-bridge"; -import { AndroidDeviceHashService } from "../../common/mobile/android/android-device-hash-service"; -import { DeviceLiveSyncServiceBase } from "./device-livesync-service-base"; +import { AndroidDeviceLiveSyncServiceBase } from "./android-device-livesync-service-base"; import { APP_FOLDER_NAME } from "../../constants"; import { LiveSyncPaths } from "../../common/constants"; import { AndroidLivesyncTool } from "./android-livesync-tool"; @@ -8,24 +6,25 @@ import * as path from "path"; import * as temp from "temp"; import * as semver from "semver"; -export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { +export class AndroidDeviceSocketsLiveSyncService extends AndroidDeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { private livesyncTool: IAndroidLivesyncTool; private static STATUS_UPDATE_INTERVAL = 10000; private static MINIMAL_VERSION_LONG_LIVING_CONNECTION = "0.2.0"; constructor( private data: IProjectData, - private $injector: IInjector, + $injector: IInjector, protected $platformsData: IPlatformsData, protected $staticConfig: Config.IStaticConfig, - private $logger: ILogger, + $logger: ILogger, protected device: Mobile.IAndroidDevice, private $options: IOptions, private $processService: IProcessService, private $fs: IFileSystem, - private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants) { - super($platformsData, device); - this.livesyncTool = this.$injector.resolve(AndroidLivesyncTool); + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + $filesHashService: IFilesHashService) { + super($injector, $platformsData, $filesHashService, $logger, device); + this.livesyncTool = this.$injector.resolve(AndroidLivesyncTool); } public async beforeLiveSyncAction(deviceAppData: Mobile.IDeviceAppData): Promise { @@ -101,57 +100,17 @@ export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBa public async removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { await this.livesyncTool.removeFiles(_.map(localToDevicePaths, (element: any) => element.filePath)); - - await this.getDeviceHashService(deviceAppData.appIdentifier).removeHashes(localToDevicePaths); - } - - public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { - let transferredFiles; - - if (isFullSync) { - transferredFiles = await this._transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); - } else { - transferredFiles = await this._transferFiles(deviceAppData, localToDevicePaths); - } - - return transferredFiles; - } - - private async _transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { - await this.livesyncTool.sendFiles(localToDevicePaths.map(localToDevicePathData => localToDevicePathData.getLocalPath())); - - // Update hashes const deviceHashService = this.getDeviceHashService(deviceAppData.appIdentifier); - if (! await deviceHashService.updateHashes(localToDevicePaths)) { - this.$logger.trace("Unable to find hash file on device. The next livesync command will create it."); - } - - return localToDevicePaths; + await deviceHashService.removeHashes(localToDevicePaths); } - private async _transferDirectory(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { - let transferredLocalToDevicePaths: Mobile.ILocalToDevicePathData[]; - const deviceHashService = this.getDeviceHashService(deviceAppData.appIdentifier); - const currentShasums: IStringDictionary = await deviceHashService.generateHashesFromLocalToDevicePaths(localToDevicePaths); - const oldShasums = await deviceHashService.getShasumsFromDevice(); - - if (this.$options.force || !oldShasums) { - await this.livesyncTool.sendDirectory(projectFilesPath); - await deviceHashService.uploadHashFileToDevice(currentShasums); - transferredLocalToDevicePaths = localToDevicePaths; - } else { - const changedShasums = deviceHashService.getChangedShasums(oldShasums, currentShasums); - const changedFiles = _.keys(changedShasums); - if (changedFiles.length) { - await this.livesyncTool.sendFiles(changedFiles); - await deviceHashService.uploadHashFileToDevice(currentShasums); - transferredLocalToDevicePaths = localToDevicePaths.filter(localToDevicePathData => changedFiles.indexOf(localToDevicePathData.getLocalPath()) >= 0); - } else { - transferredLocalToDevicePaths = []; - } - } + public async transferFilesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + const files = _.map(localToDevicePaths, localToDevicePath => localToDevicePath.getLocalPath()); + await this.livesyncTool.sendFiles(files); + } - return transferredLocalToDevicePaths; + public async transferDirectoryOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { + await this.livesyncTool.sendDirectory(projectFilesPath); } private async connectLivesyncTool(appIdentifier: string) { @@ -165,9 +124,4 @@ export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBa }); } } - - public getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService { - const adb = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: this.device.deviceInfo.identifier }); - return this.$injector.resolve(AndroidDeviceHashService, { adb, appIdentifier }); - } } diff --git a/lib/services/livesync/device-livesync-service-base.ts b/lib/services/livesync/device-livesync-service-base.ts index 9b46395a96..e9e848d8bd 100644 --- a/lib/services/livesync/device-livesync-service-base.ts +++ b/lib/services/livesync/device-livesync-service-base.ts @@ -27,10 +27,10 @@ export abstract class DeviceLiveSyncServiceBase { return fastSyncFileExtensions; } - public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { + public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise { let transferredFiles: Mobile.ILocalToDevicePathData[] = []; - if (isFullSync) { + if (options.isFullSync) { transferredFiles = await this.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); } else { transferredFiles = await this.device.fileSystem.transferFiles(deviceAppData, localToDevicePaths); diff --git a/lib/services/livesync/ios-livesync-service.ts b/lib/services/livesync/ios-livesync-service.ts index 5ad992bb96..e2f3f03208 100644 --- a/lib/services/livesync/ios-livesync-service.ts +++ b/lib/services/livesync/ios-livesync-service.ts @@ -60,7 +60,7 @@ export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements I public liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise { if (liveSyncInfo.isReinstalled) { // In this case we should execute fullsync because iOS Runtime requires the full content of app dir to be extracted in the root of sync dir. - return this.fullSync({ projectData: liveSyncInfo.projectData, device, syncAllFiles: liveSyncInfo.syncAllFiles, watch: true }); + return this.fullSync({ projectData: liveSyncInfo.projectData, device, syncAllFiles: liveSyncInfo.syncAllFiles, liveSyncDeviceInfo: liveSyncInfo.liveSyncDeviceInfo, watch: true }); } else { return super.liveSyncWatchAction(device, liveSyncInfo); } diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index 4b597136f8..a7328c8c9d 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -505,10 +505,13 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi await platformLiveSyncService.prepareForLiveSync(device, projectData, liveSyncData, deviceBuildInfoDescriptor.debugOptions); const liveSyncResultInfo = await platformLiveSyncService.fullSync({ - projectData, device, + projectData, + device, syncAllFiles: liveSyncData.watchAllFiles, useHotModuleReload: liveSyncData.useHotModuleReload, - watch: !liveSyncData.skipWatcher + watch: !liveSyncData.skipWatcher, + force: liveSyncData.force, + liveSyncDeviceInfo: deviceBuildInfoDescriptor }); await this.$platformService.trackActionForPlatform({ action: "LiveSync", platform: device.deviceInfo.platform, isForDevice: !device.isEmulator, deviceOsVersion: device.deviceInfo.version }); @@ -637,12 +640,14 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi const service = this.getLiveSyncService(device.deviceInfo.platform); const settings: ILiveSyncWatchInfo = { + liveSyncDeviceInfo: deviceBuildInfoDescriptor, projectData, filesToRemove: currentFilesToRemove, filesToSync: currentFilesToSync, isReinstalled: appInstalledOnDeviceResult.appInstalled, syncAllFiles: liveSyncData.watchAllFiles, - useHotModuleReload: liveSyncData.useHotModuleReload + useHotModuleReload: liveSyncData.useHotModuleReload, + force: liveSyncData.force }; const liveSyncResultInfo = await service.liveSyncWatchAction(device, settings); diff --git a/lib/services/livesync/platform-livesync-service-base.ts b/lib/services/livesync/platform-livesync-service-base.ts index f2c64952f0..3189ae813d 100644 --- a/lib/services/livesync/platform-livesync-service-base.ts +++ b/lib/services/livesync/platform-livesync-service-base.ts @@ -47,7 +47,7 @@ export abstract class PlatformLiveSyncServiceBase { const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, null, []); - const modifiedFilesData = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, true, projectData); + const modifiedFilesData = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, syncInfo.liveSyncDeviceInfo, { isFullSync: true, force: syncInfo.force }); return { modifiedFilesData, @@ -86,7 +86,7 @@ export abstract class PlatformLiveSyncServiceBase { const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, existingFiles, []); modifiedLocalToDevicePaths.push(...localToDevicePaths); - modifiedLocalToDevicePaths = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, false, projectData); + modifiedLocalToDevicePaths = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, liveSyncInfo.liveSyncDeviceInfo, { isFullSync: false, force: liveSyncInfo.force}); } } @@ -113,11 +113,11 @@ export abstract class PlatformLiveSyncServiceBase { }; } - protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean, projectData: IProjectData): Promise { + protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, projectData: IProjectData, liveSyncDeviceInfo: ILiveSyncDeviceInfo, options: ITransferFilesOptions): Promise { let transferredFiles: Mobile.ILocalToDevicePathData[] = []; const deviceLiveSyncService = this.getDeviceLiveSyncService(deviceAppData.device, projectData); - transferredFiles = await deviceLiveSyncService.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, isFullSync); + transferredFiles = await deviceLiveSyncService.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, projectData, liveSyncDeviceInfo, options); this.logFilesSyncInformation(transferredFiles, "Successfully transferred %s.", this.$logger.info); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 984e5ed98b..bde73c5458 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -490,6 +490,13 @@ export class PlatformService extends EventEmitter implements IPlatformService { await device.applicationManager.reinstallApplication(projectData.projectId, packageFile); + await this.updateHashesOnDevice({ + device, + appIdentifier: projectData.projectId, + outputFilePath, + platformData + }); + if (!buildConfig.release) { const deviceFilePath = await this.getDeviceBuildInfoFilePath(device, projectData); const options = buildConfig; @@ -503,6 +510,22 @@ export class PlatformService extends EventEmitter implements IPlatformService { this.$logger.out(`Successfully installed on device with identifier '${device.deviceInfo.identifier}'.`); } + private async updateHashesOnDevice(data: { device: Mobile.IDevice, appIdentifier: string, outputFilePath: string, platformData: IPlatformData }): Promise { + const { device, appIdentifier, platformData, outputFilePath } = data; + + if (!this.$mobileHelper.isAndroidPlatform(platformData.normalizedPlatformName)) { + return; + } + + let hashes = {}; + const hashesFilePath = path.join(outputFilePath || platformData.deviceBuildOutputPath, constants.HASHES_FILE_NAME); + if (this.$fs.exists(hashesFilePath)) { + hashes = this.$fs.readJson(hashesFilePath); + } + + await device.fileSystem.updateHashesOnDevice(hashes, appIdentifier); + } + public async deployPlatform(deployInfo: IDeployPlatformInfo): Promise { await this.preparePlatform({ platform: deployInfo.platform, diff --git a/test/debug.ts b/test/debug.ts index 8e55b0c22a..a5d25638b9 100644 --- a/test/debug.ts +++ b/test/debug.ts @@ -78,6 +78,7 @@ function createTestInjector(): IInjector { testInjector.register("androidPluginBuildService", stubs.AndroidPluginBuildServiceStub); testInjector.register("platformEnvironmentRequirements", {}); testInjector.register("androidResourcesMigrationService", stubs.AndroidResourcesMigrationServiceStub); + testInjector.register("filesHashService", {}); return testInjector; } diff --git a/test/services/livesync/android-device-livesync-service-base.ts b/test/services/livesync/android-device-livesync-service-base.ts new file mode 100644 index 0000000000..97c793656e --- /dev/null +++ b/test/services/livesync/android-device-livesync-service-base.ts @@ -0,0 +1,381 @@ +import { Yok } from "../../../lib/common/yok"; +import { AndroidDeviceLiveSyncServiceBase } from "../../../lib/services/livesync/android-device-livesync-service-base"; +import { LiveSyncPaths } from "../../../lib/common/constants"; +import { assert } from "chai"; +import * as path from "path"; + +interface ITestSetupInput { + existsHashesFile?: boolean; + addChangedFile?: boolean; + addUnchangedFile?: boolean; + changedFileLocalName?: string; + unchangedFileLocalName?: string; +} + +interface ITestSetupOutput { + deviceAppData: Mobile.IDeviceAppData; + localToDevicePaths: Mobile.ILocalToDevicePathData[]; + androidDeviceLiveSyncServiceBase: any; + projectRoot: string; + changedFileLocalPath: string; + unchangedFileLocalPath: string; +} + +const transferFilesOnDeviceParams: any[] = []; +let transferDirectoryOnDeviceParams: any[] = []; +let resolveParams: any[] = []; +const deleteFileParams: any[] = []; +const writeJsonParams: any[] = []; +const pushFileParams: any[] = []; + +class AndroidDeviceLiveSyncServiceBaseMock extends AndroidDeviceLiveSyncServiceBase { + constructor($injector: IInjector, + $platformsData: any, + $filesHashService: any, + $logger: ILogger, + device: Mobile.IAndroidDevice) { + super($injector, $platformsData, $filesHashService, $logger, device); + } + + public async transferFilesOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + transferFilesOnDeviceParams.push({ deviceAppData, localToDevicePaths }); + } + + public async transferDirectoryOnDevice(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { + transferDirectoryOnDeviceParams.push({ deviceAppData, localToDevicePaths, projectFilesPath }); + } +} + +class LocalToDevicePathDataMock { + constructor(private filePath: string) { } + + public getLocalPath(): string { + return this.filePath; + } + + public getDevicePath(): string { + return `${LiveSyncPaths.ANDROID_TMP_DIR_NAME}/${path.basename(this.filePath)}`; + } +} + +function mockPlatformsData() { + return { + getPlatformData: () => { + return { + deviceBuildOutputPath: "testDeviceBuildOutputPath" + }; + } + }; +} + +function createTestInjector() { + const injector = new Yok(); + injector.register("fs", {}); + injector.register("mobileHelper", { + buildDevicePath: (filePath: string) => filePath + }); + return injector; +} + +const device: Mobile.IAndroidDevice = { + deviceInfo: mockDeviceInfo(), + adb: mockAdb(), + applicationManager: mockDeviceApplicationManager(), + fileSystem: mockDeviceFileSystem(), + isEmulator: true, + openDeviceLogStream: () => Promise.resolve(), + getApplicationInfo: () => Promise.resolve(null), + init: () => Promise.resolve() +}; + +function mockDeviceInfo(): Mobile.IDeviceInfo { + return { + identifier: "testIdentifier", + displayName: "testDisplayName", + model: "testModel", + version: "8.0.0", + vendor: "Google", + status: "", + errorHelp: null, + isTablet: true, + type: "Device", + platform: "Android" + }; +} + +function createDeviceAppData(androidVersion?: string): Mobile.IDeviceAppData { + return { + getDeviceProjectRootPath: async () => `${LiveSyncPaths.ANDROID_TMP_DIR_NAME}/${LiveSyncPaths.SYNC_DIR_NAME}`, + appIdentifier, + device: { + deviceInfo: { + version: androidVersion || "8.1.2" + } + }, + isLiveSyncSupported: async () => true, + platform: "Android" + }; +} + +function mockAdb(): Mobile.IDeviceAndroidDebugBridge { + return { + pushFile: async (filePath: string) => { + pushFileParams.push({ filePath }); + }, + sendBroadcastToDevice: async (action: string, extras?: IStringDictionary) => 1, + executeCommand: () => Promise.resolve(), + executeShellCommand: () => Promise.resolve(), + removeFile: () => Promise.resolve(), + getPropertyValue: async () => "testProperty", + getDevicesSafe: async () => [], + getDevices: async () => [] + }; +} + +function mockDeviceApplicationManager(): Mobile.IDeviceApplicationManager { + return {}; +} + +function mockDeviceFileSystem(): Mobile.IDeviceFileSystem { + return { + deleteFile: async (deviceFilePath, appIdentifier) => { + deleteFileParams.push({ deviceFilePath, appIdentifier }); + } + }; +} + +function mockFsStats(options: { isDirectory: boolean, isFile: boolean }): (filePath: string) => { isDirectory: () => boolean, isFile: () => boolean } { + return (filePath: string) => ({ + isDirectory: (): boolean => options.isDirectory, + isFile: (): boolean => options.isFile + }); +} + +function mockFilesHashService() { + return { + saveHashes: () => ({}) + }; +} + +function mockLogger(): any { + return { + trace: () => ({}) + }; +} + +function createAndroidDeviceLiveSyncServiceBase() { + const injector = new Yok(); + injector.resolve = (service, args) => resolveParams.push({ service, args }); + const androidDeviceLiveSyncServiceBase = new AndroidDeviceLiveSyncServiceBaseMock(injector, mockPlatformsData(), mockFilesHashService(), mockLogger(), device); + return androidDeviceLiveSyncServiceBase; +} + +function setup(options?: ITestSetupInput): ITestSetupOutput { + options = options || {}; + const projectRoot = "~/TestApp/app"; + const changedFileName = "test.js"; + const unchangedFileName = "notChangedFile.js"; + const changedFileLocalPath = `${projectRoot}/${options.changedFileLocalName || changedFileName}`; + const unchangedFileLocalPath = `${projectRoot}/${options.unchangedFileLocalName || unchangedFileName}`; + const filesToShasums: IStringDictionary = {}; + if (options.addChangedFile) { + filesToShasums[changedFileLocalPath] = "1"; + } + if (options.addUnchangedFile) { + filesToShasums[unchangedFileLocalPath] = "2"; + } + + const injector = createTestInjector(); + const localToDevicePaths = _.keys(filesToShasums).map(file => injector.resolve(LocalToDevicePathDataMock, { filePath: file })); + const deviceAppData = createDeviceAppData(); + const androidDeviceLiveSyncServiceBase = new AndroidDeviceLiveSyncServiceBaseMock(injector, mockPlatformsData(), mockFilesHashService(), mockLogger(), device); + + const fs = injector.resolve("fs"); + fs.exists = () => options.existsHashesFile; + fs.getFsStats = mockFsStats({ isDirectory: false, isFile: true }); + fs.getFileShasum = async (filePath: string) => filesToShasums[filePath]; + fs.writeJson = (filename: string, data: any) => writeJsonParams.push({ filename, data }); + fs.readJson = (filePath: string) => { + const deviceHashesFileContent: IStringDictionary = {}; + deviceHashesFileContent[`${projectRoot}/${changedFileName}`] = "11"; + if (options.addUnchangedFile) { + deviceHashesFileContent[`${projectRoot}/${unchangedFileName}`] = "2"; + } + + return deviceHashesFileContent; + }; + + return { + localToDevicePaths, + deviceAppData, + androidDeviceLiveSyncServiceBase, + projectRoot, + changedFileLocalPath, + unchangedFileLocalPath + }; +} + +const appIdentifier = "testAppIdentifier"; + +async function transferFiles(testSetup: ITestSetupOutput, options: { force: boolean, isFullSync: boolean}): Promise { + const androidDeviceLiveSyncServiceBase = testSetup.androidDeviceLiveSyncServiceBase; + const transferredFiles = await androidDeviceLiveSyncServiceBase.transferFiles( + testSetup.deviceAppData, + testSetup.localToDevicePaths, + testSetup.projectRoot, + {}, + {}, + options + ); + return transferredFiles; +} + +describe("AndroidDeviceLiveSyncServiceBase", () => { + describe("getDeviceHashService", () => { + beforeEach(() => { + resolveParams = []; + }); + it("should resolve AndroidDeviceHashService when the key is not stored in dictionary", () => { + const androidDeviceLiveSyncServiceBase = createAndroidDeviceLiveSyncServiceBase(); + androidDeviceLiveSyncServiceBase.getDeviceHashService(appIdentifier); + assert.equal(resolveParams.length, 1); + assert.isFunction(resolveParams[0].service); + assert.isDefined(resolveParams[0].args.adb); + assert.equal(resolveParams[0].args.appIdentifier, appIdentifier); + }); + it("should return already stored value when the method is called for second time with the same deviceIdentifier and appIdentifier", () => { + const androidDeviceLiveSyncServiceBase = createAndroidDeviceLiveSyncServiceBase(); + androidDeviceLiveSyncServiceBase.getDeviceHashService(appIdentifier); + assert.equal(resolveParams.length, 1); + assert.isFunction(resolveParams[0].service); + assert.isDefined(resolveParams[0].args.adb); + assert.equal(resolveParams[0].args.appIdentifier, appIdentifier); + + androidDeviceLiveSyncServiceBase.getDeviceHashService(appIdentifier); + assert.equal(resolveParams.length, 1); + assert.isFunction(resolveParams[0].service); + assert.isDefined(resolveParams[0].args.adb); + assert.equal(resolveParams[0].args.appIdentifier, appIdentifier); + }); + it("should return AndroidDeviceHashService when the method is called for second time with different appIdentifier and same deviceIdentifier", () => { + const androidDeviceLiveSyncServiceBase = createAndroidDeviceLiveSyncServiceBase(); + androidDeviceLiveSyncServiceBase.getDeviceHashService(appIdentifier); + assert.equal(resolveParams.length, 1); + assert.isFunction(resolveParams[0].service); + assert.isDefined(resolveParams[0].args.adb); + assert.equal(resolveParams[0].args.appIdentifier, appIdentifier); + + androidDeviceLiveSyncServiceBase.getDeviceHashService(appIdentifier); + assert.equal(resolveParams.length, 1); + assert.isFunction(resolveParams[0].service); + assert.isDefined(resolveParams[0].args.adb); + assert.equal(resolveParams[0].args.appIdentifier, appIdentifier); + + const newAppIdentifier = "myNewAppIdentifier"; + androidDeviceLiveSyncServiceBase.getDeviceHashService(newAppIdentifier); + assert.equal(resolveParams.length, 2); + assert.isFunction(resolveParams[1].service); + assert.isDefined(resolveParams[1].args.adb); + assert.equal(resolveParams[1].args.appIdentifier, newAppIdentifier); + }); + }); + + describe("transferFiles", () => { + beforeEach(() => { + transferDirectoryOnDeviceParams = []; + }); + + describe("when is full sync", () => { + it("transfers the whole directory when force option is specified", async () => { + const testSetup = setup({ + addChangedFile: true + }); + const transferredFiles = await transferFiles(testSetup, { force: true, isFullSync: true }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + assert.equal(transferDirectoryOnDeviceParams.length, 1); + assert.equal(transferDirectoryOnDeviceParams[0].localToDevicePaths.length, 1); + }); + it("transfers only changed files when there are file changes", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: true, + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + }); + it("transfers only changed files when there are both changed and not changed files", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: true, + addUnchangedFile: true + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + }); + it("does not transfer files when no file changes", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: false + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 0); + }); + it("transfers files which has different location and there are changed files", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: true, + addUnchangedFile: true, + unchangedFileLocalName: "newLocation/notChangedFile.js" + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 2); + assert.deepEqual(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + assert.deepEqual(transferredFiles[1].getLocalPath(), testSetup.unchangedFileLocalPath); + }); + it("transfers files which has different location and no changed files", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: false, + addUnchangedFile: true, + unchangedFileLocalName: "newLocation/notChangedFile.js" + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 1); + assert.deepEqual(transferredFiles[0].getLocalPath(), testSetup.unchangedFileLocalPath); + }); + it("transfers changed files with different location", async () => { + const testSetup = setup({ + existsHashesFile: true, + addChangedFile: true, + changedFileLocalName: "newLocation/test.js" + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: true }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + }); + }); + + describe("when is not full sync", () => { + it("does not transfer the whole directory when force option is specified", async () => { + const testSetup = setup({ + addChangedFile: true + }); + const transferredFiles = await transferFiles(testSetup, { force: true, isFullSync: false }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + assert.equal(transferDirectoryOnDeviceParams.length, 0); + }); + it("transfers all provided files", async () => { + const testSetup = setup({ + addChangedFile: true + }); + const transferredFiles = await transferFiles(testSetup, { force: false, isFullSync: false }); + assert.equal(transferredFiles.length, 1); + assert.equal(transferredFiles[0].getLocalPath(), testSetup.changedFileLocalPath); + }); + }); + }); +});