diff --git a/CHANGELOG.md b/CHANGELOG.md index e76f1de..9d7109f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +### version 1.3.0 +* Support `ADB_SERVER_SOCKET`, `ANDROID_ADB_SERVER_ADDRESS` & `ANDROID_ADB_SERVER_PORT` env vars when connecting to ADB. +* Replace `adbPort` configuration option with a new `adbSocket` value to allow ADB server host to be overidden. (`adbPort` is now deprecated). +* Allow the JDWP local port to be fixed using a new `jdwpPort` configuration option. + ### version 1.2.1 * Java Intellisense: automatically import dependencies of AndroidX libraries. * Debugger: Warn about open instances of Android Studio diff --git a/README.md b/README.md index a549eb1..285a30c 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,9 @@ The following settings are used to configure the debugger: // Fully qualified path to the built APK (Android Application Package). "apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk", - // Port number to connect to the local ADB (Android Debug Bridge) instance. - // Default: 5037 - "adbPort": 5037, + // `host:port` configuration for connecting to the ADB (Android Debug Bridge) server instance. + // Default: localhost:5037 + "adbSocket": "localhost:5037", // Automatically launch 'adb start-server' if not already started. // Default: true diff --git a/package-lock.json b/package-lock.json index 1f8b849..f7e899f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "android-dev-ext", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a1d4325..2943d1f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "android-dev-ext", "displayName": "Android", "description": "Android debugging support for VS Code", - "version": "1.2.1", + "version": "1.3.0", "publisher": "adelphes", "preview": true, "license": "MIT", @@ -238,8 +238,7 @@ "launch": { "required": [ "appSrcRoot", - "apkFile", - "adbPort" + "apkFile" ], "properties": { "amStartArgs": { @@ -265,12 +264,17 @@ }, "adbPort": { "type": "integer", - "description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037", + "description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037\nDeprecated: Configure the 'adbSocket' property instead.", "default": 5037 }, + "adbSocket": { + "type": "string", + "description": "`host : port` configuration for connecting to the ADB (Android Debug Bridge) server instance. Default: \"localhost:5037\"", + "default": "localhost:5037" + }, "autoStartADB": { "type": "boolean", - "description": "Automatically launch 'adb start-server' if not already started. Default: true", + "description": "Automatically attempt to launch 'adb start-server' if not already started. Default: true", "default": true }, "callStackDisplaySize": { @@ -278,6 +282,11 @@ "description": "Number of entries to display in call stack views (for locations outside of the project source). 0 shows the entire call stack. Default: 1", "default": 1 }, + "jdwpPort": { + "type": "integer", + "description": "Manually specify the local port used for connecting to the on-device debugger client.\nThis can be useful if you are using port-forwarding to connect to a remote device.\nThe specified port must be available and different from the ADB socket port.\nSet to 0 for automatic (dynamic) assignment.\nDefault: 0", + "default": 0 + }, "launchActivity": { "type": "string", "description": "Manually specify the activity to run when the app is started.", @@ -325,7 +334,6 @@ "attach": { "required": [ "appSrcRoot", - "adbPort", "processId" ], "properties": { @@ -336,9 +344,19 @@ }, "adbPort": { "type": "integer", - "description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037", + "description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037\nDeprecated: Configure the 'adbSocket' property instead.", "default": 5037 }, + "adbSocket": { + "type": "string", + "description": "`host : port` configuration for connecting to the ADB (Android Debug Bridge) server instance. Default: \"localhost:5037\"", + "default": "localhost:5037" + }, + "jdwpPort": { + "type": "integer", + "description": "Manually specify the local port used for connecting to the on-device debugger client.\nThis can be useful if you are using port-forwarding to connect to a remote device.\nThe specified port must be available and different from the ADB socket port.\nSet to 0 for automatic (dynamic) assignment.\nDefault: 0", + "default": 0 + }, "processId": { "type": "string", "description": "PID of process to attach to.\n\"${command:PickAndroidProcess}\" will display a list of debuggable PIDs to choose from during launch.", diff --git a/src/adbclient.js b/src/adbclient.js index af87844..8262d1c 100644 --- a/src/adbclient.js +++ b/src/adbclient.js @@ -36,17 +36,94 @@ function parse_device_list(data, extended = false) { return devicelist; } +let adbSocketParams; +/** + * Return the host and port for connecting to the ADB server + */ +function getADBSocketParams() { + // this is memoized to prevent alterations once the debug session is up and running + if (adbSocketParams) { + return adbSocketParams; + } + + return adbSocketParams = getIntialADBSocketParams(); +} + +/** + * Retrieve the socket parameters for connecting to an ADB server instance. + + * In priority order (highest first): + * 1. adbSocket debug configuration value + * 2. non-default adbPort debug configuration value (using localhost) + * 3. ADB_SERVER_SOCKET environment variable + * 4. ANDROID_ADB_SERVER_ADDRESS / ANDROID_ADB_SERVER_PORT environment variables + * 5. [localhost]:5037 + */ +function getIntialADBSocketParams() { + + /** + * Retrieve a trimmed environment variable or return a blank string + * @param {string} name + */ + function envValue(name) { + return (process.env[name] || '').trim(); + } + + function decode_port_string(s) { + if (!/^\d+$/.test(s)) { + return; + } + const portnum = parseInt(s, 10); + if (portnum < 1 || portnum > 65535) { + return; + } + return portnum; + } + + const default_host = '', default_port = 5037; + + // the ADBSocket.HostPort value is automatically set with adbSocket/adbPort values from + // the debug configuration when the debugger session starts. + let socket_str = ADBSocket.HostPort.trim(); + + if (socket_str !== ADBSocket.DefaultHostPort) { + // non-default debug configuration values are configured (1. or 2.) + const [host, port] = socket_str.split(':'); + return { + host, + port: decode_port_string(port) || default_port + } + } + + // ADB_SERVER_SOCKET=tcp:: + const adb_server_socket_match = envValue('ADB_SERVER_SOCKET').match(/^\s*tcp(?::(.*))?(?::(\d+))\s*$/); + if (adb_server_socket_match) { + return { + host: adb_server_socket_match[1] || default_host, + port: decode_port_string(adb_server_socket_match[2]) || default_port, + } + } + + return { + host: envValue('ANDROID_ADB_SERVER_ADDRESS') || default_host, + port: decode_port_string(envValue('ANDROID_ADB_SERVER_PORT')) || default_port, + } +} + class ADBClient { /** * @param {string} [deviceid] * @param {number} [adbPort] the port number to connect to ADB + * @param {number} [adbHost] the hostname/ip address to connect to ADB */ - constructor(deviceid, adbPort = ADBSocket.ADBPort) { + constructor(deviceid, adbPort, adbHost) { this.deviceid = deviceid; this.adbsocket = null; this.jdwp_socket = null; - this.adbPort = adbPort; + const default_adb_socket = getADBSocketParams(); + this.adbHost = adbHost || default_adb_socket.host; + this.adbPort = adbPort || default_adb_socket.port; } async test_adb_connection() { @@ -170,7 +247,10 @@ class ADBClient { // note that upon success, this method does not close the connection (it must be left open for // future commands to be sent over the jdwp socket) this.jdwp_socket = new JDWPSocket(o.onreply, o.ondisconnect); - await this.jdwp_socket.connect(o.localport) + // assume the 'local' port (routed to connect to the process on the device) + // is set up on the same host that the adb server is running on + const adb_server_socket = getADBSocketParams(); + await this.jdwp_socket.connect(o.localport, adb_server_socket.host); await this.jdwp_socket.start(); return true; } @@ -276,12 +356,9 @@ class ADBClient { return true; } - /** - * @param {string} [hostname] - */ - connect_to_adb(hostname = '127.0.0.1') { + connect_to_adb() { this.adbsocket = new ADBSocket(); - return this.adbsocket.connect(this.adbPort, hostname); + return this.adbsocket.connect(this.adbPort, this.adbHost); } disconnect_from_adb () { @@ -290,3 +367,4 @@ class ADBClient { }; exports.ADBClient = ADBClient; +exports.getADBSocketParams = getADBSocketParams; diff --git a/src/debugMain.js b/src/debugMain.js index 90da6b0..6e5ff37 100644 --- a/src/debugMain.js +++ b/src/debugMain.js @@ -325,8 +325,11 @@ class AndroidDebugSession extends DebugSession { /** * @typedef AndroidAttachArguments + * @property {number} adbPort + * @property {string} adbSocket * @property {string} appSrcRoot * @property {boolean} autoStartADB + * @property {number} jdwpPort * @property {number} processId * @property {string} targetDevice * @property {boolean} trace @@ -340,7 +343,7 @@ class AndroidDebugSession extends DebugSession { this.trace = args.trace; onMessagePrint(this.LOG.bind(this)); } - D(`Attach: ${JSON.stringify(args)}`); + D(JSON.stringify({type: 'attach', args, env:process.env}, null, ' ')); if (args.targetDevice === 'null') { // "null" is returned from the device picker if there's an error or if the @@ -368,6 +371,18 @@ class AndroidDebugSession extends DebugSession { return; } + // set the custom ADB host and port + if (typeof args.adbSocket === 'string' && args.adbSocket) { + ADBSocket.HostPort = args.adbSocket; + } else if (typeof args.adbPort === 'number' && args.adbPort >= 0 && args.adbPort <= 65535) { + ADBSocket.HostPort = `:${args.adbPort}`; + } + + // set the fixed JDWP port number (if any) + if (typeof args.jdwpPort === 'number' && args.jdwpPort >= 0 && args.jdwpPort <= 65535) { + Debugger.portManager.fixedport = args.jdwpPort; + } + try { // app_src_root must end in a path-separator for correct validation of sub-paths this.app_src_root = ensure_path_end_slash(args.appSrcRoot); @@ -446,10 +461,10 @@ class AndroidDebugSession extends DebugSession { if (/^ADB server is not running/.test(e.msg)) { this.LOG('Make sure the Android SDK Platform Tools are installed and run:'); this.LOG(' adb start-server'); - this.LOG('If you are running ADB on a non-default port, also make sure the adbPort value in your launch.json is correct.'); + this.LOG('If you are running ADB using a non-default configuration, also make sure the adbSocket value in your launch.json is correct.'); } if (/ADB|JDWP/.test(msg)) { - this.LOG('Ensure any instances of Android Studio are closed.'); + this.LOG('Ensure any instances of Android Studio are closed and ADB is running.'); } // tell the client we're done this.terminate_reason = `start-exception: ${msg}`; @@ -460,11 +475,13 @@ class AndroidDebugSession extends DebugSession { /** * @typedef AndroidLaunchArguments * @property {number} adbPort + * @property {string} adbSocket * @property {string[]} amStartArgs * @property {string} apkFile * @property {string} appSrcRoot * @property {boolean} autoStartADB * @property {number} callStackDisplaySize + * @property {number} jdwpPort * @property {string} launchActivity * @property {string} manifestFile * @property {string[]} pmInstallArgs @@ -484,7 +501,7 @@ class AndroidDebugSession extends DebugSession { this.trace = args.trace; onMessagePrint(this.LOG.bind(this)); } - D(`Launch: ${JSON.stringify(args)}`); + D(JSON.stringify({type: 'launch', args, env:process.env}, null, ' ')); if (args.targetDevice === 'null') { // "null" is returned from the device picker if there's an error or if the @@ -512,9 +529,16 @@ class AndroidDebugSession extends DebugSession { return; } - // set the custom ADB port - this should be changed to pass it to each ADBClient instance - if (typeof args.adbPort === 'number' && args.adbPort >= 0 && args.adbPort <= 65535) { - ADBSocket.ADBPort = args.adbPort; + // set the custom ADB host and port + if (typeof args.adbSocket === 'string' && args.adbSocket) { + ADBSocket.HostPort = args.adbSocket; + } else if (typeof args.adbPort === 'number' && args.adbPort >= 0 && args.adbPort <= 65535) { + ADBSocket.HostPort = `:${args.adbPort}`; + } + + // set the fixed JDWP port number (if any) + if (typeof args.jdwpPort === 'number' && args.jdwpPort >= 0 && args.jdwpPort <= 65535) { + Debugger.portManager.fixedport = args.jdwpPort; } try { diff --git a/src/debugger.js b/src/debugger.js index c6ba1e2..54651bd 100644 --- a/src/debugger.js +++ b/src/debugger.js @@ -52,9 +52,14 @@ class Debugger extends EventEmitter { static portManager = { portrange: { lowest: 31000, highest: 31099 }, + fixedport: 0, inuseports: new Set(), debuggers: {}, reserveport: function () { + if (this.fixedport > 0 && this.fixedport < 65536) { + this.inuseports.add(this.fixedport); + return this.fixedport; + } // choose a random port to use each time for (let i = 0; i < 10000; i++) { const portidx = this.portrange.lowest + ((Math.random() * 100) | 0); diff --git a/src/sockets/adbsocket.js b/src/sockets/adbsocket.js index 3d81744..1bc6b29 100644 --- a/src/sockets/adbsocket.js +++ b/src/sockets/adbsocket.js @@ -7,10 +7,17 @@ const AndroidSocket = require('./androidsocket'); class ADBSocket extends AndroidSocket { /** - * The port number to run ADB on. - * The value can be overriden by the adbPort value in each configuration. + * The host and port number to run ADB commands on, in 'host:port' format (host part is optional). + * The value can be overriden by the adbSocket (or the deprecated adbPort) value in each debug configuration. + * + * The default host value is left blank as this is the simplest way to + * specify "connect to the local machine" without explicitly specifying + * 'localhost' or '127.0.0.1' (which may be mapped to something else) */ - static ADBPort = 5037; + static HostPort = `:5037`; + static get DefaultHostPort() { + return `:5037` + } constructor() { super('ADBSocket'); @@ -18,13 +25,14 @@ class ADBSocket extends AndroidSocket { /** * Reads and checks the reply from an ADB command + * @param {string} command * @param {boolean} [throw_on_fail] true if the function should throw on non-OKAY status */ - async read_adb_status(throw_on_fail = true) { + async read_adb_status(command, throw_on_fail = true) { // read back the status const status = await this.read_bytes(4, 'latin1') if (status !== 'OKAY' && throw_on_fail) { - throw new Error(`ADB command failed. Status: '${status}'`); + throw new Error(`ADB command '${command}' failed. Status: '${status}'`); } return status; } @@ -57,7 +65,7 @@ class ADBSocket extends AndroidSocket { */ async cmd_and_status(command) { await this.write_adb_command(command); - return this.read_adb_status(); + return this.read_adb_status(command); } /** @@ -104,7 +112,7 @@ class ADBSocket extends AndroidSocket { await this.write_bytes(done_and_mtime); // read the final status and any error message - const result = await this.read_adb_status(false); + const result = await this.read_adb_status('sync:', false); const failmsg = await this.read_le_length_data('latin1'); // finish the transfer mode diff --git a/src/utils/android.js b/src/utils/android.js index 32926be..6c942dd 100644 --- a/src/utils/android.js +++ b/src/utils/android.js @@ -1,8 +1,7 @@ const fs = require('fs'); const path = require('path'); -const { ADBClient } = require('../adbclient'); -const ADBSocket = require('../sockets/adbsocket'); +const { ADBClient, getADBSocketParams } = require('../adbclient'); const { LOG } = require('../utils/print'); function getAndroidSDKFolder() { @@ -41,19 +40,21 @@ function getADBPathName() { return path.join(android_sdk, 'platform-tools', /^win/.test(process.platform)?'adb.exe':'adb'); } -/** - * @param {number} port - */ -function startADBServer(port) { - if (typeof port !== 'number' || port <= 0 || port >= 65536) { - return false; - } - +function startADBServer() { const adb_exe_path = getADBPathName(); if (!adb_exe_path) { return false; } - const adb_start_server_args = ['-P',`${port}`,'start-server']; + const adb_socket = getADBSocketParams(); + // don't try and start ADB if the server is on a remote host + if (!/^(localhost|127\.\d+\.\d+\.\d+)?$/.test(adb_socket.host)) { + LOG(`Cannot launch adb server on remote host ${adb_socket.host}:${adb_socket.port}`); + return; + } + const adb_start_server_args = ['-P',`${adb_socket.port}`,'start-server']; + if (adb_socket.host) { + adb_start_server_args.unshift(`-H`, adb_socket.host); + } try { LOG([adb_exe_path, ...adb_start_server_args].join(' ')); const stdout = require('child_process').execFileSync(adb_exe_path, adb_start_server_args, { @@ -73,7 +74,7 @@ async function checkADBStarted(auto_start) { const err = await new ADBClient().test_adb_connection(); // if adb is not running, see if we can start it ourselves using ANDROID_HOME (and a sensible port number) if (err && auto_start) { - return startADBServer(ADBSocket.ADBPort); + return startADBServer(); } return !err; }