4

I try to run shell scripts on Android using an application written in Java. I want to be able to run Android commands as root such as am or pm which need an export LD_LIBRARY_PATH=/system/lib command before. Unfortunately, the export command doesn't work. Here is the Java code I wrote :

private String readScript(String scriptPath) {

    String line = null;
    StringBuffer content = new StringBuffer();

    try {

        // Read the script line by line
        BufferedReader reader = new BufferedReader(new FileReader(scriptPath));
        while ((line = reader.readLine()) != null) {
            content.append(line).append('\n');
        }
        content.append("exit").append('\n');
        reader.close();

    } catch (IOException e) {

        Log.d(UpdateService.LOG_NAME, "Impossible to read script : " + e.getMessage());

    }

    return content.toString();

}

public StringBuffer runScript(String scriptPath) {

    String line = null;
    StringBuffer output = new StringBuffer();

    try {

        // Start the shell process
        ProcessBuilder pb = new ProcessBuilder("su");
        pb.redirectErrorStream(true);
        pb.directory(new File(context.getApplicationInfo().dataDir));
        pb.environment().put("LD_LIBRARY_PATH", "/system/lib");
        Process process = pb.start();

        // Execute the script
        OutputStreamWriter os = new OutputStreamWriter(process.getOutputStream());
        os.write(readScript(scriptPath));
        os.flush();
        os.close();

        // Get the output of the commands
        BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
        while ((line = in.readLine()) != null) {
            output.append(line).append('\n');
        }
        in.close();

    } catch (Exception e) {

        Log.d(UpdateService.LOG_NAME, "Error while executing " + scriptPath + " : " + e.getMessage());

    }

    return output;

}

I simply call it like :

Shell = new Shell();
StringBuffer output = shell.runScript("script.sh");

The script I try to run :

export LD_LIBRARY_PATH=/system/lib
printenv
am start -W -n com.android.settings/.Settings\$InputMethodAndLanguageSettingsActivity

Gives me the output :

_=/system/bin/printenv
ANDROID_BOOTLOGO=1
ANDROID_PROPERTY_WORKSPACE=9,32768
LOOP_MOUNTPOINT=/mnt/obb
EXTERNAL_STORAGE=/mnt/sdcard
ANDROID_DATA=/data
RANDOM=21985
ANDROID_SOCKET_zygote=10
ASEC_MOUNTPOINT=/mnt/asec
BOOTCLASSPATH=/system/framework/core.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/android.policy.jar:/system/framework/services.jar:/system/framework/apache-xml.jar:/system/framework/filterfw.jar
ANDROID_ROOT=/system
ANDROID_ASSETS=/system/app
PATH=/sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin
Segmentation fault

As you can see, the environment variable set with the environment() method of ProcessBuilder seems to not be set and the export in the script doesn't work either.

Any idea about what I'm doing wrong ? Thanks!

Edit

As suggested I also tried a version with exec() :

public StringBuffer runScript(String scriptPath) {

    String line = null;
    StringBuffer output = new StringBuffer();

    try {

        // Start the shell process
        Process process = Runtime.getRuntime().exec("su");

        // Execute the script commands
        OutputStreamWriter os = new OutputStreamWriter(process.getOutputStream());
        os.write(readScript(scriptPath));
        os.flush();
        os.close();

        // Get the output of the command
        BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
        while ((line = in.readLine()) != null) {
            output.append(line).append('\n');
        }
        in.close();

        process.waitFor();

    } catch (Exception ex) {

        Log.d(UpdateService.LOG_NAME, "Error while executing " + scriptPath + ".");

    }

    return output;

}

This time I get the same output without stderr (Segmentation fault) :

_=/system/bin/printenv
ANDROID_BOOTLOGO=1
ANDROID_PROPERTY_WORKSPACE=9,32768
LOOP_MOUNTPOINT=/mnt/obb
EXTERNAL_STORAGE=/mnt/sdcard
ANDROID_DATA=/data
RANDOM=21985
ANDROID_SOCKET_zygote=10
ASEC_MOUNTPOINT=/mnt/asec
BOOTCLASSPATH=/system/framework/core.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/android.policy.jar:/system/framework/services.jar:/system/framework/apache-xml.jar:/system/framework/filterfw.jar
ANDROID_ROOT=/system
ANDROID_ASSETS=/system/app
PATH=/sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin

So export LD_LIBRARY_PATH=/system/lib still doesn't work. It seems to me that ProcessBuilder is a better way to launch process as it it dedicated to it. Is it right?

Edit2

am and pm are actually wrappers to launch a java executable. I tried to implement a C version of this wrapper to create a full environment that should work in any circumstance. Here is the content of am :

# Script to start "am" on the device, which has a very rudimentary
# shell.
#
base=/system
export CLASSPATH=$base/framework/am.jar
exec app_process $base/bin com.android.commands.am.Am "$@"

And this is my C version :

#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(int argc, const char *argv[], const char *envp[]) {
    const char **p;

    const int NUM_ARG = 3;
    const char *APP_PROCESS = "/system/bin/app_process";
    const char *BIN = "/system/bin";
    const char *AM = "com.android.commands.am.Am";

    const int NUM_ENV = 11;
    const char *ENV[] = {
        "PATH=/usr/bin:/usr/sbin:/bin:/sbin:/system/sbin:/system/bin:/system/xbin:/system/xbin/bb:/data/local/bin",
        "SHELL=/system/bin/sh",
        "MKSH=/system/bin/sh",
        "HOME=/data",
        "HOSTNAME=android",
        "USER=root",
        "LOGNAME=root",
        "TERM=xterm",
        "LD_LIBRARY_PATH=/system/lib/",
        "CLASSPATH=/system/framework/am.jar",
        NULL
    };

    int i = 0;
    char *a[argc + NUM_ARG];
    a[i] = (char *)malloc(strlen(APP_PROCESS) + 1);
    strcpy(a[i++], APP_PROCESS);
    a[i] = (char *)malloc(strlen(BIN) + 1);
    strcpy(a[i++], BIN);
    a[i] = (char *)malloc(strlen(AM) + 1);
    strcpy(a[i++], AM);
    p = argv + 1;
    for (; i < argc + NUM_ARG - 1; i++) {
        a[i] = (char *)malloc(strlen(*p) + 1);
        strcpy(a[i], *p++);
    }
    a[i] = (char *)malloc(1);
    a[i] = NULL;

    p = envp;
    int envc = 0;
    while (*p++ != NULL) envc++;

    char *v[envc + NUM_ENV];
    for (i = 0; i < envc; i++) {
        v[i] = (char *)malloc(strlen(envp[i]) + 1);
        strcpy(v[i], envp[i]);
    }
    p = ENV;
    while (*p != NULL) {
        v[i] = (char *)malloc(strlen(*p) + 1);
        strcpy(v[i++], *p++);
    }
    v[i] = (char *)malloc(1);
    v[i] = NULL;

    execve(APP_PROCESS, a, v);
    fprintf(stderr, "am: %s  error: %s\n", APP_PROCESS, strerror(errno));
    for (i = 0; i < argc + NUM_ARG; i++) {
        free(a[i]);
    }
    for (i = 0; i < envc + NUM_ENV; i++) {
        free(v[i]);
    }

    return 1;
}

It works perfectly with ADB or SSH but even with this implementation, I get a "Segmentation fault" a the execve() line after an execution with ProcessBuilder. Why ???

Edit3

OK, I looked at the contents of the script init.rc which set out the environment variables. The LD_LIBRARY_PATH variable is correctly set.

on init
    sysclktz 0
    loglevel 7

    mkdir /system
    mkdir /data 0771 system system
    mkdir /cache 0770 system cache
    mkdir /config 0500 root root

    # Setup the global environment
    export PATH /sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin
    export LD_LIBRARY_PATH /vendor/lib:/system/lib
    export ANDROID_BOOTLOGO 1
    export ANDROID_ROOT /system
    export ANDROID_ASSETS /system/app
    export ANDROID_DATA /data
    export EXTERNAL_STORAGE /mnt/sdcard
    export ASEC_MOUNTPOINT /mnt/asec
    export LOOP_MOUNTPOINT /mnt/obb
    export BOOTCLASSPATH /system/framework/core.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/android.policy.jar:/system/framework/services.jar:/system/framework/apache-xml.jar:/system/framework/filterfw.jar

But... for some reason they are changed or deleted depending on the context. For example, if I'm connected via ADB :

$ env
_=/system/xbin/env
ANDROID_BOOTLOGO=1
ANDROID_PROPERTY_WORKSPACE=9,32768
LOOP_MOUNTPOINT=/mnt/obb
PS1=$(precmd)$USER@$HOSTNAME:${PWD:-?} $ 
USER=shell
EXTERNAL_STORAGE=/mnt/sdcard
ANDROID_DATA=/data
RANDOM=22124
TERM=vt100
MKSH=/system/bin/sh
HOME=/data
LD_LIBRARY_PATH=/vendor/lib:/system/lib
ASEC_MOUNTPOINT=/mnt/asec
HOSTNAME=android
BOOTCLASSPATH=/system/framework/core.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/android.policy.jar:/system/framework/services.jar:/system/framework/apache-xml.jar:/system/framework/filterfw.jar
ANDROID_ROOT=/system
SHELL=/system/bin/sh
ANDROID_ASSETS=/system/app
PATH=/sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin

If I'm connected via SSH :

$ env
_=/system/xbin/env
ANDROID_PROPERTY_WORKSPACE=9,32768
ANDROID_BOOTLOGO=1
PS1=$(precmd)$USER@$HOSTNAME:${PWD:-?} $ 
USER=shell
EXTERNAL_STORAGE=/mnt/sdcard
LOGNAME=shell
ANDROID_DATA=/data
RANDOM=4546
TERM=xterm
SHELL=/system/bin/sh
MKSH=/system/bin/sh
HOME=/data/data/com.teslacoilsw.quicksshd/home
HOSTNAME=android
BOOTCLASSPATH=/system/framework/core.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/android.policy.jar:/system/framework/services.jar:/system/framework/apache-xml.jar:/system/framework/filterfw.jar
ANDROID_ROOT=/system
ANDROID_ASSETS=/system/app
PATH=/data/data/com.teslacoilsw.quicksshd/dropbear:/usr/bin:/usr/sbin:/bin:/sbin:/system/sbin:/system/bin:/system/xbin:/system/xbin/bb:/data/local/bin

If I run the command through Java :

$ env
_=/system/bin/printenv
ANDROID_BOOTLOGO=1
ANDROID_PROPERTY_WORKSPACE=9,32768
LOOP_MOUNTPOINT=/mnt/obb
EXTERNAL_STORAGE=/mnt/sdcard
ANDROID_DATA=/data
RANDOM=7424
ANDROID_SOCKET_zygote=10
ASEC_MOUNTPOINT=/mnt/asec
BOOTCLASSPATH=/system/framework/core.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/android.policy.jar:/system/framework/services.jar:/system/framework/apache-xml.jar:/system/framework/filterfw.jar
ANDROID_ROOT=/system
ANDROID_ASSETS=/system/app
PATH=/sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin

Via ADB the command am works very well. Via SSH it can work if I run export LD_LIBRARY_PATH=/system/lib/. In Java it never works :/

didil
  • 693
  • 8
  • 22
  • this looks nice http://androidcookbook.com/Recipe.seam;jsessionid=1BF1DCA30905444BC776098D4F81DB48?recipeId=2457 – eduyayo Oct 22 '14 at 07:11
  • I had a similar problem, updating su binary on the mobile device solved the issue. – EvZ Oct 24 '14 at 12:00
  • To simplify running processes from java, I suggest to use LibSuperUser. – HappyCactus Oct 24 '14 at 12:03
  • Back on the problem: LD_LIBRARY_PATH has restrictions on linux, and more restrictions on Android. not that any app launched from am will likely not inherit the shell environment, because am will not spawn a process from the shell, but instead from the init. So it is very likely that you'll not be able to inherit it. I suggest to avoid this problem by putting the library in a directory already in the library search path. – HappyCactus Oct 24 '14 at 12:07
  • Updating su doesn't solve the problem. For LD_LIBRARY_PATH, it works now when run `am` via SSH, something that needed an `export` before. I consider that the environment variables are correctly set. If I run `am` in the Terminal Emulator app, I get the same "Segmentation fault". My tablet runs under Android 4.0.3 with Superuser 3.0.7 and QuickSSHd 2.0.3. `am` works perfectly through SSH or ADB. I have an other tablet, different hardware and running under Android 4.1.1 with SuperSU 1.94 and QuickSSHd 2.0.3. The `am` command and the others (`pm`, `input`) work in any circumstance on it. – didil Oct 24 '14 at 14:48

0 Answers0