@WorkerThread
  public static CommandResult run(@NonNull String shell, @NonNull String[] commands, @Nullable String[] env) {
    List<String> stdout = Collections.synchronizedList(new ArrayList<String>());
    List<String> stderr = Collections.synchronizedList(new ArrayList<String>());
    int exitCode;

    try {
      // setup our process, retrieve stdin stream, and stdout/stderr gobblers
      Process process = runWithEnv(shell, env);
      DataOutputStream stdin = new DataOutputStream(process.getOutputStream());
      StreamGobbler stdoutGobbler = new StreamGobbler(process.getInputStream(), stdout);
      StreamGobbler stderrGobbler = new StreamGobbler(process.getErrorStream(), stderr);

      // start gobbling and write our commands to the shell
      stdoutGobbler.start();
      stderrGobbler.start();
      try {
        for (String write : commands) {
          stdin.write((write + "\n").getBytes("UTF-8"));
          stdin.flush();
        }
        stdin.write("exit\n".getBytes("UTF-8"));
        stdin.flush();
      } catch (IOException e) {
        //noinspection StatementWithEmptyBody
        if (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed")) {
          // Method most horrid to catch broken pipe, in which case we do nothing. The command is not a shell, the
          // shell closed stdin, the script already contained the exit command, etc. these cases we want the output
          // instead of returning null
        } else {
          // other issues we don't know how to handle, leads to returning null
          throw e;
        }
      }

      // wait for our process to finish, while we gobble away in the background
      exitCode = process.waitFor();

      // make sure our threads are done gobbling, our streams are closed, and the process is destroyed - while the
      // latter two shouldn't be needed in theory, and may even produce warnings, in "normal" Java they are required
      // for guaranteed cleanup of resources, so lets be safe and do this on Android as well
      try {
        stdin.close();
      } catch (IOException e) {
        // might be closed already
      }
      stdoutGobbler.join();
      stderrGobbler.join();
      process.destroy();
    } catch (InterruptedException e) {
      exitCode = ShellExitCode.WATCHDOG_EXIT;
    } catch (IOException e) {
      exitCode = ShellExitCode.SHELL_WRONG_UID;
    }

    return new CommandResult(stdout, stderr, exitCode);
  }
<p>Runs commands using the supplied shell, and returns the result.</p>

<p>When in debug mode, the code will also excessively log the commands passed to and the output returned from
the shell.</p>

<p>Though this function uses background threads to gobble stdout and stderr so a deadlock does not occur if the
shell produces massive output, the output is still stored in a List&lt;String&gt;, and as such doing something
like <em>'ls -lR /'</em> will probably have you run out of memory.</p>

@param shell
    The shell to use for executing the commands
@param commands
    The commands to execute
@param env
    List of all environment variables (in 'key=value' format) or null for defaults
@return Output of the commands, or null in case of an error
imitate/src/main/java/com/engineer/imitate/shell/Shell.java