summaryrefslogtreecommitdiffhomepage
path: root/app/src/main/java/com/wireguard/android/backends/RootShell.java
blob: 9fd5020d5f39e4426846e1e8a67727304d0bc26b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package com.wireguard.android.backends;

import android.content.Context;
import android.util.Log;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

/**
 * Helper class for running commands as root.
 */

class RootShell {
    /**
     * Setup commands that are run at the beginning of each root shell. The trap command ensures
     * access to the return value of the last command, since su itself always exits with 0.
     */
    private static final String SETUP_TEMPLATE = "export TMPDIR=%s\ntrap 'echo $?' EXIT\n";
    private static final String TAG = "RootShell";
    private static final Pattern ERRNO_EXTRACTOR = Pattern.compile("error=(\\d+)");

    private final byte[] setupCommands;
    private final String shell;

    RootShell(final Context context) {
        this(context, "su");
    }

    RootShell(final Context context, final String shell) {
        final String tmpdir = context.getCacheDir().getPath();
        setupCommands = String.format(SETUP_TEMPLATE, tmpdir).getBytes(StandardCharsets.UTF_8);
        this.shell = shell;
    }

    /**
     * Run a series of commands in a root shell. These commands are all sent to the same shell
     * process, so they can be considered a shell script.
     *
     * @param output   Lines read from stdout and stderr are appended to this list. Pass null if the
     *                 output from the shell is not important.
     * @param commands One or more commands to run as root (each element is a separate line).
     * @return The exit value of the last command run, or -1 if there was an internal error.
     */
    int run(final List<String> output, final String... commands) {
        if (commands.length < 1)
            throw new IndexOutOfBoundsException("At least one command must be supplied");
        int exitValue = -1;
        try {
            final ProcessBuilder builder = new ProcessBuilder().redirectErrorStream(true);
            final Process process = builder.command(shell).start();
            final OutputStream stdin = process.getOutputStream();
            stdin.write(setupCommands);
            for (final String command : commands)
                stdin.write(command.concat("\n").getBytes(StandardCharsets.UTF_8));
            stdin.close();
            Log.d(TAG, "Sent " + commands.length + " command(s), now reading output");
            final InputStream stdout = process.getInputStream();
            final BufferedReader stdoutReader =
                    new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8));
            String line;
            String lastLine = null;
            while ((line = stdoutReader.readLine()) != null) {
                Log.v(TAG, line);
                lastLine = line;
                if (output != null)
                    output.add(line);
            }
            process.waitFor();
            process.destroy();
            if (lastLine != null) {
                // Remove the exit value line from the output
                if (output != null)
                    output.remove(output.size() - 1);
                exitValue = Integer.parseInt(lastLine);
            }
            Log.d(TAG, "Session completed with exit value " + exitValue);
        } catch (IOException | InterruptedException | NumberFormatException e) {
            Log.w(TAG, "Session failed with exception", e);
            final Matcher match = ERRNO_EXTRACTOR.matcher(e.toString());
            if (match.find())
                exitValue = Integer.valueOf(match.group(1));
        }
        return exitValue;
    }
}