UNPKG

@u4/adbkit

Version:

A Typescript client for the Android Debug Bridge.

1,052 lines (1,051 loc) 49.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _DeviceClient_extra; Object.defineProperty(exports, "__esModule", { value: true }); const adbkit_monkey_1 = require("@u4/adbkit-monkey"); const adbkit_logcat_1 = __importDefault(require("@u4/adbkit-logcat")); const sync_1 = __importDefault(require("./sync")); const stat_1 = __importDefault(require("./proc/stat")); const host_1 = require("./command/host"); const hostCmd = __importStar(require("./command/host-transport")); const host_serial_1 = require("./command/host-serial"); const utils_1 = __importDefault(require("./utils")); const Scrcpy_1 = __importDefault(require("./thirdparty/scrcpy/Scrcpy")); const Minicap_1 = __importDefault(require("./thirdparty/minicap/Minicap")); const STFService_1 = __importDefault(require("./thirdparty/STFService/STFService")); const promise_duplex_1 = __importDefault(require("promise-duplex")); const protocol_1 = __importDefault(require("./protocol")); const serviceCall_1 = __importDefault(require("./command/host-transport/serviceCall")); const DeviceClientExtra_1 = __importDefault(require("./DeviceClientExtra")); const DevicePackage_1 = __importDefault(require("./DevicePackage")); const get_port_1 = __importDefault(require("get-port")); const debug = utils_1.default.debug('adb:client'); const NoUserOptionError = (err) => err.message.indexOf('--user') !== -1; class DeviceClient { constructor(client, serial, options) { this.client = client; this.serial = serial; _DeviceClient_extra.set(this, void 0); options = options || {}; const sudo = options.sudo || false; this.options = { sudo }; } sudo() { if (this.options.sudo) return this; else return new DeviceClient(this.client, this.serial, { ...this.options, sudo: true }); } /** * Gets the serial number of the device identified by the given serial number. With our API this doesn't really make much sense, but it has been implemented for completeness. _FYI: in the raw ADB protocol you can specify a device in other ways, too._ * * @returns The serial number of the device. */ async getSerialNo() { const conn = await this.connection(); return new host_serial_1.GetSerialNoCommand(conn).execute(this.serial); } /** * Gets the device path of the device identified by the given serial number. * @returns The device path. This corresponds to the device path in `client.listDevicesWithPaths()`. * * @example * List devices withPath * ```ts * import Adb from '@u4/adbkit'; * const client = Adb.createClient(); * const devices = client.listDevicesWithPaths(); * devices.then((devices) => { * devices.forEach(function (d) { * console.log('id: ' + d.id); * console.log('type: ' + d.type); * console.log('model ' + d.model); * console.log('path: ' + d.path); * console.log('product: ' + d.product); * console.log('transportId: ' + d.transportId + '\n'); * }); * }); * ``` */ async getDevicePath() { const conn = await this.connection(); return new host_serial_1.GetDevicePathCommand(conn).execute(this.serial); } /** * Gets the state of the device identified by the given serial number. * * @returns The device state. This corresponds to the device type in `client.listDevices()`. */ async getState() { const conn = await this.connection(); return new host_serial_1.GetStateCommand(conn).execute(this.serial); } /** * Retrieves the properties of the device identified by the given serial number. This is analogous to `adb shell getprop`. * * @returns An object of device properties. Each key corresponds to a device property. Convenient for accessing things like `'ro.product.model'`. */ async getProperties() { const transport = await this.transport(); return new hostCmd.GetPropertiesCommand(transport, this.options).execute(); } /** * Retrieves the features of the device identified by the given serial number. This is analogous to `adb shell pm list features`. Useful for checking whether hardware features such as NFC are available (you'd check for `'android.hardware.nfc'`). * * @returns An object of device features. Each key corresponds to a device feature, with the value being either `true` for a boolean feature, or the feature value as a string (e.g. `'0x20000'` for `reqGlEsVersion`). * * @example * Checking for NFC support * ```ts * import Adb from '@u4/adbkit'; * * const client = Adb.createClient(); * * const test = async () => { * try { * const devices = await client.listDevices(); * const supportedDevices: string[] = []; * for (const device of devices) { * const client = device.client(); * const features = await client.getFeatures(device.id); * if (features['android.hardware.nfc']) * supportedDevices.push(device.serial); * } * console.log('The following devices support NFC:', supportedDevices); * } catch (err) { * console.error('Something went wrong:', err.stack); * } * }; * ``` */ async getFeatures() { const transport = await this.transport(); return new hostCmd.GetFeaturesCommand(transport, this.options).execute(); } /** * Retrieves the list of packages present on the device. This is analogous to `adb shell pm list packages`. If you just want to see if something's installed, consider using `client.isInstalled()` instead. * * @param flags Flags to pass to the `pm list packages` command to filter the list * -d: filter to only show disabled packages * -e: filter to only show enabled packages * -s: filter to only show system packages * -3: filter to only show third party packages * @returns An object of device features. Each key corresponds to a device feature, with the value being either `true` for a boolean feature, or the feature value as a string (e.g. `'0x20000'` for `reqGlEsVersion`) */ async getPackages(flags) { const transport = await this.transport(); return new hostCmd.GetPackagesCommand(transport, this.options).execute(flags); } /** * Retrieves the list of running Process * * @param flags TODO * @returns a PsEntry array */ async getPs(...flags) { const transport = await this.transport(); return new hostCmd.PsCommand(transport, this.options).execute(...flags); } /** * call ip route command * * @returns a IpRouteEntry array */ async ipRoute(...args) { const transport = await this.transport(); return new hostCmd.IpRouteCommand(transport, this.options).execute(...args); } /** * call ip rule command * * @returns a IpRuleEntry array */ async ipRule(...args) { const transport = await this.transport(); return new hostCmd.IpRuleCommand(transport, this.options).execute(...args); } /** * Retrieves the list of available services * * @returns a PsEntry array */ async getServices() { const transport = await this.transport(); return new hostCmd.ServicesListCommand(transport, this.options).execute(); } /** * Retrieves the list of available services * * @returns a PsEntry array */ async checkService(serviceName) { const transport = await this.transport(); return new hostCmd.ServiceCheckCommand(transport, this.options).execute(serviceName); } /** * exec a service call command and return Parcel responce Data as a Buffer * * @returns a PsEntry array */ async callServiceRaw(serviceName, code, ...args) { const transport = await this.transport(); return new serviceCall_1.default(transport, this.options).execute(serviceName, code, args); } /** * Attemps to retrieve the IP addresses of the device. using `ip addr show` command. * * @param [iface] The network interface. Defaults to `'wlan0'`. * * @returns The IP addresses as string[] starting with IPv4 then IPv6. */ async getIpAddress(iface = 'wlan0') { const ipData = await this.execOut(`ip addr show ${iface}`, 'utf-8'); const ipV4 = [...ipData.matchAll(/inet ([\d]+\.[\d]+\.[\d]+\.[\d]+)\/\d+/g)].map(m => m[1]); const ipV6 = [...ipData.matchAll(/inet6 ([0-9a-f:]+)\/\d+/g)].map(m => m[1]); return [...ipV4, ...ipV6]; } /** * Forwards socket connections from the ADB server host (local) to the device (remote). This is analogous to `adb forward <local> <remote>`. It's important to note that if you are connected to a remote ADB server, the forward will be created on that host. * * @param local A string representing the local endpoint on the ADB host. At time of writing, can be one of: * - `tcp:<port>` * - `localabstract:<unix domain socket name>` * - `localreserved:<unix domain socket name>` * - `localfilesystem:<unix domain socket name>` * - `dev:<character device name>` * @param remote A string representing the remote endpoint on the device. At time of writing, can be one of: * Any value accepted by the `local` argument * `jdwp:<process pid>` * @returns true */ async forward(local, remote) { const conn = await this.connection(); return new host_serial_1.ForwardCommand(conn).execute(this.serial, local, remote); } /** * Get a localTCP port connected to remote socket, this method will try to get the requested port, but if the port is already taken, will choose an other one. * Note if a foward already existe to the same destination with a different port, no new foward will be create. * @param preferedPort the TCP port you would like to get. * @param remote A string representing the remote endpoint on the device. At time of writing, can be one of: * Any value accepted by the `local` argument * `jdwp:<process pid>` * @returns the used port */ async tryForwardTCP(remote, preferedPort) { const fwds = await this.listForwards(); // const usedPort = fwds.filter(a => a.local === local); const usedPort = fwds.filter(a => a.remote === remote); const prev = usedPort.filter(a => a.remote === remote && a.serial === this.serial); if (prev.length) { // already connected return Number(prev[0].local.substring(4)); } if (preferedPort && !usedPort.length) try { if (await this.forward(`tcp:${preferedPort}`, remote)) return preferedPort; } catch (e) { // need a new port } const freePort = await (0, get_port_1.default)(); await this.forward(`tcp:${freePort}`, remote); return freePort; } /** * Remove the port forward at ADB server host (local). This is analogous to adb forward --remove <local>. It's important to note that if you are connected to a remote ADB server, the forward on that host will be removed. * @param local A string representing the local endpoint on the ADB host. At time of writing, can be one of: `tcp:<port>`, `localabstract:<unix domain socket name>`, `localreserved:<unix domain socket name>`, `localfilesystem:<unix domain socket name>`, `dev:<character device name>` * @returns true */ async removeForward(local) { const conn = await this.connection(); return new host_serial_1.KillForwardCommand(conn).execute(this.serial, local); } /** * Lists forwarded connections on the device. This is analogous to `adb forward --list`. * * @returns An array of forward objects with the following properties: * - **serial** The device serial. * - **local** The local endpoint. Same format as `client.forward()`'s `local` argument. * - **remote** The remote endpoint on the device. Same format as `client.forward()`'s `remote` argument. */ async listForwards() { const conn = await this.connection(); return new host_serial_1.ListForwardsCommand(conn).execute(this.serial); } /** * Reverses socket connections from the device (remote) to the ADB server host (local). This is analogous to `adb reverse <remote> <local>`. It's important to note that if you are connected to a remote ADB server, the reverse will be created on that host. * @param remote A string representing the remote endpoint on the device. At time of writing, can be one of: * - `tcp:<port>` * - `localabstract:<unix domain socket name>` * - `localreserved:<unix domain socket name>` * - `localfilesystem:<unix domain socket name>` * @param local A string representing the local endpoint on the ADB host. At time of writing, can be any value accepted by the `remote` argument. * @return true */ async reverse(remote, local) { const transport = await this.transport(); return new hostCmd.ReverseCommand(transport).execute(remote, local); } /** * Lists forwarded connections on the device. This is analogous to `adb reverse --list`. * * @returns An array of Reverse objects with the following properties: * - **remote** The remote endpoint on the device. Same format as `client.reverse()`'s `remote` argument. * - **local** The local endpoint on the host. Same format as `client.reverse()`'s `local` argument. */ async listReverses() { const transport = await this.transport(); return new hostCmd.ListReversesCommand(transport).execute(); } /** * return a new connection to ADB. */ connection() { return this.client.connection(); } /** * return a new connextion to the current Host devices */ async transport() { const conn = await this.connection(); await new host_1.HostTransportCommand(conn).execute(this.serial); return conn; } /** * Runs a shell command on the device. Note that you'll be limited to the permissions of the `shell` user, which ADB uses. * * @param command The shell command to execute. When `String`, the command is run as-is. When `Array`, the elements will be rudimentarily escaped (for convenience, not security) and joined to form a command. * * @returns A readable stream (`Socket` actually) containing the progressive `stdout` of the command. Use with `adb.util.readAll` to get a readable String from it. * @example * Read the output of an instantaneous command * ```ts * import Adb from '@u4/adbkit'; * * try { * const client = Adb.createClient(); * const devices = await client.listDevices(); * for (const device of devices) { * const stream = await device.getClient().shell('echo $RANDOM'); * // Use the readAll() utility to read all the content without * // having to deal with the readable stream. `output` will be a Buffer * // containing all the output. * const output = await adb.util.readAll(stream); * console.log('[%s] %s', device.id, output.toString().trim()); * } * console.log('Done.'); * } catch(err) { * console.error('Something went wrong:', err.stack); * } * ``` * @example * Progressively read the output of a long-running command and terminate it * ```ts * import Adb from '@u4/adbkit'; * * const client = Adb.createClient(); * const devices = await client.listDevices() * for (const device of devices) { * // logcat just for illustration, prefer client.openLogcat in real use * const conn = await device.getClient().shell('logcat') * let line = 0 * conn.on('data', function(data) { * // here `ps` on the device shows the running logcat process * console.log(data.toString()) * line += 1 * // close the stream and the running process * // on the device will be gone, gracefully * if (line > 100) conn.end() * }); * conn.on('close', function() { * // here `ps` on the device shows the logcat process is gone * console.log('100 lines read already, bye') * }) * } * console.log('Done.') * ``` */ async shell(command) { const transport = await this.transport(); return new hostCmd.ShellCommand(transport, this.options).execute(command); } async exec(command) { const transport = await this.transport(); return new hostCmd.ExecCommand(transport, this.options).execute(command); } async execOut(command, encoding) { const duplex = new promise_duplex_1.default(await (this.exec(command))); if (encoding) { duplex.setEncoding(encoding); } return duplex.readAll(); } /** * Puts the device into root mode which may be needed by certain shell commands. A remount is generally required after a successful root call. **Note that this will only work if your device supports this feature. Production devices almost never do.** * * @return true */ async reboot(type) { const transport = await this.transport(); return new hostCmd.RebootCommand(transport, this.options).execute(type); } /** * Attempts to remount the `/system` partition in read-write mode. This will usually only work on emulators and developer devices. * * @returns true */ async remount() { const transport = await this.transport(); return new hostCmd.RemountCommand(transport, this.options).execute(); } /** * Puts the device into root mode which may be needed by certain shell commands. A remount is generally required after a successful root call. **Note that this will only work if your device supports this feature. Production devices almost never do.** * * @return true */ async root() { const transport = await this.transport(); return new hostCmd.RootCommand(transport, this.options).execute(); } /** * Starts a JDWP tracker for the given device. * * Note that as the tracker will keep a connection open, you must call `tracker.end()` if you wish to stop tracking JDWP processes. * * @returns The JDWP tracker, which is an [`EventEmitter`][node-events]. The following events are available: * - **add** **(pid)** Emitted when a new JDWP process becomes available, once per pid. * - **remove** **(pid)** Emitted when a JDWP process becomes unavailable, once per pid. * - **changeSet** **(changes, pids)** All changes in a single event. * - **changes** An object with the following properties always present: * - **added** An array of pids that were added. Empty if none. * - **removed** An array of pids that were removed. Empty if none. * - **pids** All currently active pids (including pids from previous runs). * - **end** Emitted when the underlying connection ends. * - **error** **(err)** Emitted if there's an error. */ async trackJdwp() { const transport = await this.transport(); return new hostCmd.TrackJdwpCommand(transport, this.options).execute(); } /** * Fetches the current **raw** framebuffer (i.e. what is visible on the screen) from the device, and optionally converts it into something more usable by using [GraphicsMagick][graphicsmagick]'s `gm` command, which must be available in `$PATH` if conversion is desired. Note that we don't bother supporting really old framebuffer formats such as RGB_565. If for some mysterious reason you happen to run into a `>=2.3` device that uses RGB_565, let us know. * * Note that high-resolution devices can have quite massive framebuffers. For example, a device with a resolution of 1920x1080 and 32 bit colors would have a roughly 8MB (`1920*1080*4` byte) RGBA framebuffer. Empirical tests point to about 5MB/s bandwidth limit for the ADB USB connection, which means that it can take ~1.6 seconds for the raw data to arrive, or even more if the USB connection is already congested. Using a conversion will further slow down completion. * * @param format The desired output format. Any output format supported by [GraphicsMagick][graphicsmagick] (such as `'png'`) is supported. Defaults to `'raw'` for raw framebuffer data. * * @returns The possibly converted framebuffer stream. The stream also has a `meta`.: */ async framebuffer(format = 'raw') { const transport = await this.transport(); return new hostCmd.FrameBufferCommand(transport, this.options).execute(format); } /** * Takes a screenshot in PNG format using the built-in `screencap` utility. This is analogous to `adb shell screencap -p`. Sadly, the utility is not available on most Android `<=2.3` devices, but a silent fallback to the `client.framebuffer()` command in PNG mode is attempted, so you should have its dependencies installed just in case. * * Generating the PNG on the device naturally requires considerably more processing time on that side. However, as the data transferred over USB easily decreases by ~95%, and no conversion being required on the host, this method is usually several times faster than using the framebuffer. Naturally, this benefit does not apply if we're forced to fall back to the framebuffer. * * For convenience purposes, if the screencap command fails (e.g. because it doesn't exist on older Androids), we fall back to `client.framebuffer(serial, 'png')`, which is slower and has additional installation requirements. * * @return The PNG stream. */ async screencap() { const transport = await this.transport(); try { return await new hostCmd.ScreencapCommand(transport, this.options).execute(); } catch (err) { debug(`Emulating screencap command due to '${err}'`); return this.framebuffer('png'); } } /** * Opens a direct connection to a unix domain socket in the given path. * * @param path The path to the socket. Prefixed with `'localfilesystem:'` by default, include another prefix (e.g. `'localabstract:'`) in the path to override. * * @returns The connection (i.e. [`net.Socket`][node-net]). Read and write as you please. Call `conn.end()` to end the connection. */ async openLocal(path) { const transport = await this.transport(); return new hostCmd.LocalCommand(transport, this.options).execute(path); } /** * Testing only */ async openLocal2(path, debugCtxt = '') { const transport = await this.transport(); const data = path.includes(':') ? path : `localfilesystem:${path}`; const duplex = new promise_duplex_1.default(transport.parser.raw()); await duplex.write(protocol_1.default.encodeData(data)); await utils_1.default.waitforReadable(duplex, 0, `openLocal2 ${data} - ${debugCtxt}`); const code = await duplex.read(4); if (!code.equals(protocol_1.default.bOKAY)) { if (code.equals(protocol_1.default.bFAIL)) throw await transport.parser.readError(); throw transport.parser.unexpected(code.toString('ascii'), 'OKAY or FAIL'); } return duplex; } /** * Opens a direct connection to a binary log file, providing access to the raw log data. Note that it is usually much more convenient to use the `client.openLogcat()` method, described separately. * * @param name The name of the log. Available logs include `'main'`, `'system'`, `'radio'` and `'events'`. * * @returns The binary log stream. Call `log.end()` when you wish to stop receiving data. */ async openLog(name) { const transport = await this.transport(); return new hostCmd.LogCommand(transport, this.options).execute(name); } /** * Opens a direct TCP connection to a port on the device, without any port forwarding required. * @param port The port number to connect to. * @param host Optional. The host to connect to. Allegedly this is supposed to establish a connection to the given host from the device, but we have not been able to get it to work at all. Skip the host and everything works great. * * @returns The TCP connection (i.e. [`net.Socket`][node-net]). Read and write as you please. Call `conn.end()` to end the connection. */ async openTcp(port, host) { const transport = await this.transport(); return new hostCmd.TcpCommand(transport, this.options).execute(port, host); } /** * Starts the built-in `monkey` utility on the device, connects to it using `client.openTcp()` and hands the connection to [adbkit-monkey][adbkit-monkey], a pure Node.js Monkey client. This allows you to create touch and key events, among other things. * * For more information, check out the [adbkit-monkey][adbkit-monkey] documentation. * * @param port Optional. The device port where you'd like Monkey to run at. Defaults to `1080`. * * @returns The Monkey client. Please see the [adbkit-monkey][adbkit-monkey] documentation for details. */ async openMonkey(port = 1080) { const tryConnect = async (times) => { try { const stream = await this.openTcp(port); const client = adbkit_monkey_1.Monkey.connectStream(stream); return client; } catch (err) { if ((times -= 1)) { debug(`Monkey can't be reached, trying ${times} more times`); await utils_1.default.delay(100); return tryConnect(times); } else { throw err; } } }; try { return await tryConnect(1); } catch { const transport = await this.transport(); const out = await new hostCmd.MonkeyCommand(transport, 1000, this.options).execute(port); const monkey = await tryConnect(20); return monkey.once('end', () => out.end()); } } /** * Calls the `logcat` utility on the device and hands off the connection to [adbkit-logcat][adbkit-logcat], a pure Node.js Logcat client. This is analogous to `adb logcat -B`, but the event stream will be parsed for you and a separate event will be emitted for every log entry, allowing for easy processing. * * For more information, check out the [adbkit-logcat][adbkit-logcat] documentation. * * @param options Optional. The following options are supported: * - **clear** When `true`, clears logcat before opening the reader. Not set by default. * * @returns The Logcat client. Please see the [adbkit-logcat][adbkit-logcat] documentation for details. */ async openLogcat(options = {}) { const transport = await this.transport(); const stream = await new hostCmd.LogcatCommand(transport, this.options).execute(options); return adbkit_logcat_1.default.readStream(stream, { fixLineFeeds: false }); } /** * Tracks `/proc/stat` and emits useful information, such as CPU load. * A single sync service instance is used to download the `/proc/stat` file for processing. * While doing this does consume some resources, it is very light and should not be a problem. * /proc/stat is pulled once per sec, and emit a 'load' event. * * @returns The `/proc/stat` tracker, which is an [`EventEmitter`][node-events]. Call `stat.end()` to stop tracking. The following events are available: * - **load** **(loads)** Emitted when a CPU load calculation is available. * - **loads** CPU loads of **online** CPUs. Each key is a CPU id (e.g. `'cpu0'`, `'cpu1'`) and the value an object with the following properties: * - **user** Percentage (0-100) of ticks spent on user programs. * - **nice** Percentage (0-100) of ticks spent on `nice`d user programs. * - **system** Percentage (0-100) of ticks spent on system programs. * - **idle** Percentage (0-100) of ticks spent idling. * - **iowait** Percentage (0-100) of ticks spent waiting for IO. * - **irq** Percentage (0-100) of ticks spent on hardware interrupts. * - **softirq** Percentage (0-100) of ticks spent on software interrupts. * - **steal** Percentage (0-100) of ticks stolen by others. * - **guest** Percentage (0-100) of ticks spent by a guest. * - **guestnice** Percentage (0-100) of ticks spent by a `nice`d guest. * - **total** Total. Always 100. */ async openProcStat() { const sync = await this.syncService(); return new stat_1.default(sync); } /** * Deletes all data associated with a package from the device. This is roughly analogous to `adb shell pm clear <pkg>`. * * @param pkg The package name. This is NOT the APK. * * @returns true */ async clear(pkg) { const transport = await this.transport(); return new hostCmd.ClearCommand(transport, this.options).execute(pkg); } /** * Installs the APK on the device, replacing any previously installed version. This is roughly analogous to `adb install -r <apk>`. * * Note that if the call seems to stall, you may have to accept a dialog on the phone first. * * @param apk When `String`, interpreted as a path to an APK file. When [`Stream`][node-stream], installs directly from the stream, which must be a valid APK. * @returns true * @example * This example requires the [request](https://www.npmjs.org/package/request) module. It also doesn't do any error handling (404 responses, timeouts, invalid URLs etc). * ```ts * import Adb from '@u4/adbkit'; * import request from 'request'; * import { Readable } from 'stream'; * * const client = Adb.createClient(); * * const test = async () => { * // The request module implements old-style streams, so we have to wrap it. * try { * // request is deprecated * const device = client.getClient('<serial>'); * await device.install(new Readable().wrap(request('http://example.org/app.apk') as any) as any) * console.log('Installed') * } catch (err) { * console.error('Something went wrong:', err.stack) * } * } * ``` * @example * Install an apk to all connected devices * ```ts * import Adb from '@u4/adbkit'; * * const client = Adb.createClient(); * const apk = 'vendor/app.apk'; * * const test = async () => { * try { * const devices = await client.listDevices(); * for (const device of devices) { * await device.getClient().install(apk); * console.log(`Installed ${apk} on all connected devices`); * } * } catch (err) { * console.error('Something went wrong:', err.stack); * } * }; * ``` */ async install(apk) { const temp = sync_1.default.temp(typeof apk === 'string' ? apk : '_stream.apk'); const transfer = await this.push(apk, temp); await transfer.waitForEnd(); const value = await this.installRemote(temp); return value; } /** * Installs an APK file which must already be located on the device file system, and replaces any previously installed version. Useful if you've previously pushed the file to the device for some reason (perhaps to have direct access to `client.push()`'s transfer stats). This is roughly analogous to `adb shell pm install -r <apk>` followed by `adb shell rm -f <apk>`. * * Note that if the call seems to stall, you may have to accept a dialog on the phone first. * * @param apk The path to the APK file on the device. The file will be removed when the command completes. * @returns true */ async installRemote(apk) { const transport = await this.transport(); try { await new hostCmd.InstallCommand(transport, this.options).execute(apk); } finally { await this.execOut(['rm', '-f', apk]); } return true; } /** * Uninstalls the package from the device. This is roughly analogous to `adb uninstall <pkg>`. * * @param pkg The package name. This is NOT the APK. * @returns true */ async uninstall(pkg, opts) { const transport = await this.transport(); return new hostCmd.UninstallCommand(transport, this.options).execute(pkg, opts); } /** * Tells you if the specific package is installed or not. This is analogous to `adb shell pm path <pkg>` and some output parsing. * * @param pkg The package name. This is NOT the APK. * * @returns `true` if the package is installed, `false` otherwise. */ async isInstalled(pkg) { const transport = await this.transport(); return new hostCmd.IsInstalledCommand(transport, this.options).execute(pkg); } /** * Starts the configured activity on the device. Roughly analogous to `adb shell am start <options>`. * * @param options The activity configuration. */ async startActivity(options) { try { const transport = await this.transport(); return await new hostCmd.StartActivityCommand(transport, this.options).execute(options); } catch (err) { if (err instanceof NoUserOptionError) { options.user = undefined; return this.startActivity(options); } throw err; } } /** * Starts the configured service on the device. Roughly analogous to `adb shell am startservice <options>`. * @param options The activity configuration. */ async startService(options) { try { const transport = await this.transport(); if (!(options.user || options.user === null)) { options.user = 0; } return await new hostCmd.StartServiceCommand(transport, this.options).execute(options); } catch (err) { if (err instanceof NoUserOptionError) { options.user = undefined; return this.startService(options); } throw err; } } /** * Establishes a new Sync connection that can be used to push and pull files. This method provides the most freedom and the best performance for repeated use, but can be a bit cumbersome to use. For simple use cases, consider using `client.stat()`, `client.push()` and `client.pull()`. * * @returns The Sync client. See below for details. Call `sync.end()` when done. */ async syncService() { const transport = await this.transport(); return new hostCmd.SyncCommand(transport, this.options).execute(); } /** * A convenience shortcut for `sync.stat()`, mainly for one-off use cases. The connection cannot be reused, resulting in poorer performance over multiple calls. However, the Sync client will be closed automatically for you, so that's one less thing to worry about. * * @param path The path. * * @returns An [`fs.Stats`][node-fs-stats] instance. While the `stats.is*` methods are available, only the following properties are supported: * - **mode** The raw mode. * - **size** The file size. * - **mtime** The time of last modification as a `Date`. */ async stat(path) { const sync = await this.syncService(); try { return await sync.stat(path); } finally { sync.end(); } } /** * A convenience shortcut for `sync.stat64()`, mainly for one-off use cases. The connection cannot be reused, resulting in poorer performance over multiple calls. However, the Sync client will be closed automatically for you, so that's one less thing to worry about. * * @param path The path. * * @returns An [`fs.Stats`][node-fs-stats] instance. While the `stats.is*` methods are available, only the following properties are supported: * - **mode** The raw mode. * - **size** The file size. * - **mtime** The time of last modification as a `Date`. */ async stat64(path) { const sync = await this.syncService(); try { return await sync.stat64(path); } finally { sync.end(); } } /** * A convenience shortcut for `sync.readdir()`, mainly for one-off use cases. The connection cannot be reused, resulting in poorer performance over multiple calls. However, the Sync client will be closed automatically for you, so that's one less thing to worry about. * * @param path See `sync.readdir()` for details. * @returns Files Lists * @example * List files in a folder * ```ts * import Bluebird from 'bluebird'; * import Adb from '@u4/adbkit'; * const client = Adb.createClient(); * * const test = async () => { * try { * const devices = await client.listDevices(); * await Bluebird.map(devices, async (device) => { * const files = await client.readdir(device.id, '/sdcard'); * // Synchronous, so we don't have to care about returning at the * // right time * files.forEach((file) => { * if (file.isFile()) { * console.log(`[${device.id}] Found file "${file.name}"`); * } * }); * }); * console.log('Done checking /sdcard files on connected devices'); * } catch (err) { * console.error('Something went wrong:', err.stack); * } * }; * ``` */ async readdir(path) { const sync = await this.syncService(); try { return await sync.readdir(path); } finally { sync.end(); } } /** * A convenience shortcut for `sync.readdir2()`, mainly for one-off use cases. The connection cannot be reused, resulting in poorer performance over multiple calls. However, the Sync client will be closed automatically for you, so that's one less thing to worry about. * * @param path See `sync.readdir()` for details. * @returns Files Lists */ async readdir64(path) { const sync = await this.syncService(); try { return await sync.readdir64(path); } finally { sync.end(); } } /** * A convenience shortcut for `sync.pull()`, mainly for one-off use cases. The connection cannot be reused, resulting in poorer performance over multiple calls. However, the Sync client will be closed automatically for you, so that's one less thing to worry about. * * @param path See `sync.pull()` for details. * * @returns A `PullTransfer` instance. * * @example * Pulling a file from all cofnnected devices * ```ts * import Bluebird from 'bluebird'; * import fs from 'fs'; * import Adb from '@u4/adbkit'; * const client = Adb.createClient(); * * const test = async () => { * try { * const devices = await client.listDevices(); * await Bluebird.map(devices, async (device) => { * const transfer = await client.pull(device.id, '/system/build.prop'); * const fn = `/tmp/${device.id}.build.prop`; * await new Bluebird((resolve, reject) => { * transfer.on('progress', (stats) => * console.log(`[${device.id}] Pulled ${stats.bytesTransferred} bytes so far`), * ); * transfer.on('end', () => { * console.log(`[${device.id}] Pull complete`); * resolve(device.id); * }); * transfer.on('error', reject); * transfer.pipe(fs.createWriteStream(fn)); * }); * }); * console.log('Done pulling /system/build.prop from all connected devices'); * } catch (err) { * console.error('Something went wrong:', err.stack); * } * }; * ``` */ async pull(path) { const sync = await this.syncService(); const pullTransfer = await sync.pull(path); pullTransfer.waitForEnd().finally(() => sync.end()); return pullTransfer; } /** * A convenience shortcut for `sync.push()`, mainly for one-off use cases. The connection cannot be reused, resulting in poorer performance over multiple calls. However, the Sync client will be closed automatically for you, so that's one less thing to worry about. * * @param contents See `sync.push()` for details. * @param path See `sync.push()` for details. * @param mode See `sync.push()` for details. * * @example * Pushing a file to all connected devices * ```ts * import Bluebird from 'bluebird'; * import Adb from '@u4/adbkit'; * const client = Adb.createClient(); * * const test = async () => { * try { * const devices = await client.listDevices(); * await Bluebird.map(devices, async (device) => { * const transfer = await client.push(device.id, 'temp/foo.txt', '/data/local/tmp/foo.txt'); * await new Bluebird(function (resolve, reject) { * transfer.on('progress', (stats) => * console.log(`[${device.id}] Pushed ${stats.bytesTransferred} bytes so far`), * ); * transfer.on('end', () => { * console.log('[${device.id}] Push complete'); * resolve(); * }); * transfer.on('error', reject); * }); * }); * console.log('Done pushing foo.txt to all connected devices'); * } catch (err) { * console.error('Something went wrong:', err.stack); * } * }; * ``` */ async push(contents, path, mode) { const sync = await this.syncService(); const transfert = await sync.push(contents, path, mode); transfert.waitForEnd().finally(() => sync.end()); return transfert; } /** * Puts the device's ADB daemon into tcp mode, allowing you to use `adb connect` or `client.connect()` to connect to it. Note that the device will still be visible to ADB as a regular USB-connected device until you unplug it. Same as `adb tcpip <port>`. * * @param port Optional. The port the device should listen on. Defaults to `5555`. * @returns The port the device started listening on. */ async tcpip(port = 5555) { const transport = await this.transport(); return new hostCmd.TcpIpCommand(transport, this.options).execute(port); } /** * Puts the device's ADB daemon back into USB mode. Reverses `client.tcpip()`. Same as `adb usb`. * * @returns true */ async usb() { const transport = await this.transport(); return new hostCmd.UsbCommand(transport, this.options).execute(); } /** * Waits until the device has finished booting. Note that the device must already be seen by ADB. This is roughly analogous to periodically checking `adb shell getprop sys.boot_completed`. * * @returns true */ async waitBootComplete() { const transport = await this.transport(); return new hostCmd.WaitBootCompleteCommand(transport, this.options).execute(); } /** * Waits until ADB can see the device. Note that you must know the serial in advance. Other than that, works like `adb -s serial wait-for-device`. If you're planning on reacting to random devices being plugged in and out, consider using `client.trackDevices()` instead. * * @returns The device ID. Can be useful for chaining. */ async waitForDevice() { const conn = await this.connection(); return new host_serial_1.WaitForDeviceCommand(conn).execute(this.serial); } /** * prepare a Scrcpy server * this server must be started with the start() method */ scrcpy(options) { const scrcpy = new Scrcpy_1.default(this, options); return scrcpy; } /** * prepare a minicap server * this server must be started with the start() method */ minicap(options) { const minicap = new Minicap_1.default(this, options); return minicap; } /** * prepare a STFService and STFagent * this server must be started with the start() method */ STFService(options) { const service = new STFService_1.default(this, options); return service; } /** * get extra fucntions * @returns an DeviceClientExtra */ get extra() { if (!__classPrivateFieldGet(this, _DeviceClient_extra, "f")) { __classPrivateFieldSet(this, _DeviceClient_extra, new DeviceClientExtra_1.default(this), "f"); } return __classPrivateFieldGet(this, _DeviceClient_extra, "f"); } /** * List package installed into the devices, * @param options list all or only third party apps * @returns an array of DevicePackage */ async listPackages(options) { options = options || {}; let cmd = 'pm list packages'; if (options.thirdparty) { cmd += ' -3'; } const list = await this.execOut(cmd, 'utf-8'); const packages = [...list.matchAll(/package:([\w\d.]+)/g)]; return packages.map(m => new DevicePackage_1.default(this, m[1])); } } _DeviceClient_extra = new WeakMap(); exports.default = DeviceClient; //# sourceMappingURL=DeviceClient.js.map