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