promise-android-tools
Version:
A wrapper for adb, fastboot, and heimdall that returns convenient promises.
1,281 lines (1,267 loc) • 45.5 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropNames = Object.getOwnPropertyNames;
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 __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
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/module.ts
var module_exports = {};
__export(module_exports, {
Adb: () => Adb,
AdbError: () => AdbError,
DeviceTools: () => DeviceTools,
Fastboot: () => Fastboot,
FastbootError: () => FastbootError,
Heimdall: () => Heimdall,
HeimdallError: () => HeimdallError,
HierarchicalAbortController: () => HierarchicalAbortController,
Interface: () => Interface,
Tool: () => Tool,
ToolError: () => ToolError
});
module.exports = __toCommonJS(module_exports);
// src/adb.ts
var import_promises = require("fs/promises");
// src/exec.ts
var import_node_child_process = require("child_process");
var import_node_util = require("util");
var import_node_child_process2 = require("child_process");
var exec = (0, import_node_util.promisify)(import_node_child_process.execFile);
// src/android_tools.ts
var import_path = require("path");
var import_fs = require("fs");
function getAndroidToolBaseDir(platform = process.platform, arch = process.arch) {
const baseDir = (0, import_path.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 = (0, import_path.join)(
getAndroidToolBaseDir(platform, arch),
platform === "win32" ? `${tool}.exe` : tool
);
if ((0, import_fs.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
var import_node_events = require("events");
var import_typescript_mix = require("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([
(0, import_typescript_mix.use)(import_node_events.EventEmitter, HierarchicalAbortController)
], Interface.prototype, "this", 2);
// src/tool.ts
var import_node_path = require("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 = (0, import_node_path.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 = (0, import_node_child_process2.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) => (0, import_promises.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");
});
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Adb,
AdbError,
DeviceTools,
Fastboot,
FastbootError,
Heimdall,
HeimdallError,
HierarchicalAbortController,
Interface,
Tool,
ToolError
});