Skip to content

Commit a48af84

Browse files
committed
fix: ensure a single web client connected for debugging
1 parent 76b1580 commit a48af84

File tree

9 files changed

+66
-21
lines changed

9 files changed

+66
-21
lines changed

lib/common/definitions/mobile.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ declare module Mobile {
107107

108108
interface IiOSDevice extends IDevice {
109109
getDebugSocket(appId: string, projectName: string): Promise<any>;
110-
destroyDebugSocket(appId: string): void;
110+
destroyDebugSocket(appId: string): Promise<void>;
111111
openDeviceLogStream(options?: IiOSLogStreamOptions): Promise<void>;
112-
destroyAllSockets(): void;
112+
destroyAllSockets(): Promise<void>;
113113
}
114114

115115
interface IAndroidDevice extends IDevice {

lib/common/mobile/ios/device/ios-application-manager.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export class IOSApplicationManager extends ApplicationManagerBase {
7070
public async stopApplication(appData: Mobile.IApplicationData): Promise<void> {
7171
const { appId } = appData;
7272

73-
this.device.destroyDebugSocket(appId);
73+
await this.device.destroyDebugSocket(appId);
7474

7575
const action = () => this.$iosDeviceOperations.stop([{ deviceId: this.device.deviceInfo.identifier, ddi: this.$options.ddi, appId }]);
7676

lib/common/mobile/ios/ios-device-base.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export abstract class IOSDeviceBase implements Mobile.IiOSDevice {
2424
this.cachedSockets[appId] = await this.getDebugSocketCore(appId, projectName);
2525

2626
if (this.cachedSockets[appId]) {
27-
this.cachedSockets[appId].on("close", () => {
28-
this.destroyDebugSocket(appId);
27+
this.cachedSockets[appId].on("close", async () => {
28+
await this.destroyDebugSocket(appId);
2929
});
3030

3131
this.$processService.attachToProcessExitSignals(this, () => this.destroyDebugSocket(appId));
@@ -51,22 +51,25 @@ export abstract class IOSDeviceBase implements Mobile.IiOSDevice {
5151
return port;
5252
}
5353

54-
public destroyAllSockets() {
54+
public async destroyAllSockets(): Promise<void> {
5555
for (const appId in this.cachedSockets) {
56-
this.destroySocketSafe(this.cachedSockets[appId]);
56+
await this.destroySocketSafe(this.cachedSockets[appId]);
5757
}
5858

5959
this.cachedSockets = {};
6060
}
6161

62-
public destroyDebugSocket(appId: string) {
63-
this.destroySocketSafe(this.cachedSockets[appId]);
62+
public async destroyDebugSocket(appId: string): Promise<void> {
63+
await this.destroySocketSafe(this.cachedSockets[appId]);
6464
this.cachedSockets[appId] = null;
6565
}
6666

67-
private destroySocketSafe(socket: net.Socket) {
67+
private async destroySocketSafe(socket: net.Socket): Promise<void> {
6868
if (socket && !socket.destroyed) {
69-
socket.destroy();
69+
return new Promise<void>((resolve, reject) => {
70+
socket.on("close", resolve);
71+
socket.destroy();
72+
});
7073
}
7174
}
7275

lib/common/mobile/ios/simulator/ios-simulator-application-manager.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export class IOSSimulatorApplicationManager extends ApplicationManagerBase {
6969
public async stopApplication(appData: Mobile.IApplicationData): Promise<void> {
7070
const { appId } = appData;
7171

72-
this.device.destroyDebugSocket(appId);
72+
await this.device.destroyDebugSocket(appId);
7373
await this.detachNativeDebugger(appId);
7474

7575
await this.iosSim.stopApplication(this.device.deviceInfo.identifier, appData.appId, appData.projectName);

lib/common/services/lock-service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class LockService implements ILockService {
4848
}
4949
}
5050

51-
private lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise<string> {
51+
public lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise<string> {
5252
const { filePath, fileOpts } = this.getLockFileSettings(lockFilePath, lockOpts);
5353
this.currentlyLockedFiles.push(filePath);
5454

@@ -62,7 +62,7 @@ export class LockService implements ILockService {
6262
});
6363
}
6464

65-
private unlock(lockFilePath?: string): void {
65+
public unlock(lockFilePath?: string): void {
6666
const { filePath } = this.getLockFileSettings(lockFilePath);
6767
_.remove(this.currentlyLockedFiles, e => e === lockFilePath);
6868
lockfile.unlockSync(filePath);

lib/common/test/unit-tests/stubs.ts

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import * as util from "util";
44
import { EventEmitter } from "events";
55

66
export class LockServiceStub implements ILockService {
7+
public async lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise<string> {
8+
return lockFilePath;
9+
}
10+
11+
public unlock(lockFilePath?: string): void {
12+
}
13+
714
public async executeActionWithLock<T>(action: () => Promise<T>, lockFilePath?: string, lockOpts?: ILockOptions): Promise<T> {
815
const result = await action();
916
return result;

lib/definitions/lock-service.d.ts

+14
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,19 @@ declare global {
1515
*/
1616
executeActionWithLock<T>(action: () => Promise<T>, lockFilePath?: string, lockOpts?: ILockOptions): Promise<T>
1717
// TODO: expose as decorator
18+
19+
/**
20+
* Wait until the `unlock` method is called for the specified file
21+
* @param {string} lockFilePath Path to lock file that has to be created. Defaults to `<profile dir>/lockfile.lock`
22+
* @param {ILockOptions} lockOpts Options used for creating the lock file.
23+
* @returns {Promise<T>}
24+
*/
25+
lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise<string>
26+
27+
/**
28+
* Resolve the lock methods for the specified file
29+
* @param {string} lockFilePath Path to lock file that has to be removed. Defaults to `<profile dir>/lockfile.lock`
30+
*/
31+
unlock(lockFilePath?: string): void
1832
}
1933
}

lib/device-sockets/ios/app-debug-socket-proxy-factory.ts

+25-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu
1111

1212
constructor(private $logger: ILogger,
1313
private $errors: IErrors,
14+
private $lockService: ILockService,
1415
private $options: IOptions,
1516
private $net: INet) {
1617
super();
@@ -54,9 +55,9 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu
5455
}
5556
});
5657

57-
frontendSocket.on("close", () => {
58+
frontendSocket.on("close", async () => {
5859
this.$logger.info("Frontend socket closed");
59-
device.destroyDebugSocket(appId);
60+
await device.destroyDebugSocket(appId);
6061
});
6162

6263
appDebugSocket.on("close", () => {
@@ -91,6 +92,7 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu
9192
}
9293

9394
private async addWebSocketProxy(device: Mobile.IiOSDevice, appId: string, projectName: string): Promise<ws.Server> {
95+
const clientConnectionLockFile = `debug-connection-${device.deviceInfo.identifier}-${appId}.lock`;
9496
const cacheKey = `${device.deviceInfo.identifier}-${appId}`;
9597
const existingServer = this.deviceWebServers[cacheKey];
9698
if (existingServer) {
@@ -107,22 +109,37 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu
107109
// We store the socket that connects us to the device in the upgrade request object itself and later on retrieve it
108110
// in the connection callback.
109111

112+
let currentAppSocket: net.Socket = null;
113+
let currentWebSocket: ws = null;
110114
const server = new ws.Server(<any>{
111115
port: localPort,
112116
host: "localhost",
113117
verifyClient: async (info: any, callback: (res: boolean, code?: number, message?: string) => void) => {
118+
await this.$lockService.lock(clientConnectionLockFile);
114119
let acceptHandshake = true;
115120
this.$logger.info("Frontend client connected.");
116121
let appDebugSocket;
117122
try {
123+
if (currentAppSocket) {
124+
currentAppSocket.removeAllListeners();
125+
currentAppSocket = null;
126+
if (currentWebSocket) {
127+
currentWebSocket.removeAllListeners();
128+
currentWebSocket.close();
129+
currentWebSocket = null;
130+
}
131+
await device.destroyDebugSocket(appId);
132+
}
118133
appDebugSocket = await device.getDebugSocket(appId, projectName);
134+
currentAppSocket = appDebugSocket;
119135
this.$logger.info("Backend socket created.");
120136
info.req["__deviceSocket"] = appDebugSocket;
121137
} catch (err) {
122138
err.deviceIdentifier = device.deviceInfo.identifier;
123139
this.$logger.trace(err);
124140
this.emit(CONNECTION_ERROR_EVENT_NAME, err);
125141
acceptHandshake = false;
142+
this.$lockService.unlock(clientConnectionLockFile);
126143
this.$logger.warn(`Cannot connect to device socket. The error message is '${err.message}'.`);
127144
}
128145

@@ -131,6 +148,7 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu
131148
});
132149
this.deviceWebServers[cacheKey] = server;
133150
server.on("connection", (webSocket, req) => {
151+
currentWebSocket = webSocket;
134152
const encoding = "utf16le";
135153

136154
const appDebugSocket: net.Socket = (<any>req)["__deviceSocket"];
@@ -163,20 +181,23 @@ export class AppDebugSocketProxyFactory extends EventEmitter implements IAppDebu
163181
});
164182

165183
appDebugSocket.on("close", () => {
184+
currentAppSocket = null;
166185
this.$logger.info("Backend socket closed!");
167186
webSocket.close();
168187
});
169188

170-
webSocket.on("close", () => {
189+
webSocket.on("close", async () => {
190+
currentWebSocket = null;
171191
this.$logger.info('Frontend socket closed!');
172192
appDebugSocket.unpipe(packets);
173193
packets.destroy();
174-
device.destroyDebugSocket(appId);
194+
await device.destroyDebugSocket(appId);
175195
if (!this.$options.watch) {
176196
process.exit(0);
177197
}
178198
});
179199

200+
this.$lockService.unlock(clientConnectionLockFile);
180201
});
181202

182203
return server;

lib/services/livesync/ios-device-livesync-service.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,16 @@ export class IOSDeviceLiveSyncService extends DeviceLiveSyncServiceBase implemen
150150
});
151151
} catch (error) {
152152
this.$logger.trace("Error while sending message:", error);
153-
this.destroySocket();
153+
await this.destroySocket();
154154
}
155155
}
156156

157-
private destroySocket(): void {
157+
private async destroySocket(): Promise<void> {
158158
if (this.socket) {
159159
// we do not support LiveSync on multiple apps on the same device
160160
// in order to do that, we should cache the socket per app
161161
// and destroy just the current app socket when possible
162-
this.device.destroyAllSockets();
162+
await this.device.destroyAllSockets();
163163
this.socket = null;
164164
}
165165
}

0 commit comments

Comments
 (0)