-
-
Notifications
You must be signed in to change notification settings - Fork 410
/
device-connections-factory.js
208 lines (186 loc) · 6.71 KB
/
device-connections-factory.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import _ from 'lodash';
import net from 'net';
import B from 'bluebird';
import { logger, util } from 'appium-support';
import { utilities } from 'appium-ios-device';
import { checkPortStatus } from 'portscanner';
class iProxy {
constructor (udid, localport, deviceport) {
this.localport = parseInt(localport, 10);
this.deviceport = parseInt(deviceport, 10);
this.udid = udid;
this.serverSocket = null;
this.log = logger.getLogger(`iProxy@${udid.substring(0, 8)}`);
}
async start () {
if (this.serverSocket) {
return;
}
this.serverSocket = net.createServer(async (localSocket) => {
try {
const remoteSocket = await utilities.connectPort(this.udid, this.deviceport);
remoteSocket.on('close', () => localSocket.end());
remoteSocket.on('error', (e) => {
// not all remote socket errors are critical for the user
this.log.info(e.message);
this.log.debug(e);
});
localSocket.on('close', () => remoteSocket.end());
localSocket.on('error', (e) => this.log.warn(e.message));
localSocket.pipe(remoteSocket);
remoteSocket.pipe(localSocket);
} catch (e) {
this.log.error(e);
localSocket.end();
}
});
const status = new B((resolve, reject) => {
this.serverSocket.once('listening', resolve);
this.serverSocket.once('error', reject);
});
this.serverSocket.listen(this.localport);
await status;
this.serverSocket.on('error', (e) => this.log.warn(e.message));
}
// eslint-disable-next-line require-await
async quit () {
if (!this.serverSocket) {
return;
}
// TODO: Wait until the connection is actually closed
// after the corresponding fixes are done in appium-ios-device
// module. Without the fixes the close event might be delayed for up to
// 30 seconds (see https://github.com/appium/appium-xcuitest-driver/pull/1094#issuecomment-546578765)
this.serverSocket.once('close', () => {
this.log.info('The connection has been closed');
this.serverSocket = null;
});
this.serverSocket.close();
}
}
const log = logger.getLogger('DevCon Factory');
const SPLITTER = ':';
class DeviceConnectionsFactory {
constructor () {
this._connectionsMapping = {};
}
_udidAsToken (udid) {
return `${util.hasValue(udid) ? udid : ''}${SPLITTER}`;
}
_portAsToken (port) {
return `${SPLITTER}${util.hasValue(port) ? port : ''}`;
}
_toKey (udid = null, port = null) {
return `${util.hasValue(udid) ? udid : ''}${SPLITTER}${util.hasValue(port) ? port : ''}`;
}
async _releaseProxiedConnections (connectionKeys) {
const promises = [];
for (const key of connectionKeys.filter((k) => _.has(this._connectionsMapping[k], 'iproxy'))) {
promises.push((async () => {
log.info(`Releasing the listener for '${key}'`);
try {
await this._connectionsMapping[key].iproxy.quit();
} catch (ign) {}
return key;
})());
}
return await B.all(promises);
}
listConnections (udid = null, port = null, strict = false) {
if (!udid && !port) {
return [];
}
// `this._connectionMapping` keys have format `udid:port`
// the `strict` argument enforces to match keys having both `udid` and `port`
// if they are defined
// while in non-strict mode keys having any of these are going to be matched
return _.keys(this._connectionsMapping)
.filter((key) => (strict && udid && port)
? (key === this._toKey(udid, port))
: (udid && key.startsWith(this._udidAsToken(udid)) || port && key.endsWith(this._portAsToken(port)))
);
}
async requestConnection (udid, port, options = {}) {
if (!udid || !port) {
log.warn('Did not know how to request the connection:');
if (!udid) {
log.warn('- Device UDID is unset');
}
if (!port) {
log.warn('- The local port number is unset');
}
return;
}
const {
usePortForwarding,
devicePort,
} = options;
log.info(`Requesting connection for device ${udid} on local port ${port}` +
(devicePort ? `, device port ${devicePort}` : ''));
log.debug(`Cached connections count: ${_.size(this._connectionsMapping)}`);
const connectionsOnPort = this.listConnections(null, port);
if (!_.isEmpty(connectionsOnPort)) {
log.info(`Found cached connections on port #${port}: ${JSON.stringify(connectionsOnPort)}`);
}
if (usePortForwarding) {
let isPortBusy = (await checkPortStatus(port, '127.0.0.1')) === 'open';
if (isPortBusy) {
log.warn(`Port #${port} is busy`);
if (!_.isEmpty(connectionsOnPort)) {
log.info('Trying to release the port');
for (const key of await this._releaseProxiedConnections(connectionsOnPort)) {
delete this._connectionsMapping[key];
}
if ((await checkPortStatus(port, '127.0.0.1')) !== 'open') {
log.info(`Port #${port} has been successfully released`);
isPortBusy = false;
} else {
log.warn(`Did not know how to release port #${port}`);
}
}
}
if (isPortBusy) {
throw new Error(`The port #${port} is occupied by an other process. ` +
`You can either quit that process or select another free port.`);
}
}
const currentKey = this._toKey(udid, port);
if (usePortForwarding) {
const iproxy = new iProxy(udid, port, devicePort);
try {
await iproxy.start();
this._connectionsMapping[currentKey] = {iproxy};
} catch (e) {
try {
await iproxy.quit();
} catch (ign) {}
throw e;
}
} else {
this._connectionsMapping[currentKey] = {};
}
log.info(`Successfully requested the connection for ${currentKey}`);
}
async releaseConnection (udid = null, port = null) {
if (!udid && !port) {
log.warn('Neither device UDID nor local port is set. ' +
'Did not know how to release the connection');
return;
}
log.info(`Releasing connections for ${udid || 'any'} device on ${port || 'any'} port number`);
const keys = this.listConnections(udid, port, true);
if (_.isEmpty(keys)) {
log.info('No cached connections have been found');
return;
}
log.info(`Found cached connections to release: ${JSON.stringify(keys)}`);
await this._releaseProxiedConnections(keys);
for (const key of keys) {
delete this._connectionsMapping[key];
}
log.debug(`Cached connections count: ${_.size(this._connectionsMapping)}`);
}
}
const DEVICE_CONNECTIONS_FACTORY = new DeviceConnectionsFactory();
export { DEVICE_CONNECTIONS_FACTORY, DeviceConnectionsFactory };
export default DEVICE_CONNECTIONS_FACTORY;