UNPKG

promise-android-tools

Version:

A wrapper for adb, fastboot, and heimdall that returns convenient promises.

1,250 lines (1,237 loc) 44 kB
var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __reflectGet = Reflect.get; var __typeError = (msg) => { throw TypeError(msg); }; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __objRest = (source, exclude) => { var target = {}; for (var prop in source) if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0) target[prop] = source[prop]; if (source != null && __getOwnPropSymbols) for (var prop of __getOwnPropSymbols(source)) { if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop)) target[prop] = source[prop]; } return target; }; var __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp(target, key, result); return result; }; var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); var __superGet = (cls, obj, key) => __reflectGet(__getProtoOf(cls), key, obj); var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; // src/adb.ts import { stat } from "fs/promises"; // src/exec.ts import { execFile } from "child_process"; import { promisify } from "util"; import { spawn, ChildProcess, ExecException } from "child_process"; var exec = promisify(execFile); // src/android_tools.ts import { join } from "path"; import { existsSync } from "fs"; function getAndroidToolBaseDir(platform = process.platform, arch = process.arch) { const baseDir = join(__dirname, "..", "dist", platform, normalizedArch(arch)); return "electron" in process.versions ? baseDir.replace("app.asar", "app.asar.unpacked") : baseDir; } function normalizedArch(arch = process.arch) { switch (arch) { case "ia32": case "x64": return "x86"; case "arm": case "arm64": return "arm"; default: return arch; } } function getAndroidToolPath(tool, optimistic = true, native = {}, platform = process.platform, arch = process.arch) { try { if (native.all || process.env.USE_SYSTEM_TOOLS || native[tool] || process.env[`USE_SYSTEM_${tool.toUpperCase()}`]) { return tool; } const assumedPath = join( getAndroidToolBaseDir(platform, arch), platform === "win32" ? `${tool}.exe` : tool ); if (existsSync(assumedPath)) { return assumedPath; } else { throw new Error(`No binary of ${tool} for ${arch} ${platform}`); } } catch (error) { if (optimistic) return tool; else throw new Error(`Failed to get tool: ${error}`); } } // src/common.ts function removeFalsy(obj) { var _a; if (typeof obj !== "object" || Array.isArray(obj)) return obj; for (const i in obj) { if (Object.getOwnPropertyDescriptor(obj, i).get) { Object.defineProperty(obj, i, { value: removeFalsy(obj[i]), writable: true }); } if ((_a = obj[i]) == null ? void 0 : _a.trim) obj[i] = obj[i].trim(); if (!obj[i]) { delete obj[i]; } else { obj[i] = removeFalsy(obj[i]); if (!obj[i]) { delete obj[i]; } } } return Object.keys(obj).length ? obj : null; } // src/interface.ts import { EventEmitter } from "events"; import { use } from "typescript-mix"; // src/hierarchicalAbortController.ts var HierarchicalAbortController = class { constructor(...abortSignals) { this.controller = new AbortController(); this.signal = this.controller.signal; this.listen(...abortSignals); } abort() { this.controller.abort(); } listen(...abortSignals) { abortSignals.forEach((abortSignal) => { if (abortSignal.aborted) return this.controller.abort(abortSignal.reason); abortSignal.onabort = () => { this.controller.abort(this.signal.reason); }; }); if (this.signal.aborted) return this.controller.abort(this.signal.reason); this.signal = AbortSignal.any([this.controller.signal, ...abortSignals]); this.signal.onabort = () => { this.controller.abort(this.signal.reason); }; } }; // src/interface.ts var Interface = class extends HierarchicalAbortController { /** returns clone listening to additional AbortSignals */ _withSignals(...signals) { const ret = Object.create(this); Object.defineProperty(ret, "signal", { value: new HierarchicalAbortController(this.signal, ...signals).signal }); return ret; } /** returns clone that will time out after the spelistening to an additional timeout abortSignal */ _withTimeout(msecs = 1e3) { return this._withSignals(AbortSignal.timeout(msecs)); } /** * Find out if a device can be seen * @virtual */ hasAccess() { return __async(this, null, function* () { return false; }); } }; __decorateClass([ use(EventEmitter, HierarchicalAbortController) ], Interface.prototype, "this", 2); // src/tool.ts import { normalize } from "path"; var ToolError = class extends Error { get message() { var _a; if (this.killed) { return "aborted"; } else { return ((_a = this.cause) == null ? void 0 : _a.message) || (removeFalsy(this.cause) ? JSON.stringify( removeFalsy({ error: this.cause, stdout: this.stdout, stderr: this.stderr }) ) : this.name); } } get name() { return this.constructor.name; } get cmd() { var _a; return (_a = this.cause) == null ? void 0 : _a.cmd; } get killed() { var _a, _b, _c, _d, _e; return ((_a = this.cause) == null ? void 0 : _a.killed) || ((_c = (_b = this.cause) == null ? void 0 : _b.message) == null ? void 0 : _c.includes("aborted")) || ((_d = this.stderr) == null ? void 0 : _d.includes("Killed")) || ((_e = this.stderr) == null ? void 0 : _e.includes("killed by remote request")) === true; } constructor(error, stdout, stderr) { super(void 0, { cause: error }); this.stdout = stdout; this.stderr = stderr; } }; var _Tool_instances, initializeArgs_fn; var Tool = class extends Interface { constructor(_a) { var _b = _a, { tool, Error: Error2 = ToolError, signals = [], extraArgs = [], extraEnv = {}, setPath = false, config = {}, argsModel = {} } = _b, options = __objRest(_b, [ "tool", "Error", "signals", "extraArgs", "extraEnv", "setPath", "config", "argsModel" ]); super(); __privateAdd(this, _Tool_instances); this.tool = tool; this.executable = normalize(getAndroidToolPath(this.tool)); this.Error = Error2; this.listen(...signals); this.extraArgs = extraArgs; this.extraEnv = extraEnv; if (setPath) this.env.PATH = `${getAndroidToolBaseDir()}:${this.env.PATH}`; __privateMethod(this, _Tool_instances, initializeArgs_fn).call(this, config, argsModel); this.applyConfig(options); } /** environment variables */ get env() { return __spreadValues(__spreadValues({}, process.env), this.extraEnv); } /** cli arguments */ get args() { return [ ...this.extraArgs, ...Object.entries(this.argsModel).map( ([key, [flag, defaultValue, noArgs, overrideKey]]) => this.config[key] !== defaultValue ? noArgs ? [flag] : [flag, this.config[overrideKey || key]] : [] ) ].flat(); } /** return a clone with a specified variation in the config options */ _withConfig(config) { const ret = Object.create(this); ret.config = __spreadValues({}, this.config); for (const key in config) { if (Object.hasOwnProperty.call(config, key)) { ret.config[key] = config[key]; } } return ret; } /** returns clone with variation in env vars */ _withEnv(env) { const ret = Object.create(this); ret.extraEnv = __spreadValues({}, this.extraEnv); for (const key in env) { if (Object.hasOwnProperty.call(env, key)) { ret.extraEnv[key] = env[key]; } } return ret; } /** apply config options to the tool instance */ applyConfig(config) { for (const key in this.config) { if (Object.getOwnPropertyDescriptor( this.config, key ).writable && Object.hasOwn(config, key)) { this.config[key] = config[key]; } } } /** filter nullish and empty-string arguments */ constructArgs(args) { return [...this.args, ...args].filter((arg) => ![null, void 0, ""].includes(arg)).flat(); } /** Execute a command. Used for quick operations that do not require real-time data access. Output is trimmed. */ exec(...args) { return __async(this, null, function* () { this.signal.throwIfAborted(); const allArgs = this.constructArgs(args); const cmd = [this.tool, ...allArgs]; return exec(this.executable, allArgs, { encoding: "utf8", signal: this.signal, env: this.env, shell: true }).then(({ stdout, stderr }) => { this.emit("exec", removeFalsy({ cmd, stdout, stderr })); return stdout.trim() || stderr.trim(); }).catch(({ message, code, signal, killed, stdout, stderr }) => { const error = this.error( { message, code, signal, killed }, stdout, stderr ); this.emit("exec", removeFalsy({ cmd, error, stdout, stderr })); throw error; }); }); } /** Spawn a child process. Used for long-running operations that require real-time data access. */ spawn(...args) { this.signal.throwIfAborted(); const allArgs = this.constructArgs(args); const cmd = [this.tool, ...allArgs]; this.emit("spawn:start", removeFalsy({ cmd })); const cp = spawn(this.executable, allArgs, { env: this.env, signal: this.signal, shell: true }); cp.on( "exit", (code, signal) => this.emit("spawn:exit", removeFalsy({ cmd, code, signal })) ); cp.on( "error", (error) => this.emit("spawn:error", removeFalsy({ cmd, error })) ); return cp; } /** Parse and simplify errors */ error(error, stdout, stderr) { var _a, _b; error.message && (error.message = (_b = (_a = error.message) == null ? void 0 : _a.replaceAll(this.executable, this.tool)) == null ? void 0 : _b.trim()); return new this.Error( error, stdout == null ? void 0 : stdout.replaceAll(this.executable, this.tool), stderr == null ? void 0 : stderr.replaceAll(this.executable, this.tool) ); } /** Wait for a device */ wait() { return __async(this, null, function* () { return new Promise((resolve) => setTimeout(resolve, 2e3)).then(() => this.hasAccess()).then((access) => { if (!access) { this.signal.throwIfAborted(); return this.wait(); } }); }); } }; _Tool_instances = new WeakSet(); /** * initialize helper functions to set every config option specified in the args model. * ``` * class MyTool extends Tool { * constructor() { * super({ * config: { a: "b" }, * argsModel: { a: ["-a", "b"] } * }); * } * } * const tool = new MyTool({a: "a"}); * tool.exec("arg", "--other-flag"); // tool will be called as "tool -a a arg --other-flag" * tool.__a("b")exec("arg", "--other-flag"); // tool will be called as "tool arg --other-flag", because defaults are omitted * tool.__a("c").exec("arg", "--other-flag"); // tool will be called as "tool -a c arg --other-flag" * tool.exec("arg", "--other-flag"); // tool will be called as "tool -a a arg --other-flag", because the original instance is not changed * ``` */ initializeArgs_fn = function(config, argsModel) { this.config = config; this.argsModel = argsModel; for (const key in this.argsModel) { if (Object.hasOwn(this.argsModel, key)) { const [_arg, defaultValue, isFlag] = this.argsModel[key]; this[`__${key}`] = function(val) { return this._withConfig({ [key]: isFlag ? !defaultValue : val }); }; } } }; // src/adb.ts var SERIALNO = /^([0-9]|[a-z])+([0-9a-z]+)$/i; var DEFAULT_PORT = 5037; var DEFAULT_HOST = "localhost"; var DEFAULT_PROTOCOL = "tcp"; var AdbError = class extends ToolError { get message() { var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n; if (((_a = this.stderr) == null ? void 0 : _a.includes("error: device unauthorized")) || ((_b = this.stderr) == null ? void 0 : _b.includes("error: device still authorizing"))) { return "unauthorized"; } else if (((_c = this.stderr) == null ? void 0 : _c.includes("error: device offline")) || ((_d = this.stderr) == null ? void 0 : _d.includes("error: protocol fault")) || ((_e = this.stderr) == null ? void 0 : _e.includes("connection reset"))) { return "device offline"; } else if (((_f = this.stderr) == null ? void 0 : _f.includes("no devices/emulators found")) || ((_g = this.stdout) == null ? void 0 : _g.includes("no devices/emulators found")) || /device '.*' not found/.test(this.stderr || "") || ((_h = this.stdout) == null ? void 0 : _h.includes("adb: error: failed to read copy response")) || ((_i = this.stdout) == null ? void 0 : _i.includes("couldn't read from device")) || ((_j = this.stdout) == null ? void 0 : _j.includes("remote Bad file number")) || ((_k = this.stdout) == null ? void 0 : _k.includes("remote Broken pipe")) || ((_l = this.stderr) == null ? void 0 : _l.includes("adb: sideload connection failed: closed")) || ((_m = this.stderr) == null ? void 0 : _m.includes( "adb: pre-KitKat sideload connection failed: closed" ))) { return "no device"; } else if ((_n = this.stderr) == null ? void 0 : _n.includes("more than one device/emulator")) { return "more than one device"; } else { return super.message; } } }; var Adb = class extends Tool { constructor(options = {}) { super(__spreadValues({ tool: "adb", Error: AdbError, argsModel: { allInterfaces: ["-a", false, true], useUsb: ["-d", false, true], useTcpIp: ["-e", false, true], serialno: ["-s", null], transportId: ["-t", null], host: ["-H", DEFAULT_HOST], port: ["-P", DEFAULT_PORT], protocol: ["-L", DEFAULT_PROTOCOL, false, "socket"], exitOnWriteError: ["--exit-on-write-error", false, true] }, config: { allInterfaces: false, useUsb: false, useTcpIp: false, serialno: null, transportId: null, host: DEFAULT_HOST, port: DEFAULT_PORT, protocol: DEFAULT_PROTOCOL, get socket() { return `${this.protocol}:${this.host}:${this.port}`; }, exitOnWriteError: false } }, options)); } /** Kill all adb servers and start a new one to rule them all */ startServer() { return __async(this, arguments, function* (options = {}, serialOrUsbId) { this.applyConfig(options); yield this.killServer().then( () => this.exec( "start-server", ...serialOrUsbId ? ["--one-device", serialOrUsbId] : [] ) ); }); } /** Kill all running servers */ killServer() { return __async(this, null, function* () { yield this.exec("kill-server"); }); } /** Specifically connect to a device (tcp) */ connect(address) { return __async(this, null, function* () { const stdout = yield this.exec("connect", address); if (stdout.includes("no devices/emulators found") || stdout.includes("Name or service not known")) { throw this.error(new Error("no device"), stdout); } return this.wait(); }); } /** kick connection from host side to force reconnect */ reconnect(modifier) { return __async(this, null, function* () { const stdout = yield this.exec("reconnect", modifier); if (stdout.includes("no devices/emulators found") || stdout.includes("No route to host")) { throw this.error(new Error("no device"), stdout); } return this.wait(); }); } /** kick connection from device side to force reconnect */ reconnectDevice() { return __async(this, null, function* () { return this.reconnect("device"); }); } /** reset offline/unauthorized devices to force reconnect */ reconnectOffline() { return __async(this, null, function* () { return this.reconnect("offline"); }); } /** list devices */ devices() { return __async(this, null, function* () { return this.exec("devices", "-l").then((r) => r.replace("List of devices attached", "").trim()).then((r) => r.split("\n").map((device) => device.trim().split(/\s+/))).then( (devices) => devices.filter(([serialno]) => serialno).map( ([serialno, mode, ...props]) => Object( props.map((p) => p.split(":")).reduce((acc, [p, v]) => __spreadProps(__spreadValues({}, acc), { [p]: v }), { serialno, mode }) ) ) ); }); } /** Get the devices serial number */ getSerialno() { return __async(this, null, function* () { const serialno = yield this.exec("get-serialno"); if (serialno.includes("unknown") || !SERIALNO.test(serialno)) { throw this.error( new Error(`invalid serial number: ${serialno}`), serialno ); } return serialno; }); } /** run remote shell command and resolve stdout */ shell(...args) { return __async(this, null, function* () { return this.exec("shell", args.join(" ")); }); } /** determine child_process.spawn() result */ onCpExit(code, signal, stdout, stderr) { return __async(this, null, function* () { if (stderr == null ? void 0 : stderr.includes("adb: failed to read command: Success")) { return; } if (code || signal) { if (stdout.includes("adb: error: cannot stat") && stdout.includes("No such file or directory")) { throw this.error(new Error("file not found")); } else { throw this.error({ code, signal }, stdout, stderr); } } }); } /** extract chunk size from logging */ parseChunkSize(str, namespace = "writex") { return str.includes(namespace) ? parseInt(str.split("len=")[1].split(" ")[0]) || 0 : 0; } /** calculate progress from current/total */ normalizeProgress(current, total) { return Math.min(Math.round(current / total * 1e5) / 1e5, 1); } spawnFileTransfer(_0) { return __async(this, arguments, function* (command, files = [], args = [], progress) { progress(0); if (!files.length) { progress(1); return; } else { const _this = this; return new Promise((resolve, reject) => { const totalSize = Promise.all( files.map((file) => stat(file).then(({ size }) => size)) ).then((sizes) => sizes.reduce((a, b) => a + b)).catch((error) => { reject(this.error(error)); return 0; }); let pushedSize = 0; let stdout = ""; let stderr = ""; const cp = _this._withEnv({ ADB_TRACE: "rwx" }).spawn(command, ...files, ...args).once( "exit", (code, signal) => resolve(_this.onCpExit(code, signal, stdout, stderr)) ); cp.stdout.on("data", (d) => stdout += d.toString()); cp.stderr.on("data", (d) => { d.toString().split("\n").forEach((str) => __async(this, null, function* () { if (!str.includes("cpp")) { stderr += str; } else { pushedSize += _this.parseChunkSize(str); progress( _this.normalizeProgress( pushedSize, yield totalSize ) ); } })); }); }); } }); } /** copy local files/directories to device */ push() { return __async(this, arguments, function* (files = [], dest, progress = () => { }) { return this.spawnFileTransfer("push", files, [dest], progress); }); } /** sideload an ota package */ sideload(file, progress = () => { }) { return __async(this, null, function* () { return this.spawnFileTransfer("sideload", [file], [], progress); }); } /** * Reboot to a state * reboot the device; defaults to booting system image but * supports bootloader and recovery too. sideload reboots * into recovery and automatically starts sideload mode, * sideload-auto-reboot is the same but reboots after sideloading. */ reboot(state) { return __async(this, null, function* () { const stdout = yield this.exec("reboot", state); if (stdout.includes("failed")) { throw this.error(new Error(`reboot failed`), stdout); } }); } /** Return the status of the device */ getState() { return __async(this, null, function* () { return this.exec("get-state").then( (stdout) => stdout.trim() ); }); } ////////////////////////////////////////////////////////////////////////////// // Convenience functions ////////////////////////////////////////////////////////////////////////////// /** Reboot to a requested state, if not already in it */ ensureState(state) { return __async(this, null, function* () { return this.getState().then( (currentState) => currentState === state ? state : this.reboot(state).then(() => this.wait()) ); }); } /** read property from getprop or, failing that, the default.prop file */ getprop(prop) { return __async(this, null, function* () { const stdout = yield this.shell("getprop", prop); if (!stdout || stdout.includes("not found")) { return this.shell("cat", "default.prop").then((stdout2) => { if (stdout2 && stdout2.includes(`${prop}=`)) { return stdout2.split(`${prop}=`)[1].split("\n")[0].trim(); } else { throw this.error(new Error("unknown getprop error"), stdout2); } }); } else { return stdout; } }); } /** get device codename from getprop or by reading the default.prop file */ getDeviceName() { return __async(this, null, function* () { return this.getprop("ro.product.device"); }); } /** resolves true if recovery is system-image capable, false otherwise */ getSystemImageCapability() { return __async(this, null, function* () { return this.getprop("ro.ubuntu.recovery").then((r) => Boolean(r)).catch((e) => { if (e.message === "unknown getprop error") { return false; } else { throw e; } }); }); } /** Find out what operating system the device is running (currently android and ubuntu touch) */ getOs() { return __async(this, null, function* () { return this.shell("cat", "/etc/system-image/channel.ini").then((stdout) => { return stdout ? "ubuntutouch" : "android"; }); }); } /** Find out if a device can be seen by adb */ hasAccess() { return __async(this, null, function* () { return this.shell("echo", ".").then((stdout) => { if (stdout == ".") return true; else throw this.error(new Error("unexpected response: " + stdout), stdout); }).catch((error) => { if (error.message && error.message.includes("no device")) { return false; } else { throw error; } }); }); } /** wait for a device, optionally limiting to specific states or transport types */ wait(state = "any", transport = "any") { return __async(this, null, function* () { return this.exec(`wait-for-${transport}-${state}`).then( () => this.getState() ); }); } /** Format partition */ format(partition) { return __async(this, null, function* () { return this.shell("cat", "/etc/recovery.fstab").then((fstab) => { const block = this.findPartitionInFstab(partition, fstab); return this.shell("umount", `/${partition}`).then(() => this.shell("make_ext4fs", block)).then(() => this.shell("mount", `/${partition}`)).then((error) => { if (error) throw this.error(new Error("failed to mount: " + error), error); else return; }); }); }); } /** Format cache if possible and rm -rf its contents */ wipeCache() { return __async(this, null, function* () { yield this.format("cache").catch(() => { }); yield this.shell("rm", "-rf", "/cache/*"); return; }); } /** Find the partition associated with a mountpoint in an fstab */ findPartitionInFstab(partition, fstab) { try { return fstab.split("\n").filter((block) => block.startsWith("/dev")).filter( (block) => block.split(" ").filter((c) => c !== "")[1] === "/" + partition )[0].split(" ")[0]; } catch (error) { throw this.error(error); } } /** Find a partition and verify its type */ verifyPartitionType(partition, type) { return __async(this, null, function* () { return this.shell("mount").then((stdout) => { if (!(stdout.includes(" on /") && stdout.includes(" type ")) || typeof stdout !== "string" || !stdout.includes("/" + partition)) { throw this.error(new Error("partition not found"), stdout); } else { return stdout.includes(" on /" + partition + " type " + type); } }); }); } /** size of a file or directory */ getFileSize(file) { return __async(this, null, function* () { const size = yield this.shell("du -shk " + file); if (isNaN(parseFloat(size))) throw this.error(new Error(`Cannot parse size from ${size}`), size); return parseFloat(size); }); } /** available size of a partition */ getAvailablePartitionSize(partition) { return __async(this, null, function* () { const size = yield this.shell("df -k -P " + partition).then((stdout) => stdout.split(/[ ,]+/)).then((arr) => parseInt(arr[arr.length - 3])); if (isNaN(size)) throw this.error(new Error(`Cannot parse size from ${size}`)); return size; }); } /** total size of a partition */ getTotalPartitionSize(partition) { return __async(this, null, function* () { const size = yield this.shell("df -k -P " + partition).then((stdout) => stdout.split(/[ ,]+/)).then((arr) => parseInt(arr[arr.length - 5])); if (isNaN(size)) throw this.error(new Error(`Cannot parse size from ${size}`)); return size; }); } }; // src/fastboot.ts var FastbootError = class extends ToolError { get message() { var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u; if ((_a = this.stderr) == null ? void 0 : _a.includes( "FAILED (remote: low power, need battery charging.)" )) { return "low battery"; } else if (((_b = this.stderr) == null ? void 0 : _b.includes("not supported in locked device")) || ((_c = this.stderr) == null ? void 0 : _c.includes("Bootloader is locked")) || ((_d = this.stderr) == null ? void 0 : _d.includes("not allowed in locked state")) || ((_e = this.stderr) == null ? void 0 : _e.includes("not allowed in Lock State")) || ((_f = this.stderr) == null ? void 0 : _f.includes("Device not unlocked cannot flash or erase")) || ((_g = this.stderr) == null ? void 0 : _g.includes("Partition flashing is not allowed")) || ((_h = this.stderr) == null ? void 0 : _h.includes("Command not allowed")) || ((_i = this.stderr) == null ? void 0 : _i.includes("not allowed when locked")) || ((_j = this.stderr) == null ? void 0 : _j.includes("device is locked. Cannot flash images")) || ((_k = this.stderr) == null ? void 0 : _k.match(/download for partition '[a-z]+' is not allowed/i))) { return "bootloader locked"; } else if (((_l = this.stderr) == null ? void 0 : _l.includes("Check 'Allow OEM Unlock' in Developer Options")) || ((_m = this.stderr) == null ? void 0 : _m.includes("Unlock operation is not allowed")) || ((_n = this.stderr) == null ? void 0 : _n.includes("oem unlock is not allowed"))) { return "enable unlocking"; } else if ((_o = this.stderr) == null ? void 0 : _o.includes("FAILED (remote failure)")) { return "failed to boot"; } else if (((_p = this.stderr) == null ? void 0 : _p.includes("I/O error")) || ((_q = this.stderr) == null ? void 0 : _q.includes("FAILED (command write failed (No such device))")) || ((_r = this.stderr) == null ? void 0 : _r.includes("FAILED (command write failed (Success))")) || ((_s = this.stderr) == null ? void 0 : _s.includes("FAILED (status read failed (No such device))")) || ((_t = this.stderr) == null ? void 0 : _t.includes("FAILED (data transfer failure (Broken pipe))")) || ((_u = this.stderr) == null ? void 0 : _u.includes("FAILED (data transfer failure (Protocol error))"))) { return "no device"; } else { return super.message; } } }; var Fastboot = class _Fastboot extends Tool { constructor(options = {}) { super(__spreadValues({ tool: "fastboot", Error: FastbootError, argsModel: { wipe: ["-w", false, true], device: ["-s", null], maxSize: ["-S", null], force: ["--force", false, true], slot: ["--slot", null], skipSecondary: ["--skip-secondary", false, true], skipReboot: ["--skip-reboot", false, true], disableVerity: ["--disable-verity", false, true], disableVerification: ["--disable-verification", false, true], fsOptions: ["--fs-options", null], unbuffered: ["--unbuffered", false, true] }, config: { wipe: false, device: null, maxSize: null, force: false, slot: null, skipSecondary: false, skipReboot: false, disableVerity: false, disableVerification: false, fsOptions: null, unbuffered: false } }, options)); } /** Write a file to a flash partition */ flash(images, progress = () => { }) { return __async(this, null, function* () { progress(0); const _this = this; yield images.reduce( (prev, { raw, partition, flags, file }, i) => prev.then( () => new Promise((resolve, reject) => { let stdout = ""; let stderr = ""; let offset = i / images.length; let scale = 1 / images.length; let sparseCurr = 1; let sparseTotal = 1; let sparseOffset = () => (sparseCurr - 1) / sparseTotal; let sparseScale = () => 1 / sparseTotal; const cp = _this.spawn( raw ? "flash:raw" : "flash", partition, ...flags || [], file ); cp.once("exit", (code, signal) => { if (code || signal) { reject( _this.error({ code, signal }, stdout, stderr) ); } else { resolve("bootloader"); } }); cp.stdout.on("data", (d) => stdout += d.toString()); cp.stderr.on("data", (d) => { d.toString().trim().split("\n").forEach((str) => { try { if (!str.includes("OKAY")) { if (str.includes(`Sending '${partition}'`)) { progress(offset + 0.3 * scale); } else if (str.includes(`Sending sparse '${partition}'`)) { [sparseCurr, sparseTotal] = str.split(/' |\/| \(/).slice(1, 3).map(parseFloat); progress( offset + sparseOffset() * scale + sparseScale() * 0.33 * scale ); } else if (str.includes(`Writing '${partition}'`)) { progress( offset + sparseOffset() * scale + sparseScale() * 0.85 * scale ); } else if (str.includes(`Finished '${partition}'`)) { progress(offset + scale); } else { throw this.error( new Error(`failed to parse: ${str}`), void 0, d.toString().trim() ); } } } catch (e) { stderr += str; } }); }); }) ), _this.wait() ); }); } /** Download and boot kernel */ boot(image) { return __async(this, null, function* () { yield this.exec("boot", image); }); } /** Reflash device from update.zip and set the flashed slot as active */ update(image, wipe = false) { return __async(this, null, function* () { yield this._withConfig({ wipe }).exec("update", image); }); } /** Reboot device into bootloader */ rebootBootloader() { return __async(this, null, function* () { yield this.exec("reboot-bootloader"); }); } /** * Reboot device into userspace fastboot (fastbootd) mode * Note: this only works on devices that support dynamic partitions. */ rebootFastboot() { return __async(this, null, function* () { yield this.exec("reboot-fastboot"); }); } /** Reboot device into recovery */ rebootRecovery() { return __async(this, null, function* () { yield this.exec("reboot-recovery"); }); } /** Reboot device */ reboot() { return __async(this, null, function* () { yield this.exec("reboot"); }); } /** Continue with autoboot */ continue() { return __async(this, null, function* () { yield this.exec("continue"); }); } /** Format a flash partition. Can override the fs type and/or size the bootloader reports */ format(partition, type, size) { return __async(this, null, function* () { if (!type && size) { throw this.error({ message: "size specification requires type to be specified as well" }); } yield this.exec( `format${type ? ":" + type : ""}${size ? ":" + size : ""}`, partition ); }); } /** Erase a flash partition */ erase(partition) { return __async(this, null, function* () { yield this.exec("erase", partition); }); } /** Sets the active slot */ setActive(slot) { return this.exec(`--set-active=${slot}`).then((stdout) => { if (stdout && stdout.includes("error")) { throw this.error(new Error("failed to set active slot"), stdout); } else { return; } }); } /** Create a logical partition with the given name and size, in the super partition */ createLogicalPartition(partition, size) { return this.exec("create-logical-partition", partition, size).then( () => { } ); } /** Resize a logical partition with the given name and final size, in the super partition */ resizeLogicalPartition(partition, size) { return __async(this, null, function* () { yield this.exec("resize-logical-partition", partition, size); }); } /** Delete a logical partition with the given name */ deleteLogicalPartition(partition) { return __async(this, null, function* () { yield this.exec("delete-logical-partition", partition); }); } /** Wipe the super partition and reset the partition layout */ wipeSuper(image) { return __async(this, null, function* () { yield this.exec("wipe-super", image); }); } ////////////////////////////////////////////////////////////////////////////// // Convenience functions ////////////////////////////////////////////////////////////////////////////// /** Lift OEM lock */ oemUnlock(code) { return __async(this, null, function* () { try { yield this.exec("oem", "unlock", code); } catch (error) { if (!(error instanceof Error && error.message.match(/Already Unlocked|Not necessary/))) throw error; } }); } /** Enforce OEM lock */ oemLock() { return __async(this, null, function* () { yield this.exec("oem", "lock"); }); } /** unlock partitions for flashing */ flashingUnlock() { return __async(this, null, function* () { yield this.exec("flashing", "unlock"); }); } /** lock partitions for flashing */ flashingLock() { return __async(this, null, function* () { yield this.exec("flashing", "lock"); }); } /** unlock 'critical' bootloader partitions */ flashingUnlockCritical() { return __async(this, null, function* () { yield this.exec("flashing", "unlock_critical"); }); } /** lock 'critical' bootloader partitions */ flashingLockCritical() { return __async(this, null, function* () { yield this.exec("flashing", "lock_critical"); }); } /** Find out if a device can be flashing-unlocked */ getUnlockAbility() { return __async(this, null, function* () { return this.exec("flashing", "get_unlock_ability").then((stdout) => stdout === "1").catch(() => false); }); } /** Find out if a device can be seen by fastboot */ hasAccess() { return __async(this, null, function* () { return (yield this.exec("devices")).includes("fastboot"); }); } /** wait for a device */ wait() { return __async(this, null, function* () { yield __superGet(_Fastboot.prototype, this, "wait").call(this); return "bootloader"; }); } /** get bootloader var */ getvar(variable) { return __async(this, null, function* () { const result = yield this.exec("getvar", variable); const resultParts = result.replace(/\r\n/g, "\n").split("\n").map((element) => element.trim()); const resultPart = resultParts.find( (element) => element.startsWith(variable) ); const [name, value] = resultPart ? resultPart.split(": ") : resultParts && resultParts.length ? ( // for backwards compatibility return the first line as name, if it exists [resultParts[0], ""] ) : ( // otherwise just return empty name and value ["", ""] ); if (name !== variable) { throw this.error( new Error(`Unexpected getvar return: "${name}"`), result ); } return value; }); } /** get device codename from product bootloader var */ getDeviceName() { return this.getvar("product"); } }; // src/heimdall.ts var HeimdallError = class extends ToolError { get message() { var _a; if ((_a = this.stderr) == null ? void 0 : _a.includes("Failed to detect")) { return "no device"; } else { return super.message; } } }; var Heimdall = class extends Tool { constructor(options = {}) { super(__spreadValues({ tool: "heimdall", Error: HeimdallError }, options)); } /** Find out if a device in download mode can be seen by heimdall */ detect() { return this.hasAccess(); } /** Find out if a device in download mode can be seen by heimdall */ hasAccess() { return this.exec("detect").then(() => true).catch((error) => { if (error.message.includes("no device")) { return false; } else { throw error; } }); } /** Wait for a device */ wait() { return super.wait().then(() => "download"); } /** Prints the contents of a PIT file in a human readable format. If a filename is not provided then Heimdall retrieves the PIT file from the connected device. */ printPit(file) { return this.exec("print-pit", ...file ? ["--file", file] : []).then( (r) => r.split("\n\nEnding session...")[0].split(/--- Entry #\d ---/).slice(1).map((r2) => r2.trim()) ); } /** get partitions from pit file */ getPartitions() { return this.printPit().then( (r) => r.map( (r2) => r2.split("\n").map((r3) => r3.split(":").map((r4) => r4.trim())).reduce( (result, item) => { result[item[0]] = item[1]; return result; }, {} ) ) ); } /** Flash firmware files to partitions (names or identifiers) */ flash(images) { return __async(this, null, function* () { yield this.exec( "flash", ...images.map((i) => [`--${i.partition}`, i.file]).flat() ); }); } }; // src/module.ts var DeviceTools = class extends Interface { constructor({ adbOptions = {}, fastbootOptions = {}, heimdallOptions = {}, signals = [] }) { super(...signals); signals = [this.signal]; this.adb = new Adb(__spreadProps(__spreadValues({}, adbOptions), { signals })); this.fastboot = new Fastboot(__spreadProps(__spreadValues({}, fastbootOptions), { signals })); this.heimdall = new Heimdall(__spreadProps(__spreadValues({}, heimdallOptions), { signals })); ["adb", "fastboot", "heimdall"].forEach((tool) => { this[tool].on("exec", (r) => this.emit("exec", r)); this[tool].on("spawn:start", (r) => this.emit("spawn:start", r)); this[tool].on("spawn:exit", (r) => this.emit("spawn:exit", r)); this[tool].on("spawn:error", (r) => this.emit("spawn:error", r)); }); } /** returns clone with variation in env vars */ _withEnv(env) { const ret = Object.create(this); ret.adb = this.adb._withEnv(env); ret.fastboot = this.fastboot._withEnv(env); ret.heimdall = this.heimdall._withEnv(env); return ret; } /** Wait for a device */ wait() { const controller = new AbortController(); const _this = this._withSignals(controller.signal); return Promise.race([ _this.adb.wait(), _this.fastboot.wait(), _this.heimdall.wait() ]).finally(() => controller.abort()); } /** Resolve device name */ getDeviceName() { return this.adb.getDeviceName().catch(() => this.fastboot.getDeviceName()).catch(() => { throw new Error("no device"); }); } }; export { Adb, AdbError, DeviceTools, Fastboot, FastbootError, Heimdall, HeimdallError, HierarchicalAbortController, Interface, Tool, ToolError };