Skip to content

Fix android livesync with sockets #3789

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 3 commits into from
Aug 3, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions lib/definitions/livesync-global.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as stream from "stream";
import { Socket } from "net";

declare global {
interface IDuplexSocket extends stream.Duplex {
interface INetSocket extends Socket {
uid?: string;
}
}
Expand Down
18 changes: 10 additions & 8 deletions lib/definitions/livesync.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ interface ILiveSyncResultInfo {
useLiveEdit?: boolean;
}

interface IAndroidLiveSyncResultInfo extends ILiveSyncResultInfo, IAndroidLivesyncSyncOperationResult { }

interface IFullSyncInfo extends IProjectDataComposition {
device: Mobile.IDevice;
watch: boolean;
Expand Down Expand Up @@ -377,22 +379,22 @@ interface INativeScriptDeviceLiveSyncService extends IDeviceLiveSyncServiceBase
* @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 {
interface IAndroidNativeScriptDeviceLiveSyncService extends INativeScriptDeviceLiveSyncService {
/**
* Retrieves the android device's hash service.
* @param {string} appIdentifier Application identifier.
* @return {Promise<Mobile.IAndroidDeviceHashService>} The hash service
*/
getDeviceHashService(appIdentifier: string): Mobile.IAndroidDeviceHashService;

/**
* 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<IAndroidLiveSyncResultInfo>}
*/
finalizeSync(liveSyncInfo: ILiveSyncResultInfo, projectData: IProjectData): Promise<IAndroidLivesyncSyncOperationResult>;
}

interface IAndroidLivesyncTool {
Expand Down
44 changes: 27 additions & 17 deletions lib/services/livesync/android-device-livesync-sockets-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,23 @@ export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBa
return `${LiveSyncPaths.ANDROID_TMP_DIR_NAME}/${appIdentifier}-livesync-in-progress`;
}

public async finalizeSync(liveSyncInfo: ILiveSyncResultInfo) {
await this.doSync(liveSyncInfo);
public async finalizeSync(liveSyncInfo: ILiveSyncResultInfo, projectData: IProjectData): Promise<IAndroidLivesyncSyncOperationResult> {
try {
const result = await this.doSync(liveSyncInfo, projectData);
return result;
} finally {
this.livesyncTool.end();
}
}

private async doSync(liveSyncInfo: ILiveSyncResultInfo, { doRefresh = false }: { doRefresh?: boolean } = {}): Promise<IAndroidLivesyncSyncOperationResult> {
private async doSync(liveSyncInfo: ILiveSyncResultInfo, projectData: IProjectData): 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 canExecuteFastSync = !liveSyncInfo.isFullSync && this.canExecuteFastSyncForPaths(liveSyncInfo.modifiedFilesData, projectData, this.device.deviceInfo.platform);
const doSyncPromise = this.livesyncTool.sendDoSyncOperation(canExecuteFastSync, null, operationId);

const syncInterval: NodeJS.Timer = setInterval(() => {
if (this.livesyncTool.isOperationInProgress(operationId)) {
Expand All @@ -64,30 +69,29 @@ export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBa
};

this.$processService.attachToProcessExitSignals(this, actionOnEnd);
doSyncPromise.then(actionOnEnd, actionOnEnd);
// We need to clear resources when the action fails
// But we also need the real result of the action.
await doSyncPromise.then(actionOnEnd.bind(this), actionOnEnd.bind(this));

result = await doSyncPromise;
} else {
await this.device.fileSystem.deleteFile(this.getPathToLiveSyncFileOnDevice(liveSyncInfo.deviceAppData.appIdentifier), liveSyncInfo.deviceAppData.appIdentifier);
}

await this.device.fileSystem.deleteFile(this.getPathToLiveSyncFileOnDevice(liveSyncInfo.deviceAppData.appIdentifier), liveSyncInfo.deviceAppData.appIdentifier);

return result;
}

public async refreshApplication(projectData: IProjectData, liveSyncInfo: ILiveSyncResultInfo) {
public async refreshApplication(projectData: IProjectData, liveSyncInfo: IAndroidLiveSyncResultInfo) {
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) {
if (!canExecuteFastSync || !liveSyncInfo.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));

await this.getDeviceHashService(deviceAppData.appIdentifier).removeHashes(localToDevicePaths);
}

public async transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[], projectFilesPath: string, isFullSync: boolean): Promise<Mobile.ILocalToDevicePathData[]> {
Expand All @@ -96,15 +100,21 @@ export class AndroidDeviceSocketsLiveSyncService extends DeviceLiveSyncServiceBa
if (isFullSync) {
transferredFiles = await this._transferDirectory(deviceAppData, localToDevicePaths, projectFilesPath);
} else {
transferredFiles = await this._transferFiles(localToDevicePaths);
transferredFiles = await this._transferFiles(deviceAppData, localToDevicePaths);
}

return transferredFiles;
}

private async _transferFiles(localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise<Mobile.ILocalToDevicePathData[]> {
private async _transferFiles(deviceAppData: Mobile.IDeviceAppData, localToDevicePaths: Mobile.ILocalToDevicePathData[]): Promise<Mobile.ILocalToDevicePathData[]> {
await this.livesyncTool.sendFiles(localToDevicePaths.map(localToDevicePathData => localToDevicePathData.getLocalPath()));

// Update hashes
const deviceHashService = this.getDeviceHashService(deviceAppData.appIdentifier);
if (! await deviceHashService.updateHashes(localToDevicePaths)) {
this.$logger.trace("Unable to find hash file on device. The next livesync command will create it.");
}

return localToDevicePaths;
}

Expand Down
19 changes: 19 additions & 0 deletions lib/services/livesync/android-livesync-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ export class AndroidLiveSyncService extends PlatformLiveSyncServiceBase implemen
return this.$injector.resolve<INativeScriptDeviceLiveSyncService>(AndroidDeviceLiveSyncService, { device, data });
}

public async liveSyncWatchAction(device: Mobile.IDevice, liveSyncInfo: ILiveSyncWatchInfo): Promise<IAndroidLiveSyncResultInfo> {
const liveSyncResult = await super.liveSyncWatchAction(device, liveSyncInfo);
const result = await this.finalizeSync(device, liveSyncInfo.projectData, liveSyncResult);
return result;
}

public async fullSync(syncInfo: IFullSyncInfo): Promise<IAndroidLiveSyncResultInfo> {
const liveSyncResult = await super.fullSync(syncInfo);
const result = await this.finalizeSync(syncInfo.device, syncInfo.projectData, liveSyncResult);
return result;
}

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

private async finalizeSync(device: Mobile.IDevice, projectData: IProjectData, liveSyncResult: ILiveSyncResultInfo): Promise<IAndroidLiveSyncResultInfo> {
const liveSyncService = <IAndroidNativeScriptDeviceLiveSyncService>this.getDeviceLiveSyncService(device, projectData);
const finalizeResult = await liveSyncService.finalizeSync(liveSyncResult, projectData);
const result = _.extend(liveSyncResult, finalizeResult);
return result;
}
}
$injector.register("androidLiveSyncService", AndroidLiveSyncService);
13 changes: 7 additions & 6 deletions lib/services/livesync/android-livesync-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ const DEFAULT_LOCAL_HOST_ADDRESS = "127.0.0.1";
export class AndroidLivesyncTool implements IAndroidLivesyncTool {
private operationPromises: IDictionary<any>;
private socketError: string | Error;
private socketConnection: IDuplexSocket;
private socketConnection: INetSocket;
private configuration: IAndroidLivesyncToolConfiguration;
private pendingConnectionData: {
connectionTimer?: NodeJS.Timer,
socketTimer?: NodeJS.Timer,
rejectHandler?: Function,
socket?: IDuplexSocket
socket?: INetSocket
} = null;

constructor(private $androidProcessService: Mobile.IAndroidProcessService,
Expand Down Expand Up @@ -173,6 +173,7 @@ export class AndroidLivesyncTool implements IAndroidLivesyncTool {
this.cleanState(socketUid);
//call end of the connection (close and error callbacks won't be called - listeners removed)
socket.end();
socket.destroy();
//reject all pending sync requests and clear timeouts
this.rejectPendingSyncOperations(socketUid, error);
}
Expand Down Expand Up @@ -254,7 +255,7 @@ export class AndroidLivesyncTool implements IAndroidLivesyncTool {
});
}

private createSocket(port: number): IDuplexSocket {
private createSocket(port: number): INetSocket {
const socket = new net.Socket();
socket.connect(port, this.configuration.localHostAddress);
return socket;
Expand All @@ -280,7 +281,7 @@ export class AndroidLivesyncTool implements IAndroidLivesyncTool {
}
}

private handleConnection({ socket, data }: { socket: IDuplexSocket, data: NodeBuffer | string }) {
private handleConnection({ socket, data }: { socket: INetSocket, data: NodeBuffer | string }) {
this.socketConnection = socket;
this.socketConnection.uid = this.generateOperationIdentifier();

Expand All @@ -304,15 +305,15 @@ export class AndroidLivesyncTool implements IAndroidLivesyncTool {
});
}

private connectEventuallyUntilTimeout(factory: () => IDuplexSocket, timeout: number): Promise<{socket: IDuplexSocket, data: NodeBuffer | string}> {
private connectEventuallyUntilTimeout(factory: () => INetSocket, timeout: number): Promise<{socket: INetSocket, data: NodeBuffer | string}> {
return new Promise((resolve, reject) => {
let lastKnownError: Error | string,
isConnected = false;

const connectionTimer = setTimeout(() => {
if (!isConnected) {
isConnected = true;
reject(lastKnownError || { message: "Socket connection timeouted." });
reject(lastKnownError || new Error("Socket connection timeouted."));
this.pendingConnectionData = null;
}
}, timeout);
Expand Down
6 changes: 5 additions & 1 deletion lib/services/livesync/device-livesync-service-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export abstract class DeviceLiveSyncServiceBase {
return transferredFiles;
}

public async finalizeSync(liveSyncInfo: ILiveSyncResultInfo): Promise<void> {
public async finalizeSync(liveSyncInfo: ILiveSyncResultInfo, projectData: IProjectData): Promise<IAndroidLivesyncSyncOperationResult> {
//implement in case a sync point for all remove/create operation is needed
return {
didRefresh:true,
operationId: ""
};
}
}
3 changes: 0 additions & 3 deletions lib/services/livesync/livesync-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,6 @@ 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
Expand Down