diff --git a/lib/common/definitions/mobile.d.ts b/lib/common/definitions/mobile.d.ts index 9a76d4eae7..e66e93aed9 100644 --- a/lib/common/definitions/mobile.d.ts +++ b/lib/common/definitions/mobile.d.ts @@ -107,9 +107,9 @@ declare module Mobile { interface IiOSDevice extends IDevice { getDebugSocket(appId: string, projectName: string): Promise; - destroyDebugSocket(appId: string): void; + destroyDebugSocket(appId: string): Promise; openDeviceLogStream(options?: IiOSLogStreamOptions): Promise; - destroyAllSockets(): void; + destroyAllSockets(): Promise; } interface IAndroidDevice extends IDevice { diff --git a/lib/common/mobile/ios/device/ios-application-manager.ts b/lib/common/mobile/ios/device/ios-application-manager.ts index 022a7d8ac1..cb4b193e73 100644 --- a/lib/common/mobile/ios/device/ios-application-manager.ts +++ b/lib/common/mobile/ios/device/ios-application-manager.ts @@ -70,7 +70,7 @@ export class IOSApplicationManager extends ApplicationManagerBase { public async stopApplication(appData: Mobile.IApplicationData): Promise { const { appId } = appData; - this.device.destroyDebugSocket(appId); + await this.device.destroyDebugSocket(appId); const action = () => this.$iosDeviceOperations.stop([{ deviceId: this.device.deviceInfo.identifier, ddi: this.$options.ddi, appId }]); diff --git a/lib/common/mobile/ios/ios-device-base.ts b/lib/common/mobile/ios/ios-device-base.ts index f0e980a186..eedfa06e4f 100644 --- a/lib/common/mobile/ios/ios-device-base.ts +++ b/lib/common/mobile/ios/ios-device-base.ts @@ -24,8 +24,8 @@ export abstract class IOSDeviceBase implements Mobile.IiOSDevice { this.cachedSockets[appId] = await this.getDebugSocketCore(appId, projectName); if (this.cachedSockets[appId]) { - this.cachedSockets[appId].on("close", () => { - this.destroyDebugSocket(appId); + this.cachedSockets[appId].on("close", async () => { + await this.destroyDebugSocket(appId); }); this.$processService.attachToProcessExitSignals(this, () => this.destroyDebugSocket(appId)); @@ -51,22 +51,25 @@ export abstract class IOSDeviceBase implements Mobile.IiOSDevice { return port; } - public destroyAllSockets() { + public async destroyAllSockets(): Promise { for (const appId in this.cachedSockets) { - this.destroySocketSafe(this.cachedSockets[appId]); + await this.destroySocketSafe(this.cachedSockets[appId]); } this.cachedSockets = {}; } - public destroyDebugSocket(appId: string) { - this.destroySocketSafe(this.cachedSockets[appId]); + public async destroyDebugSocket(appId: string): Promise { + await this.destroySocketSafe(this.cachedSockets[appId]); this.cachedSockets[appId] = null; } - private destroySocketSafe(socket: net.Socket) { + private async destroySocketSafe(socket: net.Socket): Promise { if (socket && !socket.destroyed) { - socket.destroy(); + return new Promise((resolve, reject) => { + socket.on("close", resolve); + socket.destroy(); + }); } } diff --git a/lib/common/mobile/ios/simulator/ios-simulator-application-manager.ts b/lib/common/mobile/ios/simulator/ios-simulator-application-manager.ts index 6d456f743d..7e6f8ec6a2 100644 --- a/lib/common/mobile/ios/simulator/ios-simulator-application-manager.ts +++ b/lib/common/mobile/ios/simulator/ios-simulator-application-manager.ts @@ -69,7 +69,7 @@ export class IOSSimulatorApplicationManager extends ApplicationManagerBase { public async stopApplication(appData: Mobile.IApplicationData): Promise { const { appId } = appData; - this.device.destroyDebugSocket(appId); + await this.device.destroyDebugSocket(appId); await this.detachNativeDebugger(appId); await this.iosSim.stopApplication(this.device.deviceInfo.identifier, appData.appId, appData.projectName); diff --git a/lib/common/services/lock-service.ts b/lib/common/services/lock-service.ts index 807a421006..64e0db0176 100644 --- a/lib/common/services/lock-service.ts +++ b/lib/common/services/lock-service.ts @@ -48,7 +48,7 @@ export class LockService implements ILockService { } } - private lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise { + public lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise { const { filePath, fileOpts } = this.getLockFileSettings(lockFilePath, lockOpts); this.currentlyLockedFiles.push(filePath); @@ -62,7 +62,7 @@ export class LockService implements ILockService { }); } - private unlock(lockFilePath?: string): void { + public unlock(lockFilePath?: string): void { const { filePath } = this.getLockFileSettings(lockFilePath); _.remove(this.currentlyLockedFiles, e => e === lockFilePath); lockfile.unlockSync(filePath); diff --git a/lib/common/test/unit-tests/stubs.ts b/lib/common/test/unit-tests/stubs.ts index 75d10257a1..64b6c71780 100644 --- a/lib/common/test/unit-tests/stubs.ts +++ b/lib/common/test/unit-tests/stubs.ts @@ -4,6 +4,13 @@ import * as util from "util"; import { EventEmitter } from "events"; export class LockServiceStub implements ILockService { + public async lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise { + return lockFilePath; + } + + public unlock(lockFilePath?: string): void { + } + public async executeActionWithLock(action: () => Promise, lockFilePath?: string, lockOpts?: ILockOptions): Promise { const result = await action(); return result; diff --git a/lib/definitions/lock-service.d.ts b/lib/definitions/lock-service.d.ts index 66c21d1a6c..6517d6a04e 100644 --- a/lib/definitions/lock-service.d.ts +++ b/lib/definitions/lock-service.d.ts @@ -15,5 +15,19 @@ declare global { */ executeActionWithLock(action: () => Promise, lockFilePath?: string, lockOpts?: ILockOptions): Promise // TODO: expose as decorator + + /** + * Wait until the `unlock` method is called for the specified file + * @param {string} lockFilePath Path to lock file that has to be created. Defaults to `/lockfile.lock` + * @param {ILockOptions} lockOpts Options used for creating the lock file. + * @returns {Promise} + */ + lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise + + /** + * Resolve the lock methods for the specified file + * @param {string} lockFilePath Path to lock file that has to be removed. Defaults to `/lockfile.lock` + */ + unlock(lockFilePath?: string): void } } \ No newline at end of file diff --git a/lib/device-sockets/ios/app-debug-socket-proxy-factory.ts b/lib/device-sockets/ios/app-debug-socket-proxy-factory.ts index 8967bd2ee5..f68f00f6a0 100644 --- a/lib/device-sockets/ios/app-debug-socket-proxy-factory.ts +++ b/lib/device-sockets/ios/app-debug-socket-proxy-factory.ts @@ -11,6 +11,7 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu constructor(private $logger: ILogger, private $errors: IErrors, + private $lockService: ILockService, private $options: IOptions, private $net: INet) { super(); @@ -54,9 +55,9 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu } }); - frontendSocket.on("close", () => { + frontendSocket.on("close", async () => { this.$logger.info("Frontend socket closed"); - device.destroyDebugSocket(appId); + await device.destroyDebugSocket(appId); }); appDebugSocket.on("close", () => { @@ -91,6 +92,7 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu } private async addWebSocketProxy(device: Mobile.IiOSDevice, appId: string, projectName: string): Promise { + const clientConnectionLockFile = `debug-connection-${device.deviceInfo.identifier}-${appId}.lock`; const cacheKey = `${device.deviceInfo.identifier}-${appId}`; const existingServer = this.deviceWebServers[cacheKey]; if (existingServer) { @@ -107,15 +109,29 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu // We store the socket that connects us to the device in the upgrade request object itself and later on retrieve it // in the connection callback. + let currentAppSocket: net.Socket = null; + let currentWebSocket: ws = null; const server = new ws.Server({ port: localPort, host: "localhost", verifyClient: async (info: any, callback: (res: boolean, code?: number, message?: string) => void) => { + await this.$lockService.lock(clientConnectionLockFile); let acceptHandshake = true; this.$logger.info("Frontend client connected."); let appDebugSocket; try { + if (currentAppSocket) { + currentAppSocket.removeAllListeners(); + currentAppSocket = null; + if (currentWebSocket) { + currentWebSocket.removeAllListeners(); + currentWebSocket.close(); + currentWebSocket = null; + } + await device.destroyDebugSocket(appId); + } appDebugSocket = await device.getDebugSocket(appId, projectName); + currentAppSocket = appDebugSocket; this.$logger.info("Backend socket created."); info.req["__deviceSocket"] = appDebugSocket; } catch (err) { @@ -123,6 +139,7 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu this.$logger.trace(err); this.emit(CONNECTION_ERROR_EVENT_NAME, err); acceptHandshake = false; + this.$lockService.unlock(clientConnectionLockFile); this.$logger.warn(`Cannot connect to device socket. The error message is '${err.message}'.`); } @@ -131,6 +148,7 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu }); this.deviceWebServers[cacheKey] = server; server.on("connection", (webSocket, req) => { + currentWebSocket = webSocket; const encoding = "utf16le"; const appDebugSocket: net.Socket = (req)["__deviceSocket"]; @@ -163,20 +181,23 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu }); appDebugSocket.on("close", () => { + currentAppSocket = null; this.$logger.info("Backend socket closed!"); webSocket.close(); }); - webSocket.on("close", () => { + webSocket.on("close", async () => { + currentWebSocket = null; this.$logger.info('Frontend socket closed!'); appDebugSocket.unpipe(packets); packets.destroy(); - device.destroyDebugSocket(appId); + await device.destroyDebugSocket(appId); if (!this.$options.watch) { process.exit(0); } }); + this.$lockService.unlock(clientConnectionLockFile); }); return server; diff --git a/lib/services/livesync/ios-device-livesync-service.ts b/lib/services/livesync/ios-device-livesync-service.ts index 37494537df..c7595e4874 100644 --- a/lib/services/livesync/ios-device-livesync-service.ts +++ b/lib/services/livesync/ios-device-livesync-service.ts @@ -150,16 +150,16 @@ export class IOSDeviceLiveSyncService extends DeviceLiveSyncServiceBase implemen }); } catch (error) { this.$logger.trace("Error while sending message:", error); - this.destroySocket(); + await this.destroySocket(); } } - private destroySocket(): void { + private async destroySocket(): Promise { if (this.socket) { // we do not support LiveSync on multiple apps on the same device // in order to do that, we should cache the socket per app // and destroy just the current app socket when possible - this.device.destroyAllSockets(); + await this.device.destroyAllSockets(); this.socket = null; } }