-
-
Notifications
You must be signed in to change notification settings - Fork 197
Kddimitrov/android livesync sockets #3746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cf67b15
27babb8
d9f8cb8
688e431
4e3477e
fb8af69
0a62b82
23af849
4e7e815
91d3a31
dc600e1
645b527
c0a1ec3
39f7db4
911b61f
2d8039b
e22a361
294db1b
d42cd90
5ffaba7
460b4a4
9273366
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import * as stream from "stream"; | ||
|
||
declare global { | ||
interface IDuplexSocket extends stream.Duplex { | ||
uid?: string; | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -344,6 +344,7 @@ interface IPlatformLiveSyncService { | |
liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise<ILiveSyncResultInfo>; | ||
refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise<void>; | ||
prepareForLiveSync(device: Mobile.IDevice, data: IProjectDir, liveSyncInfo: ILiveSyncInfo, debugOptions: IDebugOptions): Promise<void>; | ||
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<void>} | ||
*/ | ||
removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise<void>; | ||
removeFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath?: string): Promise<void>; | ||
|
||
/** | ||
* 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<Mobile.ILocalToDevicePathData[]>} Returns the ILocalToDevicePathData of all transfered files | ||
*/ | ||
transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise<Mobile.ILocalToDevicePathData[]>; | ||
|
||
/** | ||
* 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<void>} | ||
*/ | ||
finalizeSync(liveSyncInfo: ILiveSyncResultInfo): Promise<void>; | ||
} | ||
|
||
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<void>} | ||
*/ | ||
connect(configuration: IAndroidLivesyncToolConfiguration): Promise<void>; | ||
/** | ||
* Sends a file through the socket. | ||
* @param filePath - The full path to the file. | ||
* @returns {Promise<void>} | ||
*/ | ||
sendFile(filePath: string): Promise<void>; | ||
/** | ||
* Sends files through the socket. | ||
* @param filePaths - Array of files that will be send by the socket. | ||
* @returns {Promise<void>} | ||
*/ | ||
sendFiles(filePaths: string[]): Promise<void>; | ||
/** | ||
* 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<void>} | ||
*/ | ||
sendDirectory(directoryPath: string): Promise<void>; | ||
/** | ||
* Removes file | ||
* @param filePath - The full path to the file. | ||
* @returns {Promise<boolean>} | ||
*/ | ||
removeFile(filePath: string): Promise<boolean>; | ||
/** | ||
* Removes files | ||
* @param filePaths - Array of files that will be removed. | ||
* @returns {Promise<boolean[]>} | ||
*/ | ||
removeFiles(filePaths: string[]): Promise<boolean[]>; | ||
/** | ||
* 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<void>} | ||
*/ | ||
sendDoSyncOperation(doRefresh: boolean, timeout?: number, operationId?: string): Promise<IAndroidLivesyncSyncOperationResult>; | ||
/** | ||
* 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this the opposite of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The main reason is the Socket API where the methods are actually |
||
} | ||
|
||
interface IAndroidLivesyncToolConfiguration { | ||
/** | ||
* The application identifier. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a rule for commenting а self-documented code? I don't see any additional value from such comments. |
||
*/ | ||
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; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void> { | ||
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<IAndroidLivesyncSyncOperationResult> { | ||
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<void> { | ||
await this.livesyncTool.removeFiles(_.map(localToDevicePaths, (element: any) => element.filePath)); | ||
} | ||
|
||
public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise<Mobile.ILocalToDevicePathData[]> { | ||
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<Mobile.ILocalToDevicePathData[]> { | ||
await this.livesyncTool.sendFiles(localToDevicePaths.map(localToDevicePathData => localToDevicePathData.getLocalPath())); | ||
|
||
return localToDevicePaths; | ||
} | ||
|
||
private async _transferDirectory(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string): Promise<Mobile.ILocalToDevicePathData[]> { | ||
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 }); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we handle the timeouts and the logging using just the
sendDoSyncOperation
promise without thegenerateOperationIdentifier
andisOperationInProgress
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible, but won't look good. As Promise has no property/method to check its state we have to attach to both resolve/reject and raise a local flag to indicate the state inside the interval function. It is again my personal preference to have the library provide the functionality.