Skip to content

Commit e46408f

Browse files
committed
Merge pull request #1373 from NativeScript/totev/android-debugger-unixsockets
Implement Android debugging with unix sockets redirection
2 parents a319067 + 182660a commit e46408f

File tree

1 file changed

+144
-185
lines changed

1 file changed

+144
-185
lines changed

lib/services/android-debug-service.ts

+144-185
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
///<reference path="../.d.ts"/>
22
"use strict";
3-
import * as helpers from "../common/helpers";
43
import * as path from "path";
5-
import * as util from "util";
4+
import * as net from "net";
5+
import Future = require("fibers/future");
66

77
class AndroidDebugService implements IDebugService {
8-
private static ENV_DEBUG_IN_FILENAME = "envDebug.in";
9-
private static ENV_DEBUG_OUT_FILENAME = "envDebug.out";
10-
private static DEFAULT_NODE_INSPECTOR_URL = "http://127.0.0.1:8080/debug";
11-
private static PACKAGE_EXTERNAL_DIR_TEMPLATE = "/sdcard/Android/data/%s/files/";
8+
private static DEFAULT_NODE_INSPECTOR_URL = "http://127.0.0.1:8080/debug";
129

1310
private _device: Mobile.IAndroidDevice = null;
1411

@@ -19,13 +16,10 @@ class AndroidDebugService implements IDebugService {
1916
private $logger: ILogger,
2017
private $options: IOptions,
2118
private $childProcess: IChildProcess,
22-
private $mobileHelper: Mobile.IMobileHelper,
23-
private $hostInfo: IHostInfo,
24-
private $errors: IErrors,
25-
private $opener: IOpener,
26-
private $staticConfig: IStaticConfig,
27-
private $utils: IUtils,
28-
private $config: IConfiguration) { }
19+
private $hostInfo: IHostInfo,
20+
private $errors: IErrors,
21+
private $opener: IOpener,
22+
private $config: IConfiguration) { }
2923

3024
private get platform() { return "android"; }
3125

@@ -43,14 +37,60 @@ class AndroidDebugService implements IDebugService {
4337
: this.debugOnDevice();
4438
}
4539

46-
public debugOnEmulator(): IFuture<void> {
40+
private debugOnEmulator(): IFuture<void> {
4741
return (() => {
4842
this.$platformService.deployOnEmulator(this.platform).wait();
4943
this.debugOnDevice().wait();
5044
}).future<void>()();
5145
}
5246

53-
public debugOnDevice(): IFuture<void> {
47+
private isPortAvailable(candidatePort: number): IFuture<boolean> {
48+
let future = new Future<boolean>();
49+
let server = net.createServer();
50+
server.on("error", (err: Error) => {
51+
future.return(false);
52+
});
53+
server.listen(candidatePort, (err: Error) => {
54+
server.once("close", () => {
55+
future.return(true);
56+
});
57+
server.close();
58+
});
59+
60+
return future;
61+
}
62+
63+
private getForwardedLocalDebugPortForPackageName(deviceId: string, packageName: string): IFuture<number> {
64+
return (() => {
65+
let port = -1;
66+
let forwardsResult = this.device.adb.executeCommand(["forward", "--list"]).wait();
67+
68+
//matches 123a188909e6czzc tcp:40001 localabstract:org.nativescript.testUnixSockets-debug
69+
let regexp = new RegExp(`(?:${deviceId} tcp:)([\\d]+)(?= localabstract:${packageName}-debug)`, "g");
70+
let match = regexp.exec(forwardsResult);
71+
if (match) {
72+
port = parseInt(match[1]);
73+
} else {
74+
let candidatePort = 40000;
75+
while (!this.isPortAvailable(candidatePort++).wait()) {
76+
if (candidatePort > 65534) {
77+
this.$errors.failWithoutHelp("Unable to find free local port.");
78+
}
79+
}
80+
port = candidatePort;
81+
82+
this.unixSocketForward(port, packageName + "-debug").wait();
83+
}
84+
85+
return port;
86+
}).future<number>()();
87+
}
88+
89+
private unixSocketForward(local: number, remote: string): IFuture<void> {
90+
return this.device.adb.executeCommand(["forward", `tcp:${local.toString()}`, `localabstract:${remote}`]);
91+
}
92+
93+
private debugOnDevice(): IFuture<void> {
5494
return (() => {
5595
let packageFile = "";
5696

@@ -78,183 +118,102 @@ class AndroidDebugService implements IDebugService {
78118
}
79119

80120
private debugCore(device: Mobile.IAndroidDevice, packageFile: string, packageName: string): IFuture<void> {
81-
return (() => {
121+
return (() => {
82122
this.device = device;
83123

84-
if (this.$options.getPort) {
85-
this.printDebugPort(packageName).wait();
86-
} else if (this.$options.start) {
87-
this.attachDebugger(packageName);
88-
} else if (this.$options.stop) {
89-
this.detachDebugger(packageName).wait();
90-
} else if (this.$options.debugBrk) {
91-
this.startAppWithDebugger(packageFile, packageName).wait();
92-
}
93-
}).future<void>()();
94-
}
95-
96-
private printDebugPort(packageName: string): IFuture<void> {
97-
return (() => {
98-
let res = this.device.adb.executeShellCommand(["am", "broadcast", "-a", packageName + "-GetDbgPort"]).wait();
99-
this.$logger.info(res);
100-
}).future<void>()();
101-
}
102-
103-
private attachDebugger(packageName: string): void {
104-
let startDebuggerCommand = ["am", "broadcast", "-a", '\"${packageName}-Debug\"', "--ez", "enable", "true"];
105-
let port = this.$options.debugPort;
106-
107-
if (port > 0) {
108-
startDebuggerCommand.push("--ei", "debuggerPort", port.toString());
109-
this.device.adb.executeShellCommand(startDebuggerCommand).wait();
110-
} else {
111-
let res = this.device.adb.executeShellCommand(["am", "broadcast", "-a", packageName + "-Debug", "--ez", "enable", "true"]).wait();
112-
let match = res.match(/result=(\d)+/);
113-
if (match) {
114-
port = match[0].substring(7);
115-
} else {
116-
port = 0;
117-
}
118-
}
119-
if ((0 < port) && (port < 65536)) {
120-
this.tcpForward(port, port).wait();
121-
this.startDebuggerClient(port).wait();
122-
this.openDebuggerClient(AndroidDebugService.DEFAULT_NODE_INSPECTOR_URL + "?port=" + port);
123-
} else {
124-
this.$logger.info("Cannot detect debug port.");
125-
}
126-
}
127-
128-
private detachDebugger(packageName: string): IFuture<void> {
129-
return this.device.adb.executeShellCommand(["am", "broadcast", "-a", `${packageName}-Debug`, "--ez", "enable", "false"]);
130-
}
131-
132-
private startAppWithDebugger(packageFile: string, packageName: string): IFuture<void> {
133-
return (() => {
134-
if(!this.$options.emulator) {
135-
this.device.applicationManager.uninstallApplication(packageName).wait();
136-
this.device.applicationManager.installApplication(packageFile).wait();
137-
}
138-
this.debugStartCore().wait();
139-
}).future<void>()();
140-
}
141-
142-
public debugStart(): IFuture<void> {
143-
return (() => {
144-
this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device}).wait();
145-
let action = (device: Mobile.IAndroidDevice): IFuture<void> => {
146-
this.device = device;
147-
return this.debugStartCore();
148-
};
149-
this.$devicesService.execute(action).wait();
150-
}).future<void>()();
151-
}
152-
153-
private debugStartCore(): IFuture<void> {
154-
return (() => {
155-
let packageName = this.$projectData.projectId;
156-
let packageDir = util.format(AndroidDebugService.PACKAGE_EXTERNAL_DIR_TEMPLATE, packageName);
157-
let envDebugOutFullpath = this.$mobileHelper.buildDevicePath(packageDir, AndroidDebugService.ENV_DEBUG_OUT_FILENAME);
158-
159-
this.device.adb.executeShellCommand(["rm", `${envDebugOutFullpath}`]).wait();
160-
this.device.adb.executeShellCommand(["mkdir", "-p", `${packageDir}`]).wait();
161-
162-
let debugBreakPath = this.$mobileHelper.buildDevicePath(packageDir, "debugbreak");
163-
this.device.adb.executeShellCommand([`cat /dev/null > ${debugBreakPath}`]).wait();
164-
165-
this.device.applicationManager.stopApplication(packageName).wait();
166-
this.device.applicationManager.startApplication(packageName).wait();
167-
168-
let dbgPort = this.startAndGetPort(packageName).wait();
169-
if (dbgPort > 0) {
170-
this.tcpForward(dbgPort, dbgPort).wait();
171-
this.startDebuggerClient(dbgPort).wait();
172-
this.openDebuggerClient(AndroidDebugService.DEFAULT_NODE_INSPECTOR_URL + "?port=" + dbgPort);
173-
}
174-
}).future<void>()();
175-
}
176-
177-
private tcpForward(src: Number, dest: Number): IFuture<void> {
178-
return this.device.adb.executeCommand(["forward", `tcp:${src.toString()}`, `tcp:${dest.toString()}`]);
179-
}
180-
181-
private startDebuggerClient(port: Number): IFuture<void> {
182-
return (() => {
183-
let nodeInspectorModuleFilePath = require.resolve("node-inspector");
184-
let nodeInspectorModuleDir = path.dirname(nodeInspectorModuleFilePath);
185-
let nodeInspectorFullPath = path.join(nodeInspectorModuleDir, "bin", "inspector");
186-
this.$childProcess.spawn(process.argv[0], [nodeInspectorFullPath, "--debug-port", port.toString()], { stdio: "ignore", detached: true });
187-
}).future<void>()();
188-
}
189-
190-
private openDebuggerClient(url: string): void {
191-
let defaultDebugUI = "chrome";
192-
if(this.$hostInfo.isDarwin) {
193-
defaultDebugUI = "Google Chrome";
194-
}
195-
if(this.$hostInfo.isLinux) {
196-
defaultDebugUI = "google-chrome";
197-
}
124+
if (this.$options.getPort) {
125+
this.printDebugPort(device.deviceInfo.identifier, packageName).wait();
126+
} else if (this.$options.start) {
127+
this.attachDebugger(device.deviceInfo.identifier, packageName).wait();
128+
} else if (this.$options.stop) {
129+
this.detachDebugger(packageName).wait();
130+
} else if (this.$options.debugBrk) {
131+
this.startAppWithDebugger(packageFile, packageName).wait();
132+
}
133+
}).future<void>()();
134+
}
135+
136+
private printDebugPort(deviceId: string, packageName: string): IFuture<void> {
137+
return (() => {
138+
let port = this.getForwardedLocalDebugPortForPackageName(deviceId, packageName).wait();
139+
this.$logger.info(port);
140+
}).future<void>()();
141+
}
142+
143+
private attachDebugger(deviceId: string, packageName: string): IFuture<void> {
144+
return (() => {
145+
let startDebuggerCommand = ["am", "broadcast", "-a", '\"${packageName}-debug\"', "--ez", "enable", "true"];
146+
this.device.adb.executeShellCommand(startDebuggerCommand).wait();
147+
148+
let port = this.getForwardedLocalDebugPortForPackageName(deviceId, packageName).wait();
149+
this.startDebuggerClient(port).wait();
150+
this.openDebuggerClient(AndroidDebugService.DEFAULT_NODE_INSPECTOR_URL + "?port=" + port);
151+
}).future<void>()();
152+
}
153+
154+
private detachDebugger(packageName: string): IFuture<void> {
155+
return this.device.adb.executeShellCommand(["am", "broadcast", "-a", `${packageName}-debug`, "--ez", "enable", "false"]);
156+
}
157+
158+
private startAppWithDebugger(packageFile: string, packageName: string): IFuture<void> {
159+
return (() => {
160+
if(!this.$options.emulator) {
161+
this.device.applicationManager.uninstallApplication(packageName).wait();
162+
this.device.applicationManager.installApplication(packageFile).wait();
163+
}
164+
this.debugStartCore().wait();
165+
}).future<void>()();
166+
}
167+
168+
public debugStart(): IFuture<void> {
169+
return (() => {
170+
this.$devicesService.initialize({ platform: this.platform, deviceId: this.$options.device}).wait();
171+
let action = (device: Mobile.IAndroidDevice): IFuture<void> => {
172+
this.device = device;
173+
return this.debugStartCore();
174+
};
175+
this.$devicesService.execute(action).wait();
176+
}).future<void>()();
177+
}
178+
179+
private debugStartCore(): IFuture<void> {
180+
return (() => {
181+
let packageName = this.$projectData.projectId;
182+
183+
this.device.adb.executeShellCommand([`cat /dev/null > /data/local/tmp/${packageName}-debugbreak`]).wait();
184+
185+
this.device.applicationManager.stopApplication(packageName).wait();
186+
this.device.applicationManager.startApplication(packageName).wait();
187+
188+
let localDebugPort = this.getForwardedLocalDebugPortForPackageName(this.device.deviceInfo.identifier, packageName).wait();
189+
this.startDebuggerClient(localDebugPort).wait();
190+
this.openDebuggerClient(AndroidDebugService.DEFAULT_NODE_INSPECTOR_URL + "?port=" + localDebugPort);
191+
}).future<void>()();
192+
}
193+
194+
private startDebuggerClient(port: Number): IFuture<void> {
195+
return (() => {
196+
let nodeInspectorModuleFilePath = require.resolve("node-inspector");
197+
let nodeInspectorModuleDir = path.dirname(nodeInspectorModuleFilePath);
198+
let nodeInspectorFullPath = path.join(nodeInspectorModuleDir, "bin", "inspector");
199+
this.$childProcess.spawn(process.argv[0], [nodeInspectorFullPath, "--debug-port", port.toString()], { stdio: "ignore", detached: true });
200+
}).future<void>()();
201+
}
202+
203+
private openDebuggerClient(url: string): void {
204+
let defaultDebugUI = "chrome";
205+
if(this.$hostInfo.isDarwin) {
206+
defaultDebugUI = "Google Chrome";
207+
}
208+
if(this.$hostInfo.isLinux) {
209+
defaultDebugUI = "google-chrome";
210+
}
198211

199212
let debugUI = this.$config.ANDROID_DEBUG_UI || defaultDebugUI;
200213
let child = this.$opener.open(url, debugUI);
201214
if(!child) {
202215
this.$errors.failWithoutHelp(`Unable to open ${debugUI}.`);
203216
}
204-
}
205-
206-
private checkIfRunning(packageName: string): boolean {
207-
let packageDir = util.format(AndroidDebugService.PACKAGE_EXTERNAL_DIR_TEMPLATE, packageName);
208-
let envDebugOutFullpath = packageDir + AndroidDebugService.ENV_DEBUG_OUT_FILENAME;
209-
let isRunning = this.checkIfFileExists(envDebugOutFullpath).wait();
210-
return isRunning;
211-
}
212-
213-
private checkIfFileExists(filename: string): IFuture<boolean> {
214-
return (() => {
215-
let res = this.device.adb.executeShellCommand([`test -f ${filename} && echo 'yes' || echo 'no'`]).wait();
216-
let exists = res.indexOf('yes') > -1;
217-
return exists;
218-
}).future<boolean>()();
219-
}
220-
221-
private startAndGetPort(packageName: string): IFuture<number> {
222-
return (() => {
223-
let port = -1;
224-
let timeout = this.$utils.getParsedTimeout(90);
225-
226-
let packageDir = util.format(AndroidDebugService.PACKAGE_EXTERNAL_DIR_TEMPLATE, packageName);
227-
let envDebugInFullpath = packageDir + AndroidDebugService.ENV_DEBUG_IN_FILENAME;
228-
this.device.adb.executeShellCommand(["rm", `${envDebugInFullpath}`]).wait();
229-
230-
let isRunning = false;
231-
for (let i = 0; i < timeout; i++) {
232-
helpers.sleep(1000 /* ms */);
233-
isRunning = this.checkIfRunning(packageName);
234-
if (isRunning) {
235-
break;
236-
}
237-
}
238-
239-
if (isRunning) {
240-
this.device.adb.executeShellCommand([`cat /dev/null > ${envDebugInFullpath}`]).wait();
241-
242-
for (let i = 0; i < timeout; i++) {
243-
helpers.sleep(1000 /* ms */);
244-
let envDebugOutFullpath = packageDir + AndroidDebugService.ENV_DEBUG_OUT_FILENAME;
245-
let exists = this.checkIfFileExists(envDebugOutFullpath).wait();
246-
if (exists) {
247-
let res = this.device.adb.executeShellCommand(["cat", envDebugOutFullpath]).wait();
248-
let match = res.match(/PORT=(\d)+/);
249-
if (match) {
250-
port = parseInt(match[0].substring(5), 10);
251-
break;
252-
}
253-
}
254-
}
255-
}
256-
return port;
257-
}).future<number>()();
258-
}
217+
}
259218
}
260219
$injector.register("androidDebugService", AndroidDebugService);

0 commit comments

Comments
 (0)