-
-
Notifications
You must be signed in to change notification settings - Fork 197
/
Copy pathandroid-process-service.ts
281 lines (226 loc) · 12.1 KB
/
android-process-service.ts
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
import { EOL } from "os";
import { DeviceAndroidDebugBridge } from "../android/device-android-debug-bridge";
import { TARGET_FRAMEWORK_IDENTIFIERS } from "../../constants";
import { exported } from "../../decorators";
export class AndroidProcessService implements Mobile.IAndroidProcessService {
private _devicesAdbs: IDictionary<Mobile.IDeviceAndroidDebugBridge>;
private _forwardedLocalPorts: IDictionary<number>;
constructor(private $errors: IErrors,
private $injector: IInjector,
private $net: INet,
private $cleanupService: ICleanupService,
private $staticConfig: IStaticConfig) {
this._devicesAdbs = {};
this._forwardedLocalPorts = {};
}
public async forwardFreeTcpToAbstractPort(portForwardInputData: Mobile.IPortForwardData): Promise<number> {
const adb = await this.setupForPortForwarding(portForwardInputData);
return this.forwardPort(portForwardInputData, adb);
}
public async mapAbstractToTcpPort(deviceIdentifier: string, appIdentifier: string, framework: string): Promise<string> {
const adb = await this.setupForPortForwarding({ deviceIdentifier, appIdentifier });
const processId = (await this.getProcessIds(adb, [appIdentifier]))[appIdentifier];
const applicationNotStartedErrorMessage = `The application is not started on the device with identifier ${deviceIdentifier}.`;
if (!processId) {
this.$errors.failWithoutHelp(applicationNotStartedErrorMessage);
}
const abstractPortsInformation = await this.getAbstractPortsInformation(adb);
const abstractPort = await this.getAbstractPortForApplication(adb, processId, appIdentifier, abstractPortsInformation, framework);
if (!abstractPort) {
this.$errors.failWithoutHelp(applicationNotStartedErrorMessage);
}
const forwardedTcpPort = await this.forwardPort({ deviceIdentifier, appIdentifier, abstractPort: `localabstract:${abstractPort}` }, adb);
return forwardedTcpPort && forwardedTcpPort.toString();
}
public async getMappedAbstractToTcpPorts(deviceIdentifier: string, appIdentifiers: string[], framework: string): Promise<IDictionary<number>> {
const adb = this.getAdb(deviceIdentifier),
abstractPortsInformation = await this.getAbstractPortsInformation(adb),
processIds = await this.getProcessIds(adb, appIdentifiers),
adbForwardList = await adb.executeCommand(["forward", "--list"]),
localPorts: IDictionary<number> = {};
await Promise.all(
_.map(appIdentifiers, async appIdentifier => {
localPorts[appIdentifier] = null;
const processId = processIds[appIdentifier];
if (!processId) {
return;
}
const abstractPort = await this.getAbstractPortForApplication(adb, processId, appIdentifier, abstractPortsInformation, framework);
if (!abstractPort) {
return;
}
const localPort = await this.getAlreadyMappedPort(adb, deviceIdentifier, abstractPort, adbForwardList);
if (localPort) {
localPorts[appIdentifier] = localPort;
}
}));
return localPorts;
}
public async getDebuggableApps(deviceIdentifier: string): Promise<Mobile.IDeviceApplicationInformation[]> {
const adb = this.getAdb(deviceIdentifier);
const androidWebViewPortInformation = (await this.getAbstractPortsInformation(adb)).split(EOL);
// TODO: Add tests and make sure only unique names are returned. Input before groupBy is:
// [ { deviceIdentifier: 'SH26BW100473',
// appIdentifier: 'com.telerik.EmptyNS',
// framework: 'NativeScript' },
// { deviceIdentifier: 'SH26BW100473',
// appIdentifier: 'com.telerik.EmptyNS',
// framework: 'Cordova' },
// { deviceIdentifier: 'SH26BW100473',
// appIdentifier: 'chrome',
// framework: 'Cordova' },
// { deviceIdentifier: 'SH26BW100473',
// appIdentifier: 'chrome',
// framework: 'Cordova' } ]
const portInformation = await Promise.all(
_.map(androidWebViewPortInformation, async line => await this.getApplicationInfoFromWebViewPortInformation(adb, deviceIdentifier, line)
|| await this.getNativeScriptApplicationInformation(adb, deviceIdentifier, line)
)
);
return _(portInformation)
.filter(deviceAppInfo => !!deviceAppInfo)
.groupBy(element => element.framework)
.map((group: Mobile.IDeviceApplicationInformation[]) => _.uniqBy(group, g => g.appIdentifier))
.flatten<Mobile.IDeviceApplicationInformation>()
.value();
}
@exported("androidProcessService")
public async getAppProcessId(deviceIdentifier: string, appIdentifier: string): Promise<string> {
const adb = this.getAdb(deviceIdentifier);
const processId = (await this.getProcessIds(adb, [appIdentifier]))[appIdentifier];
return processId ? processId.toString() : null;
}
private async forwardPort(portForwardInputData: Mobile.IPortForwardData, adb: Mobile.IDeviceAndroidDebugBridge): Promise<number> {
let localPort = await this.getAlreadyMappedPort(adb, portForwardInputData.deviceIdentifier, portForwardInputData.abstractPort);
if (!localPort) {
localPort = await this.$net.getFreePort();
await adb.executeCommand(["forward", `tcp:${localPort}`, portForwardInputData.abstractPort], { deviceIdentifier: portForwardInputData.deviceIdentifier });
}
this._forwardedLocalPorts[portForwardInputData.deviceIdentifier] = localPort;
await this.$cleanupService.addCleanupAction({ command: await this.$staticConfig.getAdbFilePath(), args: ["-s", portForwardInputData.deviceIdentifier, "forward", "--remove", `tcp:${localPort}`] });
return localPort && +localPort;
}
private async setupForPortForwarding(portForwardInputData?: Mobile.IPortForwardDataBase): Promise<Mobile.IDeviceAndroidDebugBridge> {
const adb = this.getAdb(portForwardInputData.deviceIdentifier);
return adb;
}
private async getApplicationInfoFromWebViewPortInformation(adb: Mobile.IDeviceAndroidDebugBridge, deviceIdentifier: string, information: string): Promise<Mobile.IDeviceApplicationInformation> {
// Need to search by processId to check for old Android webviews (@webview_devtools_remote_<processId>).
const processIdRegExp = /@webview_devtools_remote_(.+)/g;
const processIdMatches = processIdRegExp.exec(information);
let cordovaAppIdentifier: string;
if (processIdMatches) {
const processId = processIdMatches[1];
cordovaAppIdentifier = await this.getApplicationIdentifierFromPid(adb, processId);
} else {
// Search for appIdentifier (@<appIdentifier>_devtools_remote).
const chromeAppIdentifierRegExp = /@(.+)_devtools_remote\s?/g;
const chromeAppIdentifierMatches = chromeAppIdentifierRegExp.exec(information);
if (chromeAppIdentifierMatches && chromeAppIdentifierMatches.length > 0) {
cordovaAppIdentifier = chromeAppIdentifierMatches[1];
}
}
if (cordovaAppIdentifier) {
return {
deviceIdentifier: deviceIdentifier,
appIdentifier: cordovaAppIdentifier,
framework: TARGET_FRAMEWORK_IDENTIFIERS.Cordova
};
}
return null;
}
private async getNativeScriptApplicationInformation(adb: Mobile.IDeviceAndroidDebugBridge, deviceIdentifier: string, information: string): Promise<Mobile.IDeviceApplicationInformation> {
// Search for appIdentifier (@<appIdentifier-debug>).
const nativeScriptAppIdentifierRegExp = /@(.+)-(debug|inspectorServer)/g;
const nativeScriptAppIdentifierMatches = nativeScriptAppIdentifierRegExp.exec(information);
if (nativeScriptAppIdentifierMatches && nativeScriptAppIdentifierMatches.length > 0) {
const appIdentifier = nativeScriptAppIdentifierMatches[1];
return {
deviceIdentifier: deviceIdentifier,
appIdentifier: appIdentifier,
framework: TARGET_FRAMEWORK_IDENTIFIERS.NativeScript
};
}
return null;
}
private async getAbstractPortForApplication(adb: Mobile.IDeviceAndroidDebugBridge, processId: string | number, appIdentifier: string, abstractPortsInformation: string, framework: string): Promise<string> {
// The result will look like this (without the columns names):
// Num RefCount Protocol Flags Type St Inode Path
// 0000000000000000: 00000002 00000000 00010000 0001 01 189004 @webview_devtools_remote_25512
// The Path column is the abstract port.
framework = framework || "";
switch (framework.toLowerCase()) {
case TARGET_FRAMEWORK_IDENTIFIERS.Cordova.toLowerCase():
return this.getCordovaPortInformation(abstractPortsInformation, appIdentifier, processId);
case TARGET_FRAMEWORK_IDENTIFIERS.NativeScript.toLowerCase():
return this.getNativeScriptPortInformation(abstractPortsInformation, appIdentifier);
default:
return this.getCordovaPortInformation(abstractPortsInformation, appIdentifier, processId) ||
this.getNativeScriptPortInformation(abstractPortsInformation, appIdentifier);
}
}
private getCordovaPortInformation(abstractPortsInformation: string, appIdentifier: string, processId: string | number): string {
return this.getPortInformation(abstractPortsInformation, `${appIdentifier}_devtools_remote`) || this.getPortInformation(abstractPortsInformation, processId);
}
private getNativeScriptPortInformation(abstractPortsInformation: string, appIdentifier: string): string {
return this.getPortInformation(abstractPortsInformation, `${appIdentifier}-debug`);
}
private async getAbstractPortsInformation(adb: Mobile.IDeviceAndroidDebugBridge): Promise<string> {
return adb.executeShellCommand(["cat", "/proc/net/unix"]);
}
private getPortInformation(abstractPortsInformation: string, searchedInfo: string | number): string {
const processRegExp = new RegExp(`\\w+:\\s+(?:\\w+\\s+){1,6}@(.*?${searchedInfo})$`, "gm");
const match = processRegExp.exec(abstractPortsInformation);
return match && match[1];
}
private async getProcessIds(adb: Mobile.IDeviceAndroidDebugBridge, appIdentifiers: string[]): Promise<IDictionary<number>> {
// Process information will look like this (without the columns names):
// USER PID PPID VSIZE RSS WCHAN PC NAME
// u0_a63 25512 1334 1519560 96040 ffffffff f76a8f75 S com.telerik.appbuildertabstest
const result: IDictionary<number> = {};
const processIdInformation: string = await adb.executeShellCommand(["ps"]);
_.each(appIdentifiers, appIdentifier => {
const processIdRegExp = new RegExp(`^\\w*\\s*(\\d+).*?${appIdentifier}$`);
result[appIdentifier] = this.getFirstMatchingGroupFromMultilineResult<number>(processIdInformation, processIdRegExp);
});
return result;
}
private async getAlreadyMappedPort(adb: Mobile.IDeviceAndroidDebugBridge, deviceIdentifier: string, abstractPort: string, adbForwardList?: any): Promise<number> {
const allForwardedPorts: string = adbForwardList || await adb.executeCommand(["forward", "--list"]) || "";
// Sample output:
// 5e2e580b tcp:62503 localabstract:webview_devtools_remote_7985
// 5e2e580b tcp:62524 localabstract:webview_devtools_remote_7986
// 5e2e580b tcp:63160 localabstract:webview_devtools_remote_7987
// 5e2e580b tcp:57577 localabstract:com.telerik.nrel-debug
const regex = new RegExp(`${deviceIdentifier}\\s+?tcp:(\\d+?)\\s+?.*?${abstractPort}$`);
return this.getFirstMatchingGroupFromMultilineResult<number>(allForwardedPorts, regex);
}
private getAdb(deviceIdentifier: string): Mobile.IDeviceAndroidDebugBridge {
if (!this._devicesAdbs[deviceIdentifier]) {
this._devicesAdbs[deviceIdentifier] = this.$injector.resolve(DeviceAndroidDebugBridge, { identifier: deviceIdentifier });
}
return this._devicesAdbs[deviceIdentifier];
}
private async getApplicationIdentifierFromPid(adb: Mobile.IDeviceAndroidDebugBridge, pid: string, psData?: string): Promise<string> {
psData = psData || await adb.executeShellCommand(["ps"]);
// Process information will look like this (without the columns names):
// USER PID PPID VSIZE RSS WCHAN PC NAME
// u0_a63 25512 1334 1519560 96040 ffffffff f76a8f75 S com.telerik.appbuildertabstest
return this.getFirstMatchingGroupFromMultilineResult<string>(psData, new RegExp(`\\s+${pid}(?:\\s+\\d+){3}\\s+.*\\s+(.*?)$`));
}
private getFirstMatchingGroupFromMultilineResult<T>(input: string, regex: RegExp): T {
let result: T;
_((input || "").split('\n'))
.map(line => line.trim())
.filter(line => !!line)
.each(line => {
const matches = line.match(regex);
if (matches && matches[1]) {
result = <any>matches[1];
return false;
}
});
return result;
}
}
$injector.register("androidProcessService", AndroidProcessService);