Skip to content

Connect with an App ​

An app can run commands with AxManager's shell or root access through axsh, the same way a terminal does. This page is for app developers: the two ways to connect, what each costs, running your own code as shell or root with ShellService, and the demo app that tests it all.

Two ways to connect ​

AxManager's libaxsh.soThe axsh module
In your appNothing to bundleThe axsh library from Axeron API
VersionAlways matches the installed AxManagerThe one you built with
Start of a commandIn shell mode: a short-lived helper asks AxManager for the server each timeA connect to your app's own broker
Needs AxManager runningYes: it answers the helper (each time)Only for the first command

Both need AxManager's permission. AxManager's libaxsh.so and your own copy are the same client; the difference is what waits on the other side: a helper started for that one command, or a broker already running in your app. With the module, Axsh also skips the client and asks the broker's server directly.

AxManager's libaxsh.so ​

Declare AxManager in the manifest, so Android 11+ lets your app see it:

xml
<queries>
    <package android:name="frb.axeron.manager" />
</queries>

Then run libaxsh.so from AxManager's library folder. The folder changes with every AxManager update, so look it up each time your app starts:

kotlin
val dir = packageManager.getApplicationInfo("frb.axeron.manager", 0).nativeLibraryDir
val axsh = File(dir, "libaxsh.so")

val process = ProcessBuilder(axsh.path, "-c", "id").start()

The axsh module ​

Add the module from JitPack (or from Maven local or as Gradle projects, see the Axeron API README):

kotlin
// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven("https://jitpack.io")
    }
}
kotlin
// 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")
}

Start the broker in your Application:

kotlin
import frb.axeron.shell.Broker

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        Broker.start(this)
    }
}

The broker listens inside your app's process. It asks AxManager for the server once, keeps it, and serves every later command from there. Then run commands one of two ways.

Axsh, in your app's 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:

kotlin
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 resizes

For 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. It is run from your app's native library folder, so keep native libraries extracted there:

kotlin
// build.gradle.kts: only when you run libaxsh.so yourself
android {
    packaging { jniLibs { useLegacyPackaging = true } }
}
kotlin
val axsh = File(applicationInfo.nativeLibraryDir, "libaxsh.so")

ShellService doesn't need useLegacyPackaging either: it starts its process through Axsh, or through AxManager's libaxsh.so while the broker isn't running, never through your own.

Running commands ​

libaxsh.so takes the same arguments as sh. Run from an app, it has no terminal, so:

  • what you write to it goes to the shell's input;
  • stdout and stderr come back separately;
  • the exit code is the shell's.

You can start one process per command, or keep one shell open with libsu:

kotlin
Shell.setDefaultBuilder(Shell.Builder.create().setCommands(axsh.path))

Shell.cmd("pm list packages").exec()
One process per commandlibsu
Each commandA new axsh sessionA write to the open shell
Several at onceEach gets its own shellThey queue on the one shell
State (cd, variables)Never carries overCarries over unless you run each command in ( … )

Speed ​

What a command costs before it runs depends on the way you connect and, for AxManager's libaxsh.so, on how the server was started:

ServerAxManager's libaxsh.soThe axsh module
RootStraight to the serverStraight to the server
ShellHelper started, AxManager asked, every commandBroker in your app: AxManager asked once

Keep axsh ready

With AxManager's libaxsh.so in shell mode, the helper started by your app's first command leaves a small native process behind (about 3 MB), and the next commands connect to it like to a broker. Set it per app in AxManager (Privilege → your app → Keep axsh ready, under the Axeron permission):

  • Smart (default): stays while your app uses axsh, and leaves after 5 minutes without a command.
  • No limit: stays until the server stops or AxManager updates.
  • Off: every command starts a new helper.

Changing it ends the helper running now; the next command starts one with the new setting.

Permission ​

Your app needs AxManager's permission, set per app in Privilege. With Ask (the default), the first command shows AxManager's dialog and waits for your answer, up to 15 seconds. The command then runs, or stops with AxManager permission required: allow this app in AxManager.

Run your own code: ShellService ​

Commands are not always enough. ShellService runs a class of your app as shell or root, the way libsu's RootService does, but through axsh, so it works in shell mode too. It needs the axsh module (the broker is optional).

Write an AIDL interface, as for any bound service, and turn AIDL on:

aidl
// src/main/aidl/com/example/IMyService.aidl
interface IMyService {
    int getUid();
    String exec(String command);
}
kotlin
// build.gradle.kts
android { buildFeatures { aidl = true } }

The service. Everything inside it runs as shell (uid 2000) or root (uid 0):

kotlin
class MyService : ShellService() {
    override fun onBind(intent: Intent): IBinder {
        return object : IMyService.Stub() {
            override fun getUid(): Int {
                return Os.getuid()
            }

            override fun exec(command: String): String {
                val process = Runtime.getRuntime().exec(arrayOf("sh", "-c", command))
                return process.inputStream.bufferedReader().readText()
            }
        }
    }
}

Bind it from your app, on the main thread:

kotlin
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)
// when done:
ShellService.unbind(connection)

Nothing goes in the manifest.

libsu RootServiceShellService
Shell modeNoYes
CallbacksonCreate, onBind, onUnbind, onRebind, onDestroyThe same
Callsbind, unbind, stopThe same
ProcessesOne for all your services, gone with the appThe same
Daemon modeCATEGORY_DAEMON_MODEShellService.CATEGORY_DAEMON_MODE
Start the process yourselfbindOrTask, per bindShellService.setLauncher, once for all
UsersAllUser 0 only

Daemon mode ​

To keep a service running after your app closes, bind it with the daemon category and return true from onUnbind:

kotlin
class MyDaemon : ShellService() {
    override fun onUnbind(intent: Intent): Boolean {
        return true
    }

    override fun onRebind(intent: Intent) {
        // a later bind reached this same instance
    }

    override fun onBind(intent: Intent): IBinder { /* … */ }
}

val intent = Intent(context, MyDaemon::class.java)
intent.addCategory(ShellService.CATEGORY_DAEMON_MODE)
ShellService.bind(intent, connection)

It runs in a second process that outlives your app. The next bind, even after your app restarts, reaches the same instance. ShellService.stop(intent) ends it.

Good to know

  • The service's Context reports the uid's own package (com.android.shell in shell mode), so system services that check it, such as the clipboard, accept it.
  • getApplicationContext() returns the service: no Application runs in its process.
  • In a debuggable app, Android Studio's Attach Debugger lists the process as <package>:shell_shared or <package>:shell_daemon.
  • When a bind fails, ShellService.lastFailure(component) says why.

Demo app ​

demo-axsh in Axeron API is a test bench for all of the above. Build it, install it with AxManager's server running, then tap Run all.

SettingChoices
axsh_direct_connectionOn: its own libaxsh.so with the broker (the axsh module). Off: AxManager's libaxsh.so without a broker, as Termux runs it. Switching restarts the app.
Runneraxsh -c per command, or libsu's cached shell, which can be closed to time its start again.

The tests:

GroupWhat they check
ConnectionThe server answers; latency over five runs; four commands at once; nothing carried over between commands
ShellExit code; stdout and stderr apart; stdin up to its end; 100 000 lines of output; the server's environment
Privilegesuid and SELinux context; pm list packages; settings; dumpsys
Test Function pluginInstalled and enabled; axtest on PATH and callable from the app; not marked as a plugin process; its system.prop; plugin processes; every [FAIL] in its reports
libsu RootServicelibsu's own RootService through axsh, to compare (root mode only)
ShellServiceuid; Runtime.exec inside the service; two services in one process; a shared static; the clipboard from its Context; getApplicationContext()
ShellService daemonThe same pid after an unbind and after closing the app; a new pid after stop

The last group needs the Test Function plugin (plugins/test_function in the AxManager repository) installed and re-ignited. It shows that a plugin's system/bin binaries can be called from any allowed app.

Compare Latency with axsh_direct_connection on and off, and with each runner, to see what each path costs on your device.