The libraries apps use to talk to AxManager's server, which
runs as shell (ADB) or root. AxManager itself is built on them; it includes this repository as
its api submodule.
Guides: axmanager.app, in particular Run in any terminal and Connect with an app.
| Module | What it is |
|---|---|
api |
The client API: Axeron (binder, permission, listeners), relaunchServer, file service, plugins, server info |
aidl |
The server's binder interfaces (IAxeronService, IAxshProcess, file service, Shizuku's) |
shared |
Constants shared by the server and its clients (AxeronApiConstant, ShizukuApiConstant) |
provider |
AxeronProvider, through which the server hands an app its binder |
server-shared |
Server-side code shared with the manager |
axsh |
The axsh shell: native client libaxsh.so, its in-app broker, the app_process bootstrap, and ShellService |
demo-axsh |
Test bench app for axsh (see below) |
Add the repository:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}Then the modules you need, at a release tag (or a commit hash, or main-SNAPSHOT):
// build.gradle.kts
dependencies {
implementation("com.github.fahrez182.Axeron-API:axsh:2.1.0")
// axsh brings these two along at runtime, and its own API (Broker, Axsh, ShellService) uses
// none of their types. Add them, at the same version, only to use their classes directly
// (IAxeronService, AxeronApiConstant, …).
// implementation("com.github.fahrez182.Axeron-API:aidl:2.1.0")
// implementation("com.github.fahrez182.Axeron-API:shared:2.1.0")
}Published modules: axsh, and aidl and shared, which it runs on. api and provider are
AxManager's own and aren't published past 2.0.0 (releases up to it still have them on JitPack).
Include the modules as Gradle projects (as AxManager does), or publish them to Maven local and depend on them from there:
./gradlew publishToMavenLocal// settings.gradle.kts: mavenLocal() in dependencyResolutionManagement.repositories
// build.gradle.kts
// group dev.frb.axeron, version = api_version_code from manifest.gradle.kts (21000 for 2.1.0)
dependencies {
implementation("dev.frb.axeron:axsh:21000")
// As from JitPack: only to use their classes directly.
// implementation("dev.frb.axeron:aidl:21000")
// implementation("dev.frb.axeron:shared:21000")
}Every app needs AxManager's permission (Allowed, Ask or Denied in AxManager's Privilege page). With Ask, the first call shows AxManager's dialog.
An app runs shell commands through axsh in one of two ways.
<queries>
<package android:name="frb.axeron.manager" />
</queries>val dir = packageManager.getApplicationInfo("frb.axeron.manager", 0).nativeLibraryDir
val axsh = File(dir, "libaxsh.so") // changes on every AxManager update: resolve it each start
ProcessBuilder(axsh.path, "-c", "id").start()Nothing to bundle, and it always matches the installed AxManager. In ADB mode each run starts a
short-lived helper in app_process that asks AxManager for the server's binder, as in Termux.
After that first command the helper leaves libaxsh.so --axsh-daemon behind (a few MB, no JVM),
which hands later commands straight to the server; AxManager's Keep axsh ready (per app, in
its Privilege details) sets how long it stays: smart (default, 5 idle minutes), no limit or off.
// build.gradle.kts
implementation(project(":axsh")) // or the Maven artifact (see Using it)import frb.axeron.shell.Broker
class App : Application() {
override fun onCreate() {
super.onCreate()
Broker.start(this) // listens on @axsh.<uid>, in this process
}
}The broker fetches the binder once and keeps it. An app that already holds the binder passes it:
Broker.start(this) { Axeron.getBinder() }. Then run commands one of two ways:
Axsh, in this process. A shell on the server as a java.lang.Process, from one binder call:
nothing is exec'd, so there is nothing to extract. Call it off the main thread.
import frb.axeron.shell.Axsh
val process = Axsh.newProcess("-c", "id") // sh's arguments; none for a shell on stdin
val run = Axsh.exec(arrayOf("ls", "-l"), dir = "/") // a program as it is, no shell around it
val term = Axsh.newTerminal(rows = 24, cols = 80) // on a PTY: setWindowSize() when it resizesFor libsu: Shell.Builder.create().build(Axsh.newProcess()).
Your own libaxsh.so, as a program. For what takes a binary rather than a Process: a terminal
emulator, libsu's setCommands, another process. The broker serves each run of it from this
process, so a command costs a socket connect and one binder call. It is exec'd from the native
library folder, so it must be extracted there:
// build.gradle.kts: only when you run libaxsh.so yourself
android {
packaging { jniLibs { useLegacyPackaging = true } }
}val axsh = File(applicationInfo.nativeLibraryDir, "libaxsh.so")libaxsh.so takes sh's arguments. Without a terminal its pipes become the remote shell's stdio
(separate stdout and stderr, the shell's exit code); in a terminal it opens a PTY. It works as a
libsu shell too, which keeps one session open:
Shell.setDefaultBuilder(Shell.Builder.create().setCommands(axsh.path))libsu's RootService, but through axsh, so it works in ADB mode too. Your service class runs in
an app_process started as the server's uid (2000 in ADB mode, 0 with root), loaded from your
APK. Needs the axsh module, but not useLegacyPackaging: with Broker.start the process starts
through Axsh.exec, without it through AxManager's libaxsh.so, never through your own.
An AIDL interface, as for any bound service:
// src/main/aidl/com/example/IMyService.aidl
interface IMyService {
int getUid();
String exec(String command);
}// build.gradle.kts
android { buildFeatures { aidl = true } }The service. Everything in it runs as shell or root:
class MyService : ShellService() {
override fun onBind(intent: Intent): IBinder {
return object : IMyService.Stub() {
override fun getUid(): Int {
return Os.getuid() // 2000 or 0
}
override fun exec(command: String): String {
val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command))
return process.inputStream.bufferedReader().readText()
}
}
}
}Bind it from the app, on the main thread:
val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
val service = IMyService.Stub.asInterface(binder)
// call it off the main thread: service.exec("id")
}
override fun onServiceDisconnected(name: ComponentName) {}
}
ShellService.bind(Intent(context, MyService::class.java), connection)
// …
ShellService.unbind(connection)No manifest entry: the axsh module brings its provider, and its ProGuard rules keep every
ShellService subclass.
Same as libsu's RootService: onCreate, onBind, onUnbind, onRebind, onDestroy,
stopSelf(); bind, unbind, stop, main thread only; one process for all of an app's
services (statics are shared), gone with the app.
Daemon mode: bind with intent.addCategory(ShellService.CATEGORY_DAEMON_MODE) and return
true from onUnbind. The service then runs in a second process that outlives the app; the next
bind, even after the app restarts, reaches the same instance and calls onRebind.
ShellService.stop(intent) ends it.
Also:
- The service's Context reports the uid's own package (
com.android.shellin ADB mode), so system services that check it (clipboard, AppOps) accept it. getApplicationContext()returns the service itself: noApplicationruns there.- In a debuggable app, the process shows in Android Studio's Attach Debugger as
<package>:shell_sharedor<package>:shell_daemon. ShellService.lastFailure(component)says why a bind gaveonBindingDiedoronNullBinding.
Starting the process yourself: libsu's bindOrTask hands back the start command for a shell
of your choice. Here, set it once for all binds with ShellService.setLauncher { argv -> … },
which gets the app_process argv and returns the started Process.
Not (yet) like libsu: user 0 only.
A test bench for all of the above. Run it with AxManager's server running.
axsh_direct_connection(switch at the top, restarts the app): on, the app's ownlibaxsh.soandBroker; off, AxManager'slibaxsh.sowith no broker, as in Termux.- Runner: a new
axsh -cprocess per command, or libsu's cached shell. - Tests:
- connection: latency, concurrent sessions, isolation between commands;
- shell: exit code, separate streams, stdin, large output, environment;
- privileges: uid and SELinux context,
pm,settings,dumpsys; - the Test Function plugin (
plugins/test_functionin AxManager): installed,axtestonPATHand callable, not marked as a plugin process, itssystem.prop, its own reports; - libsu
RootService, root mode only; ShellService: uid,Runtime.execinside, two services in one process with a shared static, the clipboard from its Context,getApplicationContext();ShellServicedaemon: same pid across an unbind and across closing the app, a new one afterstop.
- Custom command, through the selected runner.
Compare Latency with the switch on and off, and with each runner, to see what each path costs.