|
1 |
| -export abstract class LiveSyncServiceBase<T extends Mobile.IDevice> { |
2 |
| - protected get device(): T { |
3 |
| - return <T>(this._device); |
| 1 | +import * as fiberBootstrap from "../../common/fiber-bootstrap"; |
| 2 | +import syncBatchLib = require("../../common/services/livesync/sync-batch"); |
| 3 | +import * as shell from "shelljs"; |
| 4 | +import * as path from "path"; |
| 5 | +import * as temp from "temp"; |
| 6 | +import * as minimatch from "minimatch"; |
| 7 | +import * as constants from "../../common/constants"; |
| 8 | +import * as util from "util"; |
| 9 | + |
| 10 | +let gaze = require("gaze"); |
| 11 | + |
| 12 | +class LiveSyncServiceBase implements ILiveSyncServiceBase { |
| 13 | + private showFullLiveSyncInformation: boolean = false; |
| 14 | + private fileHashes: IDictionary<string>; |
| 15 | + |
| 16 | + constructor(protected $devicesService: Mobile.IDevicesService, |
| 17 | + protected $mobileHelper: Mobile.IMobileHelper, |
| 18 | + protected $logger: ILogger, |
| 19 | + protected $options: ICommonOptions, |
| 20 | + protected $deviceAppDataFactory: Mobile.IDeviceAppDataFactory, |
| 21 | + protected $fs: IFileSystem, |
| 22 | + protected $injector: IInjector, |
| 23 | + protected $hooksService: IHooksService, |
| 24 | + private $projectFilesManager: IProjectFilesManager, |
| 25 | + private $projectFilesProvider: IProjectFilesProvider, |
| 26 | + private $liveSyncProvider: ILiveSyncProvider, |
| 27 | + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, |
| 28 | + private $hostInfo: IHostInfo, |
| 29 | + private $dispatcher: IFutureDispatcher) { |
| 30 | + this.fileHashes = Object.create(null); |
4 | 31 | }
|
5 | 32 |
|
6 |
| - constructor(private _device: Mobile.IDevice, |
7 |
| - private $liveSyncProvider: ILiveSyncProvider) { } |
| 33 | + public getPlatform(platform?: string): IFuture<string> { // gets the platform and ensures that the devicesService is initialized |
| 34 | + return (() => { |
| 35 | + this.$devicesService.initialize({ platform: platform, deviceId: this.$options.device }).wait(); |
| 36 | + return platform || this.$devicesService.platform; |
| 37 | + }).future<string>()(); |
| 38 | + } |
| 39 | + |
| 40 | + public sync(data: ILiveSyncData[], filePaths?: string[]): IFuture<void> { |
| 41 | + return (() => { |
| 42 | + this.syncCore(data, filePaths).wait(); |
| 43 | + if (this.$options.watch) { |
| 44 | + this.$hooksService.executeBeforeHooks('watch').wait(); |
| 45 | + this.partialSync(data, data[0].syncWorkingDirectory); |
| 46 | + } |
| 47 | + }).future<void>()(); |
| 48 | + } |
8 | 49 |
|
9 |
| - public refreshApplication(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], forceExecuteFullSync: boolean): IFuture<void> { |
10 |
| - let canExecuteFastSync = !forceExecuteFullSync && localToDevicePaths !== undefined; |
11 |
| - for (let localToDevicePath of localToDevicePaths) { |
12 |
| - if (!this.$liveSyncProvider.canExecuteFastSync(localToDevicePath.getLocalPath(), deviceAppData.platform)) { |
13 |
| - canExecuteFastSync = false; |
14 |
| - break; |
| 50 | + private isFileExcluded(filePath: string, excludedPatterns: string[]): boolean { |
| 51 | + let isFileExcluded = false; |
| 52 | + _.each(excludedPatterns, pattern => { |
| 53 | + if (minimatch(filePath, pattern, { nocase: true })) { |
| 54 | + isFileExcluded = true; |
| 55 | + return false; |
15 | 56 | }
|
| 57 | + }); |
| 58 | + |
| 59 | + return isFileExcluded; |
| 60 | + } |
| 61 | + |
| 62 | + private partialSync(data: ILiveSyncData[], syncWorkingDirectory: string): void { |
| 63 | + let that = this; |
| 64 | + this.showFullLiveSyncInformation = true; |
| 65 | + gaze("**/*", { cwd: syncWorkingDirectory }, function (err: any, watcher: any) { |
| 66 | + this.on('all', (event: string, filePath: string) => { |
| 67 | + fiberBootstrap.run(() => { |
| 68 | + that.$dispatcher.dispatch(() => (() => { |
| 69 | + try { |
| 70 | + |
| 71 | + if (filePath.indexOf(constants.APP_RESOURCES_FOLDER_NAME) !== -1) { |
| 72 | + that.$logger.warn(`Skipping livesync for changed file ${filePath}. This change requires a full build to update your application. `.yellow.bold); |
| 73 | + return; |
| 74 | + } |
| 75 | + |
| 76 | + let fileHash = that.$fs.exists(filePath).wait() && that.$fs.getFsStats(filePath).wait().isFile() ? that.$fs.getFileShasum(filePath).wait() : ""; |
| 77 | + if (fileHash === that.fileHashes[filePath]) { |
| 78 | + that.$logger.trace(`Skipping livesync for ${filePath} file with ${fileHash} hash.`); |
| 79 | + return; |
| 80 | + } |
| 81 | + |
| 82 | + that.$logger.trace(`Adding ${filePath} file with ${fileHash} hash.`); |
| 83 | + that.fileHashes[filePath] = fileHash; |
| 84 | + |
| 85 | + for (let dataItem of data) { |
| 86 | + if (that.isFileExcluded(filePath, dataItem.excludedProjectDirsAndFiles)) { |
| 87 | + that.$logger.trace(`Skipping livesync for changed file ${filePath} as it is excluded in the patterns: ${dataItem.excludedProjectDirsAndFiles.join(", ")}`); |
| 88 | + continue; |
| 89 | + } |
| 90 | + let mappedFilePath = that.$projectFilesProvider.mapFilePath(filePath, dataItem.platform); |
| 91 | + that.$logger.trace(`Syncing filePath ${filePath}, mappedFilePath is ${mappedFilePath}`); |
| 92 | + if (!mappedFilePath) { |
| 93 | + that.$logger.warn(`Unable to sync ${filePath}.`); |
| 94 | + continue; |
| 95 | + } |
| 96 | + |
| 97 | + if (event === "added" || event === "changed" || event === "renamed") { |
| 98 | + that.batchSync(dataItem, mappedFilePath); |
| 99 | + } else if (event === "deleted") { |
| 100 | + that.fileHashes = <any>(_.omit(that.fileHashes, filePath)); |
| 101 | + that.syncRemovedFile(dataItem, mappedFilePath).wait(); |
| 102 | + } |
| 103 | + } |
| 104 | + } catch (err) { |
| 105 | + that.$logger.info(`Unable to sync file ${filePath}. Error is:${err.message}`.red.bold); |
| 106 | + that.$logger.info("Try saving it again or restart the livesync operation."); |
| 107 | + } |
| 108 | + }).future<void>()()); |
| 109 | + }); |
| 110 | + }); |
| 111 | + }); |
| 112 | + |
| 113 | + this.$dispatcher.run(); |
| 114 | + } |
| 115 | + |
| 116 | + private batch: IDictionary<ISyncBatch> = Object.create(null); |
| 117 | + private livesyncData: IDictionary<ILiveSyncData> = Object.create(null); |
| 118 | + |
| 119 | + private batchSync(data: ILiveSyncData, filePath: string): void { |
| 120 | + let platformBatch: ISyncBatch = this.batch[data.platform]; |
| 121 | + if (!platformBatch || !platformBatch.syncPending) { |
| 122 | + let done = () => { |
| 123 | + return (() => { |
| 124 | + setTimeout(() => { |
| 125 | + fiberBootstrap.run(() => { |
| 126 | + this.$dispatcher.dispatch(() => (() => { |
| 127 | + try { |
| 128 | + for (let platformName in this.batch) { |
| 129 | + let batch = this.batch[platformName]; |
| 130 | + let livesyncData = this.livesyncData[platformName]; |
| 131 | + batch.syncFiles(((filesToSync:string[]) => { |
| 132 | + this.$liveSyncProvider.preparePlatformForSync(platformName).wait(); |
| 133 | + this.syncCore([livesyncData], filesToSync); |
| 134 | + }).future<void>()).wait(); |
| 135 | + } |
| 136 | + } catch (err) { |
| 137 | + this.$logger.warn(`Unable to sync files. Error is:`, err.message); |
| 138 | + } |
| 139 | + }).future<void>()()); |
| 140 | + |
| 141 | + }); |
| 142 | + }, syncBatchLib.SYNC_WAIT_THRESHOLD); |
| 143 | + }).future<void>()(); |
| 144 | + }; |
| 145 | + this.batch[data.platform] = this.$injector.resolve(syncBatchLib.SyncBatch, { done: done }); |
| 146 | + this.livesyncData[data.platform] = data; |
16 | 147 | }
|
17 |
| - if (canExecuteFastSync) { |
18 |
| - return this.reloadPage(deviceAppData); |
| 148 | + |
| 149 | + this.batch[data.platform].addFile(filePath); |
| 150 | + } |
| 151 | + |
| 152 | + private syncRemovedFile(data: ILiveSyncData, filePath: string): IFuture<void> { |
| 153 | + return (() => { |
| 154 | + let filePathArray = [filePath], |
| 155 | + deviceFilesAction = this.getSyncRemovedFilesAction(data); |
| 156 | + |
| 157 | + this.syncCore([data], filePathArray, deviceFilesAction).wait(); |
| 158 | + }).future<void>()(); |
| 159 | + } |
| 160 | + |
| 161 | + public getSyncRemovedFilesAction(data: ILiveSyncData): (device: Mobile.IDevice, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture<void> { |
| 162 | + return (device: Mobile.IDevice, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => { |
| 163 | + let platformLiveSyncService = this.resolvePlatformLiveSyncService(data.platform, device); |
| 164 | + return platformLiveSyncService.removeFiles(data.appIdentifier, localToDevicePaths); |
| 165 | + }; |
| 166 | + } |
| 167 | + |
| 168 | + public getSyncAction(data: ILiveSyncData, filesToSync: string[], deviceFilesAction: (device: Mobile.IDevice, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture<void>, liveSyncOptions: ILiveSyncOptions): (device: Mobile.IDevice) => IFuture<void> { |
| 169 | + let appIdentifier = data.appIdentifier; |
| 170 | + let platform = data.platform; |
| 171 | + let projectFilesPath = data.projectFilesPath; |
| 172 | + |
| 173 | + let packageFilePath: string = null; |
| 174 | + |
| 175 | + let action = (device: Mobile.IDevice): IFuture<void> => { |
| 176 | + return (() => { |
| 177 | + let shouldRefreshApplication = true; |
| 178 | + let deviceAppData = this.$deviceAppDataFactory.create(appIdentifier, this.$mobileHelper.normalizePlatformName(platform), device, liveSyncOptions); |
| 179 | + if (deviceAppData.isLiveSyncSupported().wait()) { |
| 180 | + let platformLiveSyncService = this.resolvePlatformLiveSyncService(platform, device); |
| 181 | + |
| 182 | + if (platformLiveSyncService.beforeLiveSyncAction) { |
| 183 | + platformLiveSyncService.beforeLiveSyncAction(deviceAppData).wait(); |
| 184 | + } |
| 185 | + |
| 186 | + // Not installed application |
| 187 | + device.applicationManager.checkForApplicationUpdates().wait(); |
| 188 | + |
| 189 | + let wasInstalled = true; |
| 190 | + if (!device.applicationManager.isApplicationInstalled(appIdentifier).wait() && !this.$options.companion) { |
| 191 | + this.$logger.warn(`The application with id "${appIdentifier}" is not installed on device with identifier ${device.deviceInfo.identifier}.`); |
| 192 | + if (!packageFilePath) { |
| 193 | + packageFilePath = this.$liveSyncProvider.buildForDevice(device).wait(); |
| 194 | + } |
| 195 | + device.applicationManager.installApplication(packageFilePath).wait(); |
| 196 | + |
| 197 | + if (platformLiveSyncService.afterInstallApplicationAction) { |
| 198 | + let localToDevicePaths = this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, filesToSync, data.excludedProjectDirsAndFiles, liveSyncOptions); |
| 199 | + shouldRefreshApplication = platformLiveSyncService.afterInstallApplicationAction(deviceAppData, localToDevicePaths).wait(); |
| 200 | + } else { |
| 201 | + shouldRefreshApplication = false; |
| 202 | + } |
| 203 | + |
| 204 | + if (device.applicationManager.canStartApplication() && !shouldRefreshApplication) { |
| 205 | + device.applicationManager.startApplication(appIdentifier).wait(); |
| 206 | + } |
| 207 | + wasInstalled = false; |
| 208 | + } |
| 209 | + |
| 210 | + // Restart application or reload page |
| 211 | + if (shouldRefreshApplication) { |
| 212 | + // Transfer or remove files on device |
| 213 | + let localToDevicePaths = this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, filesToSync, data.excludedProjectDirsAndFiles, liveSyncOptions); |
| 214 | + if (deviceFilesAction) { |
| 215 | + deviceFilesAction(device, localToDevicePaths).wait(); |
| 216 | + } else { |
| 217 | + this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, !filesToSync).wait(); |
| 218 | + } |
| 219 | + |
| 220 | + this.$logger.info("Applying changes..."); |
| 221 | + platformLiveSyncService.refreshApplication(deviceAppData, localToDevicePaths, data.forceExecuteFullSync || !wasInstalled).wait(); |
| 222 | + this.$logger.info(`Successfully synced application ${data.appIdentifier} on device ${device.deviceInfo.identifier}.`); |
| 223 | + } |
| 224 | + } else { |
| 225 | + this.$logger.warn(`LiveSync is not supported for application: ${deviceAppData.appIdentifier} on device with identifier ${device.deviceInfo.identifier}.`); |
| 226 | + } |
| 227 | + }).future<void>()(); |
| 228 | + }; |
| 229 | + |
| 230 | + return action; |
| 231 | + } |
| 232 | + |
| 233 | + private syncCore(data: ILiveSyncData[], filesToSync: string[], deviceFilesAction?: (device: Mobile.IDevice, localToDevicePaths: Mobile.ILocalToDevicePathData[]) => IFuture<void>): IFuture<void> { |
| 234 | + return (() => { |
| 235 | + for (let dataItem of data) { |
| 236 | + let appIdentifier = dataItem.appIdentifier; |
| 237 | + let platform = dataItem.platform; |
| 238 | + let canExecute = this.getCanExecuteAction(platform, appIdentifier, dataItem.canExecute); |
| 239 | + let action = this.getSyncAction(dataItem, filesToSync, deviceFilesAction, { isForCompanionApp: this.$options.companion, additionalConfigurations: dataItem.additionalConfigurations, configuration: dataItem.configuration, isForDeletedFiles: false }); |
| 240 | + this.$devicesService.execute(action, canExecute).wait(); |
| 241 | + } |
| 242 | + }).future<void>()(); |
| 243 | + } |
| 244 | + |
| 245 | + private transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): IFuture<void> { |
| 246 | + return (() => { |
| 247 | + this.$logger.info("Transferring project files..."); |
| 248 | + this.logFilesSyncInformation(localToDevicePaths, "Transferring %s.", this.$logger.trace); |
| 249 | + |
| 250 | + let canTransferDirectory = isFullSync && (this.$devicesService.isAndroidDevice(deviceAppData.device) || this.$devicesService.isiOSSimulator(deviceAppData.device)); |
| 251 | + if (canTransferDirectory) { |
| 252 | + let tempDir = temp.mkdirSync("tempDir"); |
| 253 | + shell.cp("-Rf", path.join(projectFilesPath, "*"), tempDir); |
| 254 | + this.$projectFilesManager.processPlatformSpecificFiles(tempDir, deviceAppData.platform).wait(); |
| 255 | + deviceAppData.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, tempDir).wait(); |
| 256 | + } else { |
| 257 | + this.$liveSyncProvider.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, isFullSync).wait(); |
| 258 | + } |
| 259 | + |
| 260 | + this.logFilesSyncInformation(localToDevicePaths, "Successfully transferred %s.", this.$logger.info); |
| 261 | + }).future<void>()(); |
| 262 | + } |
| 263 | + |
| 264 | + private logFilesSyncInformation(localToDevicePaths: Mobile.ILocalToDevicePathData[], message: string, action: Function): void { |
| 265 | + if (this.showFullLiveSyncInformation) { |
| 266 | + _.each(localToDevicePaths, (file: Mobile.ILocalToDevicePathData) => { |
| 267 | + action.call(this.$logger, util.format(message, path.basename(file.getLocalPath()).yellow)); |
| 268 | + }); |
| 269 | + } else { |
| 270 | + action.call(this.$logger, util.format(message, "all files")); |
19 | 271 | }
|
| 272 | + } |
20 | 273 |
|
21 |
| - return this.restartApplication(deviceAppData); |
| 274 | + private resolvePlatformLiveSyncService(platform: string, device: Mobile.IDevice): IPlatformLiveSyncService { |
| 275 | + return this.$injector.resolve(this.$liveSyncProvider.platformSpecificLiveSyncServices[platform.toLowerCase()], { _device: device }); |
22 | 276 | }
|
23 | 277 |
|
24 |
| - protected abstract restartApplication(deviceAppData: Mobile.IDeviceAppData): IFuture<void>; |
25 |
| - protected abstract reloadPage(deviceAppData: Mobile.IDeviceAppData): IFuture<void>; |
| 278 | + public getCanExecuteAction(platform: string, appIdentifier: string, canExecute: (dev: Mobile.IDevice) => boolean): (dev: Mobile.IDevice) => boolean { |
| 279 | + canExecute = canExecute || ((dev: Mobile.IDevice) => dev.deviceInfo.platform.toLowerCase() === platform.toLowerCase()); |
| 280 | + let finalCanExecute = canExecute; |
| 281 | + if (this.$options.device) { |
| 282 | + return (device: Mobile.IDevice): boolean => canExecute(device) && device.deviceInfo.identifier === this.$devicesService.getDeviceByDeviceOption().deviceInfo.identifier; |
| 283 | + } |
| 284 | + |
| 285 | + if (this.$mobileHelper.isiOSPlatform(platform)) { |
| 286 | + if (this.$options.emulator) { |
| 287 | + finalCanExecute = (device: Mobile.IDevice): boolean => canExecute(device) && this.$devicesService.isiOSSimulator(device); |
| 288 | + } else { |
| 289 | + let devices = this.$devicesService.getDevicesForPlatform(platform); |
| 290 | + let simulator = _.find(devices, d => this.$devicesService.isiOSSimulator(d)); |
| 291 | + if (simulator) { |
| 292 | + let iOSDevices = _.filter(devices, d => d.deviceInfo.identifier !== simulator.deviceInfo.identifier); |
| 293 | + if (iOSDevices && iOSDevices.length) { |
| 294 | + let isApplicationInstalledOnSimulator = simulator.applicationManager.isApplicationInstalled(appIdentifier).wait(); |
| 295 | + let isApplicationInstalledOnAllDevices = _.intersection.apply(null, iOSDevices.map(device => device.applicationManager.isApplicationInstalled(appIdentifier).wait())); |
| 296 | + // In case the application is not installed on both device and simulator, syncs only on device. |
| 297 | + if (!isApplicationInstalledOnSimulator && !isApplicationInstalledOnAllDevices) { |
| 298 | + finalCanExecute = (device: Mobile.IDevice): boolean => canExecute(device) && this.$devicesService.isiOSDevice(device); |
| 299 | + } |
| 300 | + } |
| 301 | + } |
| 302 | + } |
| 303 | + } |
| 304 | + |
| 305 | + return finalCanExecute; |
| 306 | + } |
26 | 307 | }
|
| 308 | +$injector.register('liveSyncServiceBase', LiveSyncServiceBase); |
0 commit comments