diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 6c016f1dd0..ac06ce0eed 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -122,6 +122,7 @@ $injector.require("deployCommandHelper", "./helpers/deploy-command-helper"); $injector.requirePublicClass("localBuildService", "./services/local-build-service"); $injector.requirePublicClass("liveSyncService", "./services/livesync/livesync-service"); +$injector.requirePublicClass("androidLivesyncTool", "./services/livesync/android-livesync-tool"); $injector.require("androidLiveSyncService", "./services/livesync/android-livesync-service"); $injector.require("iOSLiveSyncService", "./services/livesync/ios-livesync-service"); $injector.require("usbLiveSyncService", "./services/livesync/livesync-service"); // The name is used in https://github.com/NativeScript/nativescript-dev-typescript diff --git a/lib/common b/lib/common index c3aaa6cf49..61cdaaaf75 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit c3aaa6cf49e108da379a4445dd5e1d001817110e +Subproject commit 61cdaaaf7533394afbbe84dd4eee355072ade2de diff --git a/lib/definitions/livesync-global.d.ts b/lib/definitions/livesync-global.d.ts new file mode 100644 index 0000000000..ac93fae85a --- /dev/null +++ b/lib/definitions/livesync-global.d.ts @@ -0,0 +1,8 @@ +import * as stream from "stream"; + +declare global { + interface IDuplexSocket extends stream.Duplex { + uid?: string; + } +} + diff --git a/lib/definitions/livesync.d.ts b/lib/definitions/livesync.d.ts index 82d70d7958..75291a5922 100644 --- a/lib/definitions/livesync.d.ts +++ b/lib/definitions/livesync.d.ts @@ -344,6 +344,7 @@ interface IPlatformLiveSyncService { liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise; refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise; prepareForLiveSync(device: Mobile.IDevice, data: IProjectDir, liveSyncInfo: ILiveSyncInfo, debugOptions: IDebugOptions): Promise; + getDeviceLiveSyncService(device: Mobile.IDevice, projectData: IProjectData): INativeScriptDeviceLiveSyncService; } interface INativeScriptDeviceLiveSyncService extends IDeviceLiveSyncServiceBase { @@ -362,9 +363,27 @@ interface INativeScriptDeviceLiveSyncService extends IDeviceLiveSyncServiceBase * Removes specified files from a connected device * @param {Mobile.IDeviceAppData} deviceAppData Data about device and app. * @param {Mobile.ILocalToDevicePathData[]} localToDevicePaths Object containing a mapping of file paths from the system to the device. + * @param {string} projectFilesPath The Path to the app folder inside platforms folder * @return {Promise} */ - removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise; + removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath?: string): Promise; + + /** + * Transfers specified files to a connected device + * @param {Mobile.IDeviceAppData} deviceAppData Data about device and app. + * @param {Mobile.ILocalToDevicePathData[]} localToDevicePaths Object containing a mapping of file paths from the system to the device. + * @param {string} projectFilesPath The Path to the app folder inside platforms folder + * @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; + + /** + * Guarantees all remove/update operations have finished + * @param {ILiveSyncResultInfo} liveSyncInfo Describes the LiveSync operation - for which project directory is the operation and other settings. + * @return {Promise} + */ + finalizeSync(liveSyncInfo: ILiveSyncResultInfo): Promise; } interface IAndroidNativeScriptDeviceLiveSyncService { @@ -376,6 +395,95 @@ interface IAndroidNativeScriptDeviceLiveSyncService { getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService; } +interface IAndroidLivesyncTool { + /** + * Creates new socket connection. + * @param configuration - The configuration to the socket connection. + * @returns {Promise} + */ + connect(configuration: IAndroidLivesyncToolConfiguration): Promise; + /** + * Sends a file through the socket. + * @param filePath - The full path to the file. + * @returns {Promise} + */ + sendFile(filePath: string): Promise; + /** + * Sends files through the socket. + * @param filePaths - Array of files that will be send by the socket. + * @returns {Promise} + */ + sendFiles(filePaths: string[]): Promise; + /** + * Sends all files from directory by the socket. + * @param directoryPath - The path to the directory which files will be send by the socket. + * @returns {Promise} + */ + sendDirectory(directoryPath: string): Promise; + /** + * Removes file + * @param filePath - The full path to the file. + * @returns {Promise} + */ + removeFile(filePath: string): Promise; + /** + * Removes files + * @param filePaths - Array of files that will be removed. + * @returns {Promise} + */ + removeFiles(filePaths: string[]): Promise; + /** + * Sends doSyncOperation that will be handled by the runtime. + * @param doRefresh - Indicates if the application should be restarted. Defaults to true. + * @param operationId - The identifier of the operation + * @param timeout - The timeout in milliseconds + * @returns {Promise} + */ + sendDoSyncOperation(doRefresh: boolean, timeout?: number, operationId?: string): Promise; + /** + * Generates new operation identifier. + */ + generateOperationIdentifier(): string; + /** + * Checks if the current operation is in progress. + * @param operationId - The identifier of the operation. + */ + isOperationInProgress(operationId: string): boolean; + + /** + * Closes the current socket connection. + */ + end(): void; +} + +interface IAndroidLivesyncToolConfiguration { + /** + * The application identifier. + */ + appIdentifier: string; + /** + * The device identifier. + */ + deviceIdentifier: string; + /** + * The path to app folder inside platforms folder: platforms/android/app/src/main/assets/app/ + */ + appPlatformsPath: string; + /** + * If not provided, defaults to 127.0.0.1 + */ + localHostAddress?: string; + /** + * If provider will call it when an error occurs. + */ + errorHandler?: any; +} + +interface IAndroidLivesyncSyncOperationResult { + operationId: string, + didRefresh: boolean +} + interface IDeviceProjectRootOptions { appIdentifier: string; getDirname?: boolean; diff --git a/lib/device-path-provider.ts b/lib/device-path-provider.ts index 092d77eac2..c18d9a73ef 100644 --- a/lib/device-path-provider.ts +++ b/lib/device-path-provider.ts @@ -25,7 +25,7 @@ export class DevicePathProvider implements IDevicePathProvider { } else if (this.$mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) { projectRoot = `/data/local/tmp/${options.appIdentifier}`; if (!options.getDirname) { - const deviceLiveSyncService = this.$injector.resolve(AndroidDeviceLiveSyncService, { _device: device }); + const deviceLiveSyncService = this.$injector.resolve(AndroidDeviceLiveSyncService, { device }); const hashService = deviceLiveSyncService.getDeviceHashService(options.appIdentifier); const hashFile = options.syncAllFiles ? null : await hashService.doesShasumFileExistsOnDevice(); const syncFolderName = options.watch || hashFile ? LiveSyncPaths.SYNC_DIR_NAME : LiveSyncPaths.FULLSYNC_DIR_NAME; diff --git a/lib/services/livesync/android-device-livesync-service.ts b/lib/services/livesync/android-device-livesync-service.ts index c99f514c6a..c3703be1c8 100644 --- a/lib/services/livesync/android-device-livesync-service.ts +++ b/lib/services/livesync/android-device-livesync-service.ts @@ -8,17 +8,16 @@ import * as path from "path"; import * as net from "net"; export class AndroidDeviceLiveSyncService extends DeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { - private device: Mobile.IAndroidDevice; private port: number; - constructor(_device: Mobile.IDevice, + constructor( private $mobileHelper: Mobile.IMobileHelper, private $devicePathProvider: IDevicePathProvider, private $injector: IInjector, private $androidProcessService: Mobile.IAndroidProcessService, - protected $platformsData: IPlatformsData) { - super($platformsData); - this.device = (_device); + protected $platformsData: IPlatformsData, + protected device: Mobile.IAndroidDevice) { + super($platformsData, device); } public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise { @@ -53,9 +52,9 @@ export class AndroidDeviceLiveSyncService extends DeviceLiveSyncServiceBase impl getDirname: true }); - await this.device.adb.executeShellCommand(["rm", "-rf", await this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.FULLSYNC_DIR_NAME), + await this.device.adb.executeShellCommand(["rm", "-rf", this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.FULLSYNC_DIR_NAME), this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.SYNC_DIR_NAME), - await this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.REMOVEDSYNC_DIR_NAME)]); + this.$mobileHelper.buildDevicePath(deviceRootPath, LiveSyncPaths.REMOVEDSYNC_DIR_NAME)]); } private async restartApplication(deviceAppData: Mobile.IDeviceAppData, projectName: string): Promise { diff --git a/lib/services/livesync/android-device-livesync-sockets-service.ts b/lib/services/livesync/android-device-livesync-sockets-service.ts new file mode 100644 index 0000000000..586b5b0f74 --- /dev/null +++ b/lib/services/livesync/android-device-livesync-sockets-service.ts @@ -0,0 +1,135 @@ +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 { APP_FOLDER_NAME } from "../../constants"; +import { AndroidLivesyncTool } from "./android-livesync-tool"; +import * as path from "path"; + +export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBase implements IAndroidNativeScriptDeviceLiveSyncService, INativeScriptDeviceLiveSyncService { + private livesyncTool: IAndroidLivesyncTool; + private static STATUS_UPDATE_INTERVAL = 10000; + + constructor( + private data: IProjectData, + private $injector: IInjector, + protected $platformsData: IPlatformsData, + protected $staticConfig: Config.IStaticConfig, + private $logger: ILogger, + protected device: Mobile.IAndroidDevice, + private $options: ICommonOptions, + private $processService: IProcessService) { + super($platformsData, device); + this.livesyncTool = this.$injector.resolve(AndroidLivesyncTool); + } + + public async beforeLiveSyncAction(deviceAppData: Mobile.IDeviceAppData): Promise { + const platformData = this.$platformsData.getPlatformData(deviceAppData.platform, this.data); + const projectFilesPath = path.join(platformData.appDestinationDirectoryPath, APP_FOLDER_NAME); + await this.device.applicationManager.startApplication({ appId: deviceAppData.appIdentifier, projectName: this.data.projectName }); + await this.connectLivesyncTool(projectFilesPath, this.data.projectId); + } + + public async finalizeSync(liveSyncInfo: ILiveSyncResultInfo) { + await this.doSync(liveSyncInfo); + } + + private async doSync(liveSyncInfo: ILiveSyncResultInfo, {doRefresh = false}: {doRefresh?: boolean} = {}): Promise { + const operationId = this.livesyncTool.generateOperationIdentifier(); + + let result = {operationId, didRefresh: true }; + + if (liveSyncInfo.modifiedFilesData.length) { + + const doSyncPromise = this.livesyncTool.sendDoSyncOperation(doRefresh, null, operationId); + + const syncInterval : NodeJS.Timer = setInterval(() => { + if (this.livesyncTool.isOperationInProgress(operationId)) { + this.$logger.info("Sync operation in progress..."); + } + }, AndroidDeviceSocketsLiveSyncService.STATUS_UPDATE_INTERVAL); + + const clearSyncInterval = () => { + clearInterval(syncInterval); + }; + + this.$processService.attachToProcessExitSignals(this, clearSyncInterval); + doSyncPromise.then(clearSyncInterval, clearSyncInterval); + + result = await doSyncPromise; + } + + return result; + } + + public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo) { + const canExecuteFastSync = !liveSyncInfo.isFullSync && this.canExecuteFastSyncForPaths(liveSyncInfo.modifiedFilesData, projectData, this.device.deviceInfo.platform); + + const syncOperationResult = await this.doSync(liveSyncInfo, {doRefresh: canExecuteFastSync}); + + this.livesyncTool.end(); + + if (!canExecuteFastSync || !syncOperationResult.didRefresh) { + await this.device.applicationManager.restartApplication({ appId: liveSyncInfo.deviceAppData.appIdentifier, projectName: projectData.projectName }); + } + } + + public async removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise { + await this.livesyncTool.removeFiles(_.map(localToDevicePaths, (element: any) => element.filePath)); + } + + 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(localToDevicePaths); + } + + return transferredFiles; + } + + private async _transferFiles(localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise { + await this.livesyncTool.sendFiles(localToDevicePaths.map(localToDevicePathData => localToDevicePathData.getLocalPath())); + + return 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 = []; + } + } + + return transferredLocalToDevicePaths ; + } + + private async connectLivesyncTool(projectFilesPath: string, appIdentifier: string) { + await this.livesyncTool.connect({ + appIdentifier, + deviceIdentifier: this.device.deviceInfo.identifier, + appPlatformsPath: projectFilesPath + }); + } + + 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/android-livesync-service.ts b/lib/services/livesync/android-livesync-service.ts index ec33162b2c..481b2af57e 100644 --- a/lib/services/livesync/android-livesync-service.ts +++ b/lib/services/livesync/android-livesync-service.ts @@ -1,7 +1,10 @@ import { AndroidDeviceLiveSyncService } from "./android-device-livesync-service"; +import { AndroidDeviceSocketsLiveSyncService } from "./android-device-livesync-sockets-service"; import { PlatformLiveSyncServiceBase } from "./platform-livesync-service-base"; +import * as semver from "semver"; export class AndroidLiveSyncService extends PlatformLiveSyncServiceBase implements IPlatformLiveSyncService { + private static MIN_SOCKETS_LIVESYNC_RUNTIME_VERSION = "4.2.0-2018-07-20-02"; constructor(protected $platformsData: IPlatformsData, protected $projectFilesManager: IProjectFilesManager, private $injector: IInjector, @@ -12,9 +15,12 @@ export class AndroidLiveSyncService extends PlatformLiveSyncServiceBase implemen super($fs, $logger, $platformsData, $projectFilesManager, $devicePathProvider, $projectFilesProvider); } - protected _getDeviceLiveSyncService(device: Mobile.IDevice, data: IProjectDir): INativeScriptDeviceLiveSyncService { - const service = this.$injector.resolve(AndroidDeviceLiveSyncService, { _device: device, data }); - return service; + protected _getDeviceLiveSyncService(device: Mobile.IDevice, data: IProjectDir, frameworkVersion: string): INativeScriptDeviceLiveSyncService { + if (semver.gt(frameworkVersion, AndroidLiveSyncService.MIN_SOCKETS_LIVESYNC_RUNTIME_VERSION)) { + return this.$injector.resolve(AndroidDeviceSocketsLiveSyncService, { device, data }); + } + + return this.$injector.resolve(AndroidDeviceLiveSyncService, { device, data }); } public async prepareForLiveSync(device: Mobile.IDevice, data: IProjectDir): Promise { /* */ } diff --git a/lib/services/livesync/android-livesync-tool.md b/lib/services/livesync/android-livesync-tool.md new file mode 100644 index 0000000000..1ea6f60fe5 --- /dev/null +++ b/lib/services/livesync/android-livesync-tool.md @@ -0,0 +1,210 @@ +# android-livesync-tool +Tool for livesyncing changes to a NativeScript application on Android. + +## Usage +The tool has a few public methods that allow file manipulation to the files of a NativeScript application and provide control for refreshing the application. Restarting the application if necessary should be done by the user of this tool. + +### Getting an instance + +* Example: +```JavaScript +const globalModulesPath = require("global-modules-path"); +const cliPath = globalModulesPath.getPath("nativescript", "tns"); +cli = require(cliPath); + +const liveSyncTool = cli.androidLivesyncTool; +``` + + +### Calling connect +Connect method will establish a fresh socket connection with the application. The method takes a configuration as a parameter. + +* Definition +```TypeScript +interface ILivesyncToolConfiguration { + appIdentifier: string; + deviceIdentifier: string; + appPlatformsPath: string; // path to /c/myprojects/myapp/app/platforms/android/app/src/main/assets/app/ + localHostAddress?: string; + errorHandler?: any; +} + +/** + * Creates new socket connection. + * @param configuration - The configuration to the socket connection. + * @returns {Promise} + */ +connect(configuration: ILivesyncToolConfiguration): Promise; +``` + +* Example: +```JavaScript + +var configuration = { + appPlatformsPath: "/c/myprojects/myapp/app/platforms/android/app/src/main/assets/app/", + fullApplicationName: "com.tns.myapp", + deviceIdentifier: "aaaaaaaa" +} + +liveSyncTool.connect(configuration) +``` + +The method returns a promise which is resolved once the connection is established. There is a 30 seconds timeout for establishing the connection. In order the connection to be established successfully, the app must be started. + +### Calling sendFile +Send file will create/update the file with the file content it reads from the filePath that is provided. It will compute the relative path based on the fullApplicationName provided in configuration that was passed to the connect method. This method resolves its promise once the file is written to the output stream of the socket. To be sure the all files have been read and saved by the runtime see sendDoSyncOperation. + +* Definition +```TypeScript +/** + * Sends a file through the socket. + * @param filePath - The full path to the file. + * @returns {Promise} + */ +sendFile(filePath: string): Promise; +``` + +* Example: +```JavaScript +liveSyncTool.sendFile("/c/myprojects/myapp/app/platforms/android/app/src/main/assets/app/index.js"); +``` + +### Calling sendFiles +This method takes an array of file paths as an argument and sends their content to the application. + +* Definition +```TypeScript +/** + * Sends files through the socket. + * @param filePaths - Array of files that will be send by the socket. + * @returns {Promise} + */ +sendFiles(filePaths: string[]): Promise; +``` + +* Example: +```JavaScript +liveSyncTool.sendFile([ + "/c/myprojects/myapp/app/platforms/android/app/src/main/assets/app/index.js" + "/c/myprojects/myapp/app/platforms/android/app/src/main/assets/app/test.js" + "/c/myprojects/myapp/app/platforms/android/app/src/main/assets/app/page.js" +]); +``` + +### Calling sendDirectory +This method takes a path to a directory, enumerates the files recursively and sends the to the device. + +* Definition +```TypeScript +/** + * Sends all files from directory by the socket. + * @param directoryPath - The path to the directory which files will be send by the socket. + * @returns {Promise} + */ +sendDirectory(directoryPath: string): Promise; +``` + +* Example: +```JavaScript +liveSyncTool.sendDirectory("/c/myprojects/myapp/app/platforms/android/app/src/main/assets/app"); +``` + +### Calling removeFile +When called, removeFile will compute the relative path based on the fullApplicationName provided in configuration that was passed to the connect method and delete the corresponding file/directory on the device. + +* Definition +```TypeScript +/** + * Removes file + * @param filePath - The full path to the file. + * @returns {Promise} + */ +removeFile(filePath: string): Promise; +``` + +* Example: +```JavaScript +liveSyncTool.removeFile("/c/myprojects/myapp/app/platforms/android/app/src/main/assets/app/index.js"); +``` + +### Calling removeFiles +When called, removeFiles will compute the relative paths based on the fullApplicationName provided in configuration that was passed to the connect method and delete the corresponding files/directories on the device. + +* Definition +```TypeScript +/** + * Removes files + * @param filePaths - Array of files that will be removed. + * @returns {Promise} + */ +removeFiles(filePaths: string[]): Promise; +``` + +* Example: +```JavaScript +liveSyncTool.removeFiles([ + "/c/myprojects/myapp/app/platforms/android/app/src/main/assets/app/index.js" + "/c/myprojects/myapp/app/platforms/android/app/src/main/assets/app/test.js" + "/c/myprojects/myapp/app/platforms/android/app/src/main/assets/app/page.js" +]); +``` + +### Calling sendDoSyncOperation +When called, sendDoSyncOperation will tell the runtime to execute a script that will refresh the application(this will render changes to the .html and .css files). This method accepts an optional parameter - operationId. It can be used for status check of the operation. The promise returned from this method will be resolved when the application has read the operation and started executing it. This can be used as a sync point - once this promise is resolved, the user can be sure that all other operations have been read and executed by the application. The operation accepts an operation id + +* Definition +```TypeScript +/** + * Sends doSyncOperation that will be handled by the runtime. + * @param doRefresh - Indicates if the application should be restarted. Defaults to true. + * @param operationId - The identifier of the operation + * @param timeout - The timeout in milliseconds + * @returns {Promise} + */ +sendDoSyncOperation(doRefresh: boolean, timeout?: number, operationId?: string): Promise; +``` + +* Example: +```JavaScript +const operationId = liveSyncTool.generateOperationIdentifier(); +await liveSyncTool.sendDoSyncOperation(true, 10000, operationId); +``` + +### Calling end +End will close the current liveSync socket. Any sync operations that are still in progress will be rejected. + +* Definition +```TypeScript +/** + * Closes the current socket connection. + */ +end(): void; +``` + +* Example: +```JavaScript +liveSyncTool.end(); +``` + +## Protocol: + +Application input + +|Operation Name(not send) | Operation | Operation id | File Name Length Size | File Name Length | File Name | File Content Length Size | File Content Length | Header Hash | File Content | File hash | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| runSync: | 9 | 32bit | | | | | | md5 hash | | | +| create: | 8 | | 1 | 7 | ./a.txt | 2 | 11 | md5 hash | fileContent | md5 hash | +| delete: | 7 | | 1 | 3 | ./a | | | md5 hash | | | + +Application output on connect + +| Protocol Version length | Protocol Version String | Application Identifier | +| --- | --- | --- | +| 1 byte | "0.1.0" | "org.nativescript.myapp" | + +Application output after connect + +| Report Name(not send) | Report Code | Payload | +| --- | --- | --- | +| Error | 1 | Error message string | +| Sync end | 2 | Sync operation uid | \ No newline at end of file diff --git a/lib/services/livesync/android-livesync-tool.ts b/lib/services/livesync/android-livesync-tool.ts new file mode 100644 index 0000000000..238d94f3eb --- /dev/null +++ b/lib/services/livesync/android-livesync-tool.ts @@ -0,0 +1,422 @@ +import * as net from "net"; +import * as path from "path"; +import * as crypto from "crypto"; + +const PROTOCOL_VERSION_LENGTH_SIZE = 1; +const PROTOCOL_OPERATION_LENGTH_SIZE = 1; +const SIZE_BYTE_LENGTH = 1; +const DELETE_FILE_OPERATION = 7; +const CREATE_FILE_OPERATION = 8; +const DO_SYNC_OPERATION = 9; +const ERROR_REPORT = 1; +const OPERATION_END_REPORT = 2; +const OPERATION_END_NO_REFRESH_REPORT_CODE = 3; +const REPORT_LENGTH = 1; +const DO_REFRESH_LENGTH = 1; +const DO_REFRESH = 1; +const SKIP_REFRESH = 0; +const SYNC_OPERATION_TIMEOUT = 60000; +const TRY_CONNECT_TIMEOUT = 30000; +const DEFAULT_LOCAL_HOST_ADDRESS = "127.0.0.1"; + +export class AndroidLivesyncTool implements IAndroidLivesyncTool { + private operationPromises: IDictionary; + private socketError: string | Error; + private socketConnection: IDuplexSocket; + private configuration: IAndroidLivesyncToolConfiguration; + + constructor(private $androidProcessService: Mobile.IAndroidProcessService, + private $errors: IErrors, + private $fs: IFileSystem, + private $logger: ILogger, + private $mobileHelper: Mobile.IMobileHelper, + private $processService: IProcessService) { + this.operationPromises = Object.create(null); + this.socketError = null; + this.socketConnection = null; + this.$processService.attachToProcessExitSignals(this, this.dispose); + } + + public async connect(configuration: IAndroidLivesyncToolConfiguration): Promise { + if (!configuration.appIdentifier) { + this.$errors.fail(`You need to provide "appIdentifier" as a configuration property!`); + } + + if (!configuration.appPlatformsPath) { + this.$errors.fail(`You need to provide "baseDir" as a configuration property!`); + } + + if (this.socketConnection) { + this.$errors.fail("Socket connection already exists."); + } + + if (!configuration.localHostAddress) { + configuration.localHostAddress = DEFAULT_LOCAL_HOST_ADDRESS; + } + + this.configuration = configuration; + this.socketError = null; + + const port = await this.$androidProcessService.forwardFreeTcpToAbstractPort({ + deviceIdentifier: configuration.deviceIdentifier, + appIdentifier: configuration.appIdentifier, + abstractPort: `localabstract:${configuration.appIdentifier}-livesync` + }); + + const connectionResult = await this.connectEventuallyUntilTimeout(this.createSocket.bind(this, port), TRY_CONNECT_TIMEOUT); + this.handleConnection(connectionResult); + } + + public async sendFile(filePath: string): Promise { + await this.sendFileHeader(filePath); + await this.sendFileContent(filePath); + } + + public async sendFiles(filePaths: string[]) { + for (const filePath of filePaths) { + if (this.$fs.getLsStats(filePath).isFile()) { + if (!this.$fs.exists(filePath)) { + this.$errors.fail(`${filePath} doesn't exist.`); + } + + await this.sendFile(filePath); + } + } + } + + public sendDirectory(directoryPath: string) { + const list = this.$fs.enumerateFilesInDirectorySync(directoryPath); + return this.sendFiles(list); + } + + public removeFile(filePath: string): Promise { + return new Promise((resolve: Function, reject: Function) => { + this.verifyActiveConnection(reject); + const filePathData = this.getFilePathData(filePath); + const headerBuffer = Buffer.alloc(PROTOCOL_OPERATION_LENGTH_SIZE + + SIZE_BYTE_LENGTH + + filePathData.filePathLengthSize + + filePathData.filePathLengthBytes); + + let offset = 0; + offset += headerBuffer.write(DELETE_FILE_OPERATION.toString(), offset, PROTOCOL_OPERATION_LENGTH_SIZE); + offset = headerBuffer.writeInt8(filePathData.filePathLengthSize, offset); + offset += headerBuffer.write(filePathData.filePathLengthString, offset, filePathData.filePathLengthSize); + headerBuffer.write(filePathData.relativeFilePath, offset, filePathData.filePathLengthBytes); + const hash = crypto.createHash("md5").update(headerBuffer).digest(); + + this.socketConnection.write(headerBuffer); + this.socketConnection.write(hash, () => { + resolve(true); + }); + }); + } + + public removeFiles(files: string[]) { + return Promise.all(files.map(file => this.removeFile(file))); + } + + public generateOperationIdentifier(): string { + return crypto.randomBytes(16).toString("hex"); + } + + public isOperationInProgress(operationId: string): boolean { + return !!this.operationPromises[operationId]; + } + + public sendDoSyncOperation(doRefresh = true , timeout?: number, operationId?: string): Promise { + const id = operationId || this.generateOperationIdentifier(); + const operationPromise: Promise = new Promise((resolve: Function, reject: Function) => { + this.verifyActiveConnection(reject); + const message = `${DO_SYNC_OPERATION}${id}`; + const headerBuffer = Buffer.alloc(Buffer.byteLength(message) + DO_REFRESH_LENGTH); + const socketId = this.socketConnection.uid; + const doRefreshCode = doRefresh ? DO_REFRESH : SKIP_REFRESH; + const offset = headerBuffer.write(message); + + headerBuffer.writeUInt8(doRefreshCode, offset); + const hash = crypto.createHash("md5").update(headerBuffer).digest(); + + this.socketConnection.write(headerBuffer); + this.socketConnection.write(hash); + + timeout = timeout || SYNC_OPERATION_TIMEOUT; + const timeoutId = setTimeout(() => { + if (this.isOperationInProgress(id)) { + this.handleSocketError(socketId, "Sync operation is taking too long"); + } + }, timeout); + + this.operationPromises[id] = { + resolve, + reject, + socketId, + timeoutId + }; + }); + + return operationPromise; + } + + public end() { + if (this.socketConnection) { + this.socketConnection.end(); + } + } + + private sendFileHeader(filePath: string): Promise { + return new Promise((resolve, reject) => { + let error; + this.verifyActiveConnection(reject); + const filePathData = this.getFilePathData(filePath); + const stats = this.$fs.getFsStats(filePathData.filePath); + const fileContentLengthBytes = stats.size; + const fileContentLengthString = fileContentLengthBytes.toString(); + const fileContentLengthSize = Buffer.byteLength(fileContentLengthString); + const headerBuffer = Buffer.alloc(PROTOCOL_OPERATION_LENGTH_SIZE + + SIZE_BYTE_LENGTH + + filePathData.filePathLengthSize + + filePathData.filePathLengthBytes + + SIZE_BYTE_LENGTH + + fileContentLengthSize); + + if (filePathData.filePathLengthSize > 255) { + error = this.getErrorWithMessage("File name size is longer that 255 digits."); + } else if (fileContentLengthSize > 255) { + error = this.getErrorWithMessage("File name size is longer that 255 digits."); + } + + if (error) { + reject(error); + } + + let offset = 0; + offset += headerBuffer.write(CREATE_FILE_OPERATION.toString(), offset, PROTOCOL_OPERATION_LENGTH_SIZE); + offset = headerBuffer.writeUInt8(filePathData.filePathLengthSize, offset); + offset += headerBuffer.write(filePathData.filePathLengthString, offset, filePathData.filePathLengthSize); + offset += headerBuffer.write(filePathData.relativeFilePath, offset, filePathData.filePathLengthBytes); + offset = headerBuffer.writeUInt8(fileContentLengthSize, offset); + headerBuffer.write(fileContentLengthString, offset, fileContentLengthSize); + const hash = crypto.createHash("md5").update(headerBuffer).digest(); + + this.socketConnection.write(headerBuffer); + this.socketConnection.write(hash); + resolve(); + }); + } + + private sendFileContent(filePath: string): Promise { + return new Promise((resolve, reject) => { + this.verifyActiveConnection(reject); + const fileStream = this.$fs.createReadStream(filePath); + const fileHash = crypto.createHash("md5"); + + fileStream + .on("data", (chunk: string | Buffer) => { + fileHash.update(chunk); + if (this.socketConnection) { + this.socketConnection.write(chunk); + } else { + const error = this.checkConnectionStatus(); + //TODO Destroy method added in node 8.0.0. + //when we deprecate node 6.x uncomment the line below + //fileStream.destroy(error); + reject(error); + } + }) + .on("end", () => { + if (this.socketConnection) { + this.socketConnection.write(fileHash.digest(), () => { + resolve(true); + }); + } else { + const error = this.checkConnectionStatus(); + reject(error); + } + }) + .on("error", (error: Error) => { + reject(error); + }); + }); + } + + private createSocket(port: number): IDuplexSocket { + const socket = new net.Socket(); + socket.connect(port, this.configuration.localHostAddress); + return socket; + } + + private checkConnectionStatus() { + if (this.socketConnection === null) { + const defaultError = this.getErrorWithMessage("No socket connection available."); + const error = this.socketError || defaultError; + + return error; + } + } + + private verifyActiveConnection(rejectHandler?: any) { + const error = this.checkConnectionStatus(); + if (error && rejectHandler) { + rejectHandler(error); + } + + if (error && !rejectHandler) { + this.$errors.failWithoutHelp(error.toString()); + } + } + + private handleConnection({ socket, data }: { socket: IDuplexSocket, data: NodeBuffer | string }) { + this.socketConnection = socket; + this.socketConnection.uid = this.generateOperationIdentifier(); + + const versionLength = (data).readUInt8(0); + const versionBuffer = data.slice(PROTOCOL_VERSION_LENGTH_SIZE, versionLength + PROTOCOL_VERSION_LENGTH_SIZE); + const appIdentifierBuffer = data.slice(versionLength + PROTOCOL_VERSION_LENGTH_SIZE, data.length); + + const protocolVersion = versionBuffer.toString(); + const appIdentifier = appIdentifierBuffer.toString(); + this.$logger.trace(`Handle socket connection for app identifier: ${appIdentifier} with protocol version: ${protocolVersion}.`); + + this.socketConnection.on("data", (connectionData: NodeBuffer) => this.handleData(socket.uid, connectionData)); + this.socketConnection.on("close", (hasError: boolean) => this.handleSocketClose(socket.uid, hasError)); + this.socketConnection.on("error", (err: Error) => { + const error = new Error(`Socket Error:\n${err}`); + if (this.configuration.errorHandler) { + this.configuration.errorHandler(error); + } else { + this.handleSocketError(socket.uid, error.message); + } + }); + } + + private connectEventuallyUntilTimeout(factory: () => IDuplexSocket, timeout: number): Promise<{socket: IDuplexSocket, data: NodeBuffer | string}> { + return new Promise((resolve, reject) => { + let lastKnownError: Error | string, + isConnected = false; + + setTimeout(() => { + if (!isConnected) { + isConnected = true; + reject(lastKnownError); + } + }, timeout); + + const tryConnect = () => { + const tryConnectAfterTimeout = (error: Error) => { + if (isConnected) { + return; + } + + if (typeof (error) === "boolean" && error) { + error = new Error("Socket closed due to error"); + } + + lastKnownError = error; + setTimeout(tryConnect, 1000); + }; + + const socket = factory(); + + socket.once("data", data => { + socket.removeListener("close", tryConnectAfterTimeout); + socket.removeListener("error", tryConnectAfterTimeout); + isConnected = true; + resolve({ socket, data }); + }); + socket.on("close", tryConnectAfterTimeout); + socket.on("error", tryConnectAfterTimeout); + }; + + tryConnect(); + }); + } + + private handleData(socketId: string, data: any) { + const reportType = data.readUInt8(); + const infoBuffer = data.slice(REPORT_LENGTH, data.length); + + if (reportType === ERROR_REPORT) { + const errorMessage = infoBuffer.toString(); + this.handleSocketError(socketId, errorMessage); + } else if (reportType === OPERATION_END_REPORT) { + this.handleSyncEnd({data:infoBuffer, didRefresh: true}); + } else if (reportType === OPERATION_END_NO_REFRESH_REPORT_CODE) { + this.handleSyncEnd({data:infoBuffer, didRefresh: false}); + } + } + + private handleSyncEnd({data, didRefresh}: {data: any, didRefresh: boolean}) { + const operationId = data.toString(); + const promiseHandler = this.operationPromises[operationId]; + + if (promiseHandler) { + clearTimeout(promiseHandler.timeoutId); + promiseHandler.resolve({operationId, didRefresh}); + delete this.operationPromises[operationId]; + } + } + + private handleSocketClose(socketId: string, hasError: boolean) { + const errorMessage = "Socket closed from server before operation end."; + this.handleSocketError(socketId, errorMessage); + } + + private handleSocketError(socketId: string, errorMessage: string) { + const error = this.getErrorWithMessage(errorMessage); + if (this.socketConnection && this.socketConnection.uid === socketId) { + this.end(); + this.socketConnection = null; + this.socketError = error; + } + + _.keys(this.operationPromises) + .forEach(operationId => { + const operationPromise = this.operationPromises[operationId]; + if (operationPromise.socketId === socketId) { + clearTimeout(operationPromise.timeoutId); + operationPromise.reject(error); + delete this.operationPromises[operationId]; + } + }); + } + + private getErrorWithMessage(errorMessage: string) { + const error = new Error(errorMessage); + error.message = errorMessage; + + return error; + } + + private getFilePathData(filePath: string): { relativeFilePath: string, filePathLengthBytes: number, filePathLengthString: string, filePathLengthSize: number, filePath: string } { + const relativeFilePath = this.resolveRelativePath(filePath); + const filePathLengthBytes = Buffer.byteLength(relativeFilePath); + const filePathLengthString = filePathLengthBytes.toString(); + const filePathLengthSize = Buffer.byteLength(filePathLengthString); + + return { + relativeFilePath, + filePathLengthBytes, + filePathLengthString, + filePathLengthSize, + filePath + }; + } + + private resolveRelativePath(filePath: string): string { + const relativeFilePath = path.relative(this.configuration.appPlatformsPath, filePath); + + return this.$mobileHelper.buildDevicePath(relativeFilePath); + } + + private dispose(): void { + this.end(); + + _.keys(this.operationPromises) + .forEach(operationId => { + const operationPromise = this.operationPromises[operationId]; + clearTimeout(operationPromise.timeoutId); + }); + } +} +$injector.register("androidLivesyncTool", AndroidLivesyncTool); diff --git a/lib/services/livesync/device-livesync-service-base.ts b/lib/services/livesync/device-livesync-service-base.ts index bb1f44961f..6bf76cbac1 100644 --- a/lib/services/livesync/device-livesync-service-base.ts +++ b/lib/services/livesync/device-livesync-service-base.ts @@ -4,13 +4,22 @@ import * as path from "path"; export abstract class DeviceLiveSyncServiceBase { private static FAST_SYNC_FILE_EXTENSIONS = [".css", ".xml", ".html"]; - constructor(protected $platformsData: IPlatformsData) { } + constructor( + protected $platformsData: IPlatformsData, + protected device: Mobile.IDevice + ) { } public canExecuteFastSync(filePath: string, projectData: IProjectData, platform: string): boolean { const fastSyncFileExtensions = this.getFastLiveSyncFileExtensions(platform, projectData); return _.includes(fastSyncFileExtensions, path.extname(filePath)); } + protected canExecuteFastSyncForPaths(localToDevicePaths: Mobile.ILocalToDevicePathData[], projectData: IProjectData, platform: string) { + return !_.some(localToDevicePaths, + (localToDevicePath: Mobile.ILocalToDevicePathData) => + !this.canExecuteFastSync(localToDevicePath.getLocalPath(), projectData, this.device.deviceInfo.platform)); + } + @cache() private getFastLiveSyncFileExtensions(platform: string, projectData: IProjectData): string[] { const platformData = this.$platformsData.getPlatformData(platform, projectData); @@ -18,4 +27,19 @@ export abstract class DeviceLiveSyncServiceBase { return fastSyncFileExtensions; } + public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { + let transferredFiles: Mobile.ILocalToDevicePathData[] = []; + + if (isFullSync) { + transferredFiles = await this.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); + } else { + transferredFiles = await this.device.fileSystem.transferFiles(deviceAppData, localToDevicePaths); + } + + return transferredFiles; + } + + public async finalizeSync(liveSyncInfo: ILiveSyncResultInfo): Promise { + //implement in case a sync point for all remove/create operation is needed + } } diff --git a/lib/services/livesync/ios-device-livesync-service.ts b/lib/services/livesync/ios-device-livesync-service.ts index a797d11d09..c857e5a3ec 100644 --- a/lib/services/livesync/ios-device-livesync-service.ts +++ b/lib/services/livesync/ios-device-livesync-service.ts @@ -7,10 +7,8 @@ let currentPageReloadId = 0; export class IOSDeviceLiveSyncService extends DeviceLiveSyncServiceBase implements INativeScriptDeviceLiveSyncService { private socket: net.Socket; - private device: Mobile.IiOSDevice; - constructor(_device: Mobile.IiOSDevice, - data: IProjectDir, + constructor( private $iOSSocketRequestExecutor: IiOSSocketRequestExecutor, private $iOSNotification: IiOSNotification, private $iOSEmulatorServices: Mobile.IiOSSimulatorService, @@ -18,9 +16,9 @@ export class IOSDeviceLiveSyncService extends DeviceLiveSyncServiceBase implemen private $logger: ILogger, private $fs: IFileSystem, private $processService: IProcessService, - protected $platformsData: IPlatformsData) { - super($platformsData); - this.device = _device; + protected $platformsData: IPlatformsData, + protected device: Mobile.IiOSDevice) { + super($platformsData, device); } private async setupSocketIfNeeded(projectData: IProjectData): Promise { @@ -63,7 +61,7 @@ export class IOSDeviceLiveSyncService extends DeviceLiveSyncServiceBase implemen constants.LIVESYNC_EXCLUDED_FILE_PATTERNS.forEach(pattern => scriptRelatedFiles = _.concat(scriptRelatedFiles, localToDevicePaths.filter(file => minimatch(file.getDevicePath(), pattern, { nocase: true })))); const otherFiles = _.difference(localToDevicePaths, _.concat(scriptFiles, scriptRelatedFiles)); - const shouldRestart = _.some(otherFiles, (localToDevicePath: Mobile.ILocalToDevicePathData) => !this.canExecuteFastSync(localToDevicePath.getLocalPath(), projectData, deviceAppData.platform)); + const shouldRestart = this.canExecuteFastSyncForPaths(otherFiles, projectData, deviceAppData.platform); if (shouldRestart || (!liveSyncInfo.useLiveEdit && scriptFiles.length)) { await this.restartApplication(deviceAppData, projectData.projectName); diff --git a/lib/services/livesync/ios-livesync-service.ts b/lib/services/livesync/ios-livesync-service.ts index 19264718d8..ae9c0a1bc5 100644 --- a/lib/services/livesync/ios-livesync-service.ts +++ b/lib/services/livesync/ios-livesync-service.ts @@ -72,7 +72,7 @@ export class IOSLiveSyncService extends PlatformLiveSyncServiceBase implements I } protected _getDeviceLiveSyncService(device: Mobile.IDevice, data: IProjectDir): INativeScriptDeviceLiveSyncService { - const service = this.$injector.resolve(IOSDeviceLiveSyncService, { _device: device, data }); + const service = this.$injector.resolve(IOSDeviceLiveSyncService, { device, data }); return service; } } diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index 5a6de53cd0..ad17982098 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -164,6 +164,9 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi }; try { + const platformLiveSyncService = this.getLiveSyncService(liveSyncResultInfo.deviceAppData.platform); + const deviceLivesyncService = platformLiveSyncService.getDeviceLiveSyncService(deviceAppData.device, projectData); + await deviceLivesyncService.finalizeSync(liveSyncResultInfo); await deviceAppData.device.applicationManager.stopApplication({ appId: applicationId, projectName: projectData.projectName }); // Now that we've stopped the application we know it isn't started, so set debugOptions.start to false // so that it doesn't default to true in attachDebugger method diff --git a/lib/services/livesync/platform-livesync-service-base.ts b/lib/services/livesync/platform-livesync-service-base.ts index 8c25c182fd..30bb8db5e9 100644 --- a/lib/services/livesync/platform-livesync-service-base.ts +++ b/lib/services/livesync/platform-livesync-service-base.ts @@ -1,6 +1,7 @@ import * as path from "path"; import * as util from "util"; import { APP_FOLDER_NAME } from "../../constants"; +import { getHash } from "../../common/helpers"; export abstract class PlatformLiveSyncServiceBase { private _deviceLiveSyncServicesCache: IDictionary = {}; @@ -13,15 +14,17 @@ export abstract class PlatformLiveSyncServiceBase { private $projectFilesProvider: IProjectFilesProvider) { } public getDeviceLiveSyncService(device: Mobile.IDevice, projectData: IProjectData): INativeScriptDeviceLiveSyncService { - const key = device.deviceInfo.identifier + projectData.projectId; + const platformData = this.$platformsData.getPlatformData(device.deviceInfo.platform, projectData); + const frameworkVersion = platformData.platformProjectService.getFrameworkVersion(projectData); + const key = getHash(`${device.deviceInfo.identifier}${projectData.projectId}${projectData.projectDir}${frameworkVersion}`); if (!this._deviceLiveSyncServicesCache[key]) { - this._deviceLiveSyncServicesCache[key] = this._getDeviceLiveSyncService(device, projectData); + this._deviceLiveSyncServicesCache[key] = this._getDeviceLiveSyncService(device, projectData, frameworkVersion); } return this._deviceLiveSyncServicesCache[key]; } - protected abstract _getDeviceLiveSyncService(device: Mobile.IDevice, data: IProjectDir): INativeScriptDeviceLiveSyncService; + protected abstract _getDeviceLiveSyncService(device: Mobile.IDevice, data: IProjectData, frameworkVersion: string): INativeScriptDeviceLiveSyncService; public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise { if (liveSyncInfo.isFullSync || liveSyncInfo.modifiedFilesData.length) { @@ -44,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); + const modifiedFilesData = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, true, projectData); return { modifiedFilesData, @@ -55,9 +58,14 @@ export abstract class PlatformLiveSyncServiceBase { public async liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise { const projectData = liveSyncInfo.projectData; + const deviceLiveSyncService = this.getDeviceLiveSyncService(device, projectData); const syncInfo = _.merge({ device, watch: true }, liveSyncInfo); const deviceAppData = await this.getAppData(syncInfo); + if (deviceLiveSyncService.beforeLiveSyncAction) { + await deviceLiveSyncService.beforeLiveSyncAction(deviceAppData); + } + let modifiedLocalToDevicePaths: Mobile.ILocalToDevicePathData[] = []; if (liveSyncInfo.filesToSync.length) { const filesToSync = liveSyncInfo.filesToSync; @@ -77,7 +85,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); + modifiedLocalToDevicePaths = await this.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, false, projectData); } } @@ -93,8 +101,7 @@ export abstract class PlatformLiveSyncServiceBase { const localToDevicePaths = await this.$projectFilesManager.createLocalToDevicePaths(deviceAppData, projectFilesPath, mappedFiles, []); modifiedLocalToDevicePaths.push(...localToDevicePaths); - const deviceLiveSyncService = this.getDeviceLiveSyncService(device, projectData); - await deviceLiveSyncService.removeFiles(deviceAppData, localToDevicePaths); + await deviceLiveSyncService.removeFiles(deviceAppData, localToDevicePaths, projectFilesPath); } return { @@ -104,13 +111,11 @@ export abstract class PlatformLiveSyncServiceBase { }; } - protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise { + protected async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean, projectData: IProjectData): Promise { let transferredFiles: Mobile.ILocalToDevicePathData[] = []; - if (isFullSync) { - transferredFiles = await deviceAppData.device.fileSystem.transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath); - } else { - transferredFiles = await deviceAppData.device.fileSystem.transferFiles(deviceAppData, localToDevicePaths); - } + const deviceLiveSyncService = this.getDeviceLiveSyncService(deviceAppData.device, projectData); + + transferredFiles = await deviceLiveSyncService.transferFiles(deviceAppData, localToDevicePaths, projectFilesPath, isFullSync); this.logFilesSyncInformation(transferredFiles, "Successfully transferred %s.", this.$logger.info);