Coder Social home page Coder Social logo

promise-android-tools's Introduction

promise-android-tools Continuous Integration npm codecov

A wrapper for Adb, Fastboot, and Heimall written in modern asynchronous TypeScript that provides convenient promises for interacting with Android and Ubuntu Touch devices. The package was originally developed for the UBports Installer but has since been expanded to cover all APIs of the included tools.

Usage

Install the package by running npm i promise-android-tools android-tools-bin.

Quick-start example

The default settings should cover most usecases.

import { DeviceTools } from "promise-android-tools";
const dt = new DeviceTools();

dt.wait() // wait for any device
  .then(state =>
    dt
      .getDeviceName()
      .then(name => console.log(`detected ${name} in ${state} state`))
  );

Config, Env vars

Global command-line flags for tools are configurable on the <tool>.config property and described using an ArgsModel. The <tool>._withConfig() function allows overriding options temporarily. Additionally, <tool>.__<option>() helper functions are provided for all options.

import { Adb } from "promise-android-tools";
const adb = new Adb();
console.log(
  adb.config.serialno, // null
  adb._withConfig({ serialno: "1337" }).config.serialno, // "1337"
  adb.__serialno("1337").config.serialno // "1337"
);
adb.hasAccess(); // will run command `adb devices`
adb._withConfig({ serialno: "1337" }).hasAccess(); // will run command `adb -s 1337 devices`
adb.__serialno("1337").hasAccess(); // will run command `adb -s 1337 devices`

Aborting pending Promises

Pending operations can be aborted using the standardized AbortSignal/AbortController APIs. Every tool implements its own HierarchicalAbortController for this purpose.

import { Adb, HierarchicalAbortController } from "promise-android-tools";
const adb = new Adb();
adb.wait(); // will resolve once a device is detected or reject on error/abort
adb.abort(); // will abort ALL pending promises from the instance

Additional AbortSignals can be passed on the signals constructor parameter to listen to for aborting pending operations. The <tool>._withSignals() function returns a clone for the tool instance listening to additional abort signals.

import {
  Adb,
  Fastboot,
  HierarchicalAbortController
} from "promise-android-tools";
const controller = new HierarchicalAbortController();
const adb = new Adb({ signals: [controller.signal] });
const fastboot = new Fastboot({ signals: [controller.signal] });
Promise.all([adb.wait(), fastboot.wait()]);
controller.abort(); // will abort ALL pending promises from both instances

A clone that will time out after a specified amount of time can be created using the <tool>._withTimeout() function.

import { Adb, HierarchicalAbortController } from "promise-android-tools";
const adb = new Adb();
adb._withTimeout(1000).wait(); // will resolve if a device is detected or automatically reject after the timeout of one second
const controller = new HierarchicalAbortController();
adb._withSignals(controller.signal).wait(); // will be pending until aborted
controller.abort(); // will abort only this promise, not the instance overall

Log execution events

Events are available to log or introspect tool executions.

import { DeviceTools } from "promise-android-tools";
const dt = new DeviceTools();

dt.on("exec", r => console.log("exec", r));
dt.on("spawn:start", r => console.log("spawn:start", r));
dt.on("spawn:exit", r => console.log("spawn:exit", r));
dt.on("spawn:error", r => console.log("spawn:error", r));

dt.adb.shell("echo", "test");
// will log a compact object (i.e. no falsy values) consisting of the command array cmd, the error object, and the stderr and stdout buffers. The path to the executable will be replaced with the tool name for brevity:
// exec {
//   cmd: [ 'adb', '-P', 5037, 'shell', 'echo test' ],
//   error: {
//     message: 'Command failed: adb -P 5037 shell echo test\n' +
//       'adb: no devices/emulators found',
//     code: 1
//   },
//   stderr: 'adb: no devices/emulators found'
// }

Complex example

The library provides most features of the eponymous command-line utilities wrapped in the available classes. This example only serves as a demonstration - please consult the documenation to discover the full power of this library.

import { DeviceTools } from "promise-android-tools";
const dt = new DeviceTools();

db.adb
  .wait() // wait for any device over adb
  .then(() => dt.adb.ensureState("recovery")) // reboot to recovery if we have to
  .then(() => dt.adb.push(["./config.json"], "/tmp", progress)) // push a config file to the device
  .then(() => dt.adb.getDeviceName()) // read device codename
  .then(name => {
    // samsung devices do not use fastbooot
    if (name.includes("samsung")) {
      return dt.adb
        .reboot("bootloader") // reboot to samsung's download mode
        .then(() => dt.heimdall.wait()) // wait for device to respond to heimdall
        .then(() => dt.heimdall.flash("boot", "boot.img")) // flash an image to a partition
        .then(() => dt.heimdall.reboot()); // reboot to system
    } else {
      return dt.adb
        .reboot("bootloader") // reboot to bootloader (aka. fastboot mode)
        .then(() => dt.fastboot.wait()) // wait for device to respond to fastboot commands
        .then(() => dt.fastboot.flash("boot", "boot.img")) // flash an image
        .then(() => dt.fastboot.continue()); // auto-boot to system
    }
  })
  .then(() => dt.adb.wait("device")) // ignore devices in recovery or a different mode
  .then(() => console.log("flashing complete, that was easy!")); // yay

function progress(p) {
  console.log("operation", p * 100, "% complete");
}

Documentation

Typescript types are bundled and IntelliSense is supported. Run npm run docs to build html from JSdoc/Typedoc comments for all methods and types.

API Changes, Deprecation Notices, Upgrade Guide

Upgrading to 5.x

For version 5.0.0, the library has been migrated to typescript for increased stability and type safety. Many under-the-hood components have been redesigned for more efficient abstraction and ease of use.

  • CancelablePromise has been replaced by the the native AbortController interface. The <tool>.kill() method has been renamed to <tool>.abort().
  • Error handling has been restructured. Tools now throw AdbError, FastbootError, or HeimdallError objects implementing the ToolError interface and providing the original unaltered error in the cause property. Current standardized error messages include "aborted" | "no device" | "more than one device" | "unauthorized" | "device offline" | "bootloader locked" | "enable unlocking" | "low battery" | "failed to boot".
  • Dependencies have been re-evaluated and many external libraries have been replaced with more solid solutions from modern NodeJS built-ins.
  • Global command-line options for tools are now configurable on the <tool>.config property.
  • The execOptions parameter has been removed in favor of extraArgs and extraEnv properties and their respective helper functions.

Upgrading to 4.x

Version 4.0.0 includes a major re-factoring effort that touched almost every function. The APIs of most functions remained intact, but in most cases you will have to make changes to your code. This has been done to correct some early design decisions.

  • A new convenience class DeviceTools has been implemented that provides instances of all tool classes as well as some generic convenience functions such as deviceTools.wait() (wait for any device to be visible with any adb, fastboot, or heimdall) and deviceTools.getDeviceName() (read the device name from fastboot or adb). In most cases you will no longer need to instantiate any of the tool classes directly.
  • In order to properly follow the object-oriented paradigm, all tool wrapper classes now inherit from a new Tool class that implements the child_process wrappers along with some common interfaces. The implications of this are:
    • Our android-tools-bin package is now included as a dependency. If you require custom executables, you can use environment variables.
    • Specifying a custom exec function in the constructor arguments is no longer supported.
      • We no longer use child_process.exec to avoid spawining a shell. Confer with the official documentation to learn what this entails in detail. Most short-lived commands now use child_process.execFile. Long-running commands use
    • Specifying a custom log function in the constructor arguments is no longer supported. You can instead listen to the events exec, spawn:start, spawn:exit, and spawn:error on the tool object to implement your own logging or introspection logic.
    • The <tool>.<tool>Event event emitter has been deprecated. Instead, the tool class now inherits from the event emitter class directly.
  • <tool>.waitForDevice() and <tool>.stopWaiting() have been deprecated in favor of <tool>.wait().
    • On fastboot and heimdall, <tool>.wait() will poll using <tool>.hasAccess() at a fixed interval. It does not take arguments.
    • adb.wait() uses the adb wait-for-[-TRANSPORT]-STATE command instead. You can optionally specify the state or transport as arguments, eg adb.wait("recovery", "usb").
    • The <tool>.wait() function returns a CancelablePromise, which extends the native ES promise to support cancelling pending promises. Calling const p = adb.wait(); setTimeout(() => p.cancel(), 5000); will kill the waiting child-process and settle the pending promise.
  • adb.pushArray() has been deprecated and incorporated into the adb.push() API.
    • Since the adb push command supports pushing multiple files to the same location and this is the most common usecase, the adb.pushArray() function has been deprecated. The adb.push() function now takes an array of source file paths, a target destination path on the device, and a progress callback.
    • The progress is now reported on-the-fly and no longer requires polling via adb shell stat <file>. This results in faster and more accurate reporting.
  • Functions that are considered unstable or experimental have been makred as such in their documentation comments. If you're building a product around any of those, you're welcome to help us improve the library to ensure your needs will be accounted for in the future.

Upgrading to 3.x

  • Version 3.0.0 introduced a breaking API change in fastboot.flash() and fastboot.flashRaw(). Pervious the third and fourth arguments of fastboot.flash() were boolean arguments for indicating force and raw flashing. Similarly fastboot.flashRaw() is a convenience function that forwarded the third argument as a boolean flag for the force option. The new API of fastboot.flash() accepts a boolean value for raw flashing as the third argument, followed by any number of string arguments for additional flags. The fastboot.flashRaw() function similarly accepts any number of arguments for additional flags starting at the third argument. The fastboot.flashArray() function now takes an array like [ {partition, file, raw, flags}, ... ] as an argument. We believe that this change is more in line with the latest developments in the fastboot cli and provides better access to options like --force, --disable-verity, and --disable-verification.
  • NodeJS version 8 and below have been deprecated and are no longer supported. Versions 10, 12, and 14 are actively tested and supported.
  • New experimental backup and restore functions for use with Ubuntu Touch have been added to the ADB module. The API of these might change in the future.

Upgrading to 2.x

  • No breaking API changes were introduced in version 2.0.0.
  • A new Heimdall module provides access to Samsung devices.

License

Original development by Johannah Sprinz and Marius Gripsgård. Copyright (C) 2017-2022 UBports Foundation.

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

promise-android-tools's People

Contributors

amartinz avatar dependabot[bot] avatar flohack74 avatar gsilvapt avatar maciek134 avatar mariogrip avatar neothethird avatar strazzere avatar universalsuperbox avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

promise-android-tools's Issues

JSDoc/Typedoc housekeeping

If someone is ever looking for something ✨ fun ✨ to do: The docs comments could use someone going through them and simplifying/standardizing them in a way that improves their usefulness and accessibility.

Create a super-class

There are some things shared among all tools. Creating a super-class would allow us to get rid of some duplicate (testing-)code and improve maintainability.

Add support for more fastboot flash flags

  --disable-verity                         Set the disable-verity flag in the
                                           the vbmeta image being flashed.
  --disable-verification                   Set the disable-verification flag in                                           the vbmeta image being flashed.

Implement child_process.spawn for long-running tasks

For long-running tasks, child_process.exec() is not suitable, because it can lead to process termination when the stdout buffer is full. We also do not have access to the stdout as it is being generated, making it impossible to make use of things like adb push progress reporting and requiring dirty hacks.

Issue with `_withEnv` implementation within electron environment

This took quiet a while to track down and implement a work around - though I must admit, it feels like there should be another way to do this. Please take this report with a grain of salt as I may be missing the actual underlying issue as it only reproduces in some environments.

While attempting to do a simple adb operation like this following;

 const { Adb } = require("promise-android-tools")
 adb = new Adb({serialno:"12345" })
 await adb.push(['tsconfig.json'], '/data/local/tmp/derp')

The package would spit out a TypeError: Illegal Instruction on the line this.signal.throwIfAborted() inside Tool._withEnv.

This does not reproduce when running solely in a node environment, the this object appears fine. However in the electron environment where it fails, the this object appears to be a "less than deep" clone, or the this context appears to get mangled somehow - this part is a bit unclear to me.

I believe this is based on how Object.create doesn't actually run the constructor. Tested a few other methods for deep cloning the object (Object.assign({}, this), { ... this}, ...) - thought none of them seemed to solve them. Since the type is unpredictable (Adb, FastBoot, Heimdall) it seemed best to try to reflectively call this's own type constructor and just pass this to it. This resulted in the following patch;

diff --git a/src/tool.ts b/src/tool.ts
index c19b017..c1b678b 100644
--- a/src/tool.ts
+++ b/src/tool.ts
@@ -227,7 +227,8 @@ export abstract class Tool extends Interface {
 
   /** returns clone with variation in env vars */
   public _withEnv(env: NodeJS.ProcessEnv): this {
-    const ret = Object.create(this);
+    // @ts-ignore-next-line
+    const ret = new this.constructor(this);
     ret.extraEnv = { ...this.extraEnv };
     for (const key in env) {
       if (Object.hasOwnProperty.call(env, key)) {

This does work for my usecase, and seemingly all other spawn commands I tried, though I honestly hate it due to the ts-ignore.

Hoping to potentially learn something here if anyone knows a better way to fix this issue, or if maybe I'm completely misunderstanding this issue. Whatever the solution, it would likely need to be applied to a few other spots which utilize the Object.create(this) pattern.

If the solution I found above seems the best fitting, I can always draft a PR to fix this and other instance with a bit of a commented explanation.

Cheers!

Finish heimdall support

Someone at Samsung decided it was a good idea to not follow standards, and i hope to god there is a special place in hell for them where they suffer eternal damnation. We can already get our own suffering on by preparing to support Heimdall.

Batching through dataloader?

We might think about batching execs (across tools?) through dataloader to make sure the device isn't bombarded with too many things at once.

Unentagle error handling

It was a really stupid idea of mine to put error handling for the different tools into the common module. Split it to separate functions inside the respective modules.

Publish latest on npm / enable installing from repo

Thanks for all the great work on the conversion to typescript - this actually cleared up many things on my side.

Was wondering if we could get a push to the npm repository of the latest, at there seems to be a version/code mismatch.

I can submit a PR for the following changes as well;

strazzere@1939df6

Though essentially I added rollup back as a dev dependency, added it to the build command since the main entry point in the package.json has no was to be created. Then I added the prepare script which allows a user to do a direct npm install https://github.com/theuser/therepo.

Refactor adb.push() and adb.pushArray() functions

adb push already supports multiple files. These functions could be a lot leaner with a lot less code. It is of course imperative that we keep the option to report the progress at set intervals.

Re-design module.js to incorporate generic functions

To accomplish things like detecting any connected device (eg. ubports/ubports-installer#902), we could reshape module.js to be its own class with instances of all available tools as properties. That way we could create some convenience functions like a tool-agnositc waitForDevice() using something along the lines of Promise.race([adb.waitForDevice(),fastboot.waitForDevice(),heimdall.waitForDevice()]) and returning a string describing the connected mode. You could also go further and attempt os detection and device name detection.

ES6

Move to ES6 for the awesome of it all. CommonJS who?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.