|
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