Skip to content

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

Merged
merged 22 commits into from
Jul 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cf67b15
Initial integration of sockets tool
KristianDD Jun 18, 2018
27babb8
Generate hashes and init connection on beforeLiveSyncAction
KristianDD Jun 22, 2018
d9f8cb8
Await sync before restart. Update checksums.
KristianDD Jun 27, 2018
688e431
chore: improve code
KristianDD Jul 3, 2018
4e3477e
Add interval for syc wait feedback
KristianDD Jul 6, 2018
fb8af69
Move https://github.com/NativeScript/nativescript-android-livesync-li…
Fatme Jul 10, 2018
0a62b82
Rename android livesync-library and extract as public class
KristianDD Jul 10, 2018
23af849
Regenerate npm-shrinkwrap file
Fatme Jul 12, 2018
4e7e815
Hook socket livesync with exit signals.
KristianDD Jul 12, 2018
91d3a31
Cleanul livesync with sockets code.
KristianDD Jul 12, 2018
dc600e1
fix: resovle ios livesync device service
Jul 13, 2018
645b527
fix: requirement for runtime version supporting sockets LS
KristianDD Jul 13, 2018
c0a1ec3
docs: add documentation to the android-livesync-library
KristianDD Jul 13, 2018
39f7db4
Fix PR comments
Fatme Jul 16, 2018
911b61f
fix: app not restarted if in error activity
KristianDD Jul 16, 2018
2d8039b
chore: fix comments of PR
KristianDD Jul 17, 2018
e22a361
fix: unnecessary refresh before restart
KristianDD Jul 19, 2018
294db1b
fix: timout not cleared on exit signals
KristianDD Jul 20, 2018
d42cd90
fix: debug doesn't await file trasfer end with socket implementation
KristianDD Jul 23, 2018
5ffaba7
chore: fix comments
KristianDD Jul 23, 2018
460b4a4
chore: update LS with sockets required runtime version
KristianDD Jul 24, 2018
9273366
chore: update common lib
KristianDD Jul 24, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions lib/definitions/livesync-global.d.ts
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;
}
}

110 changes: 109 additions & 1 deletion lib/definitions/livesync.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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;
Copy link
Contributor

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 the generateOperationIdentifier and isOperationInProgress?

Copy link
Contributor Author

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.


/**
* Closes the current socket connection.
*/
end(): void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the opposite of the connect method? It could be disconnect instead of end.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason is the Socket API where the methods are actually connect and end. There is still a destroy method, but its used when an exception happened - reference.

}

interface IAndroidLivesyncToolConfiguration {
/**
* The application identifier.
Copy link
Contributor

@DimitarTachev DimitarTachev Jul 16, 2018

Choose a reason for hiding this comment

The 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;
Expand Down
2 changes: 1 addition & 1 deletion lib/device-path-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(AndroidDeviceLiveSyncService, { _device: device });
const deviceLiveSyncService = this.$injector.resolve<AndroidDeviceLiveSyncService>(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;
Expand Down
13 changes: 6 additions & 7 deletions lib/services/livesync/android-device-livesync-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <Mobile.IAndroidDevice>(_device);
protected $platformsData: IPlatformsData,
protected device: Mobile.IAndroidDevice) {
super($platformsData, device);
}

public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo): Promise<void> {
Expand Down Expand Up @@ -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<void> {
Expand Down
135 changes: 135 additions & 0 deletions lib/services/livesync/android-device-livesync-sockets-service.ts
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 });
}
}
12 changes: 9 additions & 3 deletions lib/services/livesync/android-livesync-service.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<INativeScriptDeviceLiveSyncService>(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<INativeScriptDeviceLiveSyncService>(AndroidDeviceSocketsLiveSyncService, { device, data });
}

return this.$injector.resolve<INativeScriptDeviceLiveSyncService>(AndroidDeviceLiveSyncService, { device, data });
}

public async prepareForLiveSync(device: Mobile.IDevice, data: IProjectDir): Promise<void> { /* */ }
Expand Down
Loading