Skip to content

Commit

Permalink
Release 3.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
topjohnwu committed Aug 3, 2020
1 parent c16575e commit fd5cbd5
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 96 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
## 3.0.0
New major release, introducing root service support!<br>
3.0.0 is fully source compatible with 2.6.0, but please migrate the deprecated methods as soon as possible as these shim will be removed soon.

### New Features
- New module `:service` is added: introduce `RootService` for remote root IPC
- `SuFileInputStream` now fully support `mark(int)`, `reset()`, and `skip(long)`
- `CallbackList` now support passing in a custom `Executor` in its constructor to configure which thread `onAddElement()` to run on

### Behavior Changes
- `CallbackList` no longer synchronizes its base `List` internally (if provided).
It is the developer's responsibility if synchronization is required

### API Changes
- `Shell.Builder` is now used to construct `Shell` objects. Each shell instance creation now has its own configurations
- `Shell.enableVerboseLogging` is now used to toggle verbose logging throughout the framework
- `Shell.setDefaultBuilder(Shell.Builder)` is now used to configure the global shell instance

### Deprecation
- `Shell.FLAG_VERBOSE_LOGGING`: use `Shell.enableVerboseLogging`
- `Shell.Config`: customize `Shell.Builder` and set it in `Shell.setDefaultBuilder(Shell.Builder)`
- `Shell.newInstance(...)`: create `Shell.Builder` and use `Shell.Builder.build(...)`

## 2.6.0
### API Changes
- New APIs to allow users to customize which thread to dispatch when returning results via callbacks
Expand Down
185 changes: 89 additions & 96 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,19 @@

[![](https://jitpack.io/v/topjohnwu/libsu.svg)](https://jitpack.io/#topjohnwu/libsu)

An Android library that provides APIs to a Unix (root) shell.
An Android library providing a complete solution for apps using root permissions.

Some poorly coded applications requests a new shell (call `su`, or worse `su -c <commands>`) for every single command, which is very inefficient. This library makes sharing a single, globally shared shell session in Android applications super easy: developers won't have to bother about concurrency issues, and with a rich selection of both synchronous and asynchronous APIs, it is much easier to create a powerful root app.

Optionally, `libsu` comes with a full suite of I/O classes, re-creating `java.io` classes but enhanced with root access. Without even thinking about command-lines, you can use `File`, `RandomAccessFile`, `FileInputStream`, and `FileOutputStream` equivalents on files that are only accessible with root permissions. The I/O stream classes are carefully optimized and have very promising performance.

Also optionally, this library bundles with prebuilt BusyBox binaries. App developers can easily setup and create an internal BusyBox environment without relying on potentially flawed (or even no) external BusyBox.

One complex Android application using `libsu` for all root related operations is [Magisk Manager](https://github.com/topjohnwu/Magisk/tree/master/app).
`libsu` comes with 2 main components: the `core` module provides a robust API to interact with a Unix shell; the `service` module allows you to create root services to run Java/Kotlin and native C/C++ code (via JNI). The library handles the creation of the shell process, I/O with standard streams, multithreading, concurrency issues, and management of remote root services for you. With synchronous/asynchronous APIs and the concept of a globally shared shell session, this library makes integrating root into application logic very easy.

## [Changelog](./CHANGELOG.md)

## [Javadoc](https://javadoc.jitpack.io/com/github/topjohnwu/libsu/docs/2.6.0/javadoc/overview-summary.html)
## [Javadoc](https://javadoc.jitpack.io/com/github/topjohnwu/libsu/docs/3.0.0/javadoc/overview-summary.html)

## Download
```groovy
android {
compileOptions {
// This library uses Java 8 features, this is required
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
Expand All @@ -28,134 +23,144 @@ repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
def libsuVersion = '2.6.0'
def libsuVersion = '3.0.0'
// The core module is used by all other components
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
// Optional: For using com.topjohnwu.superuser.io classes
// Optional: APIs for creating root services
implementation "com.github.topjohnwu.libsu:service:${libsuVersion}"
// Optional: For com.topjohnwu.superuser.io classes
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
// Optional: Bundle prebuilt BusyBox binaries
// Source code of the built binaries: https://github.com/topjohnwu/ndk-busybox
// Please note that BusyBox is GPLv2. Theoretically bundling the binary
// without code linkage does not enforce your app to also be GPLv2, so
// it shall be fine to use in closed source or projects using other licenses.
implementation "com.github.topjohnwu.libsu:busybox:${libsuVersion}"
}
```

## License
This project is licensed under the Apache License, Version 2.0. Please refer to `LICENSE` for the full text.

In the module `busybox`, a prebuilt BusyBox binary is included. BusyBox is licensed under GPLv2, please check its repository for full detail. The binaries included in the project are built with sources from [this repository](https://github.com/topjohnwu/ndk-busybox).

Theoretically, using a GPLv2 binary without linkage does not affect your app, so it should be fine to use it in closed source or other licensed projects as long as the source code of the binary itself is released (which I just provided), but **this is not legal advice**. Please consult legal experts if feeling concerned using the `busybox` module.

## Quick Tutorial
Please note that this is a rundown going through the key features of `libsu`. Please read the full Javadoc and check out the example app (`:example`) in this project for more details.

### Configurations
Set configurations in your MainActivity or Application class:
### Configuration
Set default configurations before the global `Shell` instance is created:

```java
// Somewhere such as Application or MainActivity
static {
// Shell.Config methods shall be called before any shell is created
// This is the why in this example we call it in a static block
// The followings are some examples, check Javadoc for more details
Shell.Config.setFlags(Shell.FLAG_REDIRECT_STDERR);
Shell.Config.verboseLogging(BuildConfig.DEBUG);
Shell.Config.setTimeout(10);
Shell.enableVerboseLogging = BuildConfig.DEBUG;
Shell.setDefaultBuilder(Shell.Builder.create()
.setFlags(Shell.FLAG_REDIRECT_STDERR)
.setTimeout(10));
}
```

### Shell Operations
`Shell` operations can be performed through static `Shell.sh/su(...)` methods that use the globally shared shell session, or you can directly get a `Shell` object and use the instance APIs. Here we demo the former as it covers most use cases:

```java
// Run commands and get output immediately
List<String> output = Shell.su("find /dev/block -iname boot").exec().getOut();

Shell.Result result;
// Execute commands synchronously
result = Shell.su("find /dev/block -iname boot").exec();
// Aside from commands, you can also load scripts from InputStream
Shell.Result result = Shell.su(getResources().openRawResource(R.raw.script)).exec();
result = Shell.su(getResources().openRawResource(R.raw.script)).exec();

List<String> out = result.getOut(); // stdout
int code = result.getCode(); // return code of the last command
boolean ok = result.isSuccess(); // return code == 0?

// You can get more stuffs from the results
int code = result.getCode();
boolean ok = result.isSuccess();
output = result.getOut();
// Async APIs
Shell.su("setenforce 0").submit(); // submit and don't care results
Shell.su("sleep 5", "echo hello").submit(result -> updateUI(result));

// Run commands and output to a specific List
// Run tasks and output to specific Lists
List<String> logs = new ArrayList<>();
Shell.su("cat /cache/magisk.log").to(logs).exec();
List<String> stdout = new ArrayList<>();
List<String> stderr = new ArrayList<>();
Shell.su("echo hello", "echo hello >&2").to(stdout, stderr).exec();

// Run commands in the background and don't care results
Shell.su("setenforce 0").submit();
// Receive output in real-time
List<String> callbackList = new CallbackList<String>() {
@Override
public void onAddElement(String s) { updateUI(s); }
};
Shell.su("for i in $(seq 5); do echo $i; sleep 1; done")
.to(callbackList)
.submit(result -> updateUI(result));
```

// Run commands in the background and get results via a callback
Shell.su("sleep 5", "echo hello").submit(result -> {
// This callback will be called on the main (UI) thread
// after the operation is done (5 seconds after submit)
result.getOut(); /* Should return a list with a single string "hello" */
})
### Initialization
Optionally, to do some pre-configuration, initialize shells with custom `Shell.Initializer`:

// Create a reactive callback List, and update the UI on each line of output
List<String> callbackList = new CallbackList<String>() {
@MainThread
```java
// Create something like bashrc
public class ExampleInitializer extends Shell.Initializer {
@Override
public void onAddElement(String s) {
// This callback will be called on the main (UI) thread each time
// the list adds a new element (in this case: shell outputs a new line)
uiUpdate(s); /* Some method to update the UI */
public boolean onInit(Context context, Shell shell) {
InputStream bashrc = context.getResources().openRawResource(R.raw.bashrc);
// Here we use Shell instance APIs instead of sh/su(...) static methods
shell.newJob()
.add(bashrc) /* Load a script */
.add("export ENV_VAR=VALUE") /* And some commands */
.exec();
return true; // Return false to indicate initialization failed
}
};
Shell.su(
"for i in 1 2 3 4 5;do",
" echo $i"
" sleep 1"
"done",
"echo 'countdown done!'").to(callbackList).submit(result -> {
// Some results cannot be acquired from callback lists
// e.g. return codes
uiUpdate(result.getCode());
});

// Also get STDERR
List<String> stdout = new ArrayList<>();
List<String> stderr = new ArrayList<>();
Shell.su("echo hello", "echo hello >&2").to(stdout, stderr).exec();
}
Shell.Builder builder = ... ;
builder.setInitializers(ExampleInitializer.class);
```

### I/O
Add `com.github.topjohnwu.libsu:io` as dependency to access the I/O wrapper classes:
Built on top of the `core` foundation is a suite of I/O classes, re-creating `java.io` classes for root access. Use `File`, `RandomAccessFile`, `FileInputStream`, and `FileOutputStream` equivalents on files that are only accessible with root permissions. These classes internally use Unix commands through the global root shell, and the I/O streams are carefully optimized to have very promising performance. Add `com.github.topjohnwu.libsu:io` as a dependency to access root I/O classes:

```java
// Treat files that require root access just like ordinary files
File logs = SuFile.open("/cache/magisk.log");
if (logs.exists()) {
try (InputStream in = new SuFileInputStream(logs);
OutputStream out = new SuFileOutputStream("/data/magisk.log.bak")) {
// All file data can be accessed with Java Streams
// Do I/O stuffs...
} catch (IOException e) {
e.printStackTrace();
}
}
```

### Advanced
Initialize shells with custom `Shell.Initializer`, similar to what `.bashrc` will do:
### Root Services (`minSdkVersion = 18`)
If interacting with a root shell and the I/O classes still do not serve your needs, you can also implement a root service to run complex code. A root service is similar to [Bound Services](https://developer.android.com/guide/components/bound-services) but running in a root process. `libsu` uses Android's native IPC mechanism, binder, for communication between your root service and the main application process. In addition to running Java/Kotlin code, loading native libraries with JNI is also supported (`android:extractNativeLibs=false` **is** allowed). Add `com.github.topjohnwu.libsu:service` as a dependency to access `RootService`:

```java
class ExampleInitializer extends Shell.Initializer {
@Override
public boolean onInit(Context context, Shell shell) {
try (InputStream bashrc = context.getResources().openRawResource(R.raw.bashrc)) {
// Load a script from raw resources
shell.newJob()
.add(bashrc) /* Load a script from resources */
.add("export ENVIRON_VAR=SOME_VALUE") /* Run some commands */
.exec();
}
return true;
public class RootConnection implements ServiceConnection { ... }
public class ExampleService extends RootService {
@Override
public IBinder onBind(Intent intent) {
// return IBinder from Messenger or AIDL stub implementation
}
}

// Register the class as an initializer
Shell.Config.setInitializers(ExampleInitializer.class);
RootConnection conn = new RootConnection();
Intent intent = new Intent(context, ExampleService.class);
RootService.bind(intent, conn);
```

`libsu` does NOT attempt to do messy workarounds for broken/outdated root solutions as I believe the responsibility shall not lie on the application side. This means there are no hacks for `sepolicy` live patching, `app_process` relocating etc. Any modern root solution with sane SELinux implementation should work: the library is tested on Magisk (supports API 17+), but the latest SuperSU on modern Android should also be fine.

### BusyBox
The I/O classes relies on several commandline tools. *Most* of the tools are available in modern Android via `toybox` (Android 6+), however for compatibility and reliable/reproducible behavior (some applets included in `toybox` is not fully featured), it is a good idea to have BusyBox bundled with your app.
If you want to embed BusyBox directly in your app to ensure 100% reliable/reproducible shell environment, add `com.github.topjohnwu.libsu:busybox` as a dependency (`android:extractNativeLibs=false` is **NOT** compatible with the `busybox` module):

The BusyBox binaries are pretty large in size (1.3 - 2.1 MB for each ABI). To reduce APK size, the best option is to use either [App Bundles](https://developer.android.com/guide/app-bundle) or [Split APKs](https://developer.android.com/studio/build/configure-apk-splits). If you are not publishing to Play Store, you can also limit the supported ABIs:
```java
Shell.Builder builder = ... ;
// Set BusyBoxInstaller as the first initializer.
builder.setInitializers(BusyBoxInstaller.class, /* other initializers */);
```

The BusyBox binaries are statically linked, feature complete, and includes full SElinux support. As a result they are pretty large in size (1.3 - 2.1 MB for each ABI). To reduce APK size, the best option is to use either [App Bundles](https://developer.android.com/guide/app-bundle) or [Split APKs](https://developer.android.com/studio/build/configure-apk-splits). If you are not publishing to Play Store, you can also limit your supporting ABIs:

```groovy
android {
Expand All @@ -167,15 +172,3 @@ android {
}
}
```

To setup BusyBox, set `BusyBoxInstaller` as the first shell initializer:

```java
// Add com.github.topjohnwu.libsu:busybox as a dependency, and
// register BusyBoxInstaller as the first initializer.
Shell.Config.setInitializers(BusyBoxInstaller.class, /* other initializers */);
```

## Example

This repo also comes with an example app (`:example`), check the code and play/experiment with it.

0 comments on commit fd5cbd5

Please sign in to comment.