1

Note: this question isn't necessary just for Android developers. It should be the same for Java developers that performed root operations on Linux too.

Background

My app allows the user the perform various root operations (an operation that can only be performed on rooted devices).

Until now, each time the app wishes to perform a root operation, I had opened a new process using the Runtime.getRuntime().exec("su") command.

I wish to improve the way the app works, by calling this command only once, and then writing to the outputStream and reading from the inputStream while doing so.

A similar app that does this is "Terminal emulator". Once you type there "su" (and grant it root permission), it will allow you go perform any root operation and read any path of the device.

The problem

It seems that only the "echo" command works, but any other command doesn't. Also, I couldn't think of a good way to separate between the root operations output.

What I've tried

First, in order to initialize the root batching operations, I did this:

  process=Runtime.getRuntime().exec("su");
  _inputStreamReader=new BufferedReader(new InputStreamReader(process.getInputStream()));
  _out=process.getOutputStream();
  final String testLine=Long.toString(System.currentTimeMillis());
  _out.write(("echo "+testLine+"\n").getBytes());
  _out.flush();
  final String resultLine=_inputStreamReader.readLine();
  if(resultLine==null)
    {
    _out.write("exit \n".getBytes());
    _out.flush();
    _out.close();
    _out=null;
    _inputStreamReader.close();
    _inputStreamReader=null;
    process.waitFor();
    return false;
    }
  while(!resultLine.equals(testLine))
    resultLine=_inputStreamReader.readLine();
  return true;

the code above (is supposed to) ensures I have root and that I can continue working on the "_out" and "inputStreamReader" . the call to "echo" has 2 purposes : to have some fake operation being done, and to also become a starting point and a separator between commands.

Here the second purpose is fake, but for each operation that I make further, this is how I thought of handling the separation of various root operations.

Now, if I want to make another root operation (assuming the above works fine), this is an example of an operation I wish to perform :

_out.write(("find "+someFolderPath+"/* -type f\n").getBytes());
_out.flush();
final String line=_inputStreamReader.readLine();

Thing is, it gets stuck on the "readLine" part, even though it should return a list of all of the file/folders within the specified path.

This command works perfectly using the previous way I performed root operations, and it should work here too.

using "echo" before those lines and then using "readLine()" shows that it's still available, as I will still get the result line. However, trying to call "readLine()" again (after the above command) will still make it stuck.

I've also tried adding "su" (or "su -" or "su -c") before the "find" command, but it didn't help.

This is the old code, which works fine:

  final Process p=Runtime.getRuntime().exec("su");
  final InputStream inputStream=p.getInputStream();
  final DataOutputStream os=new DataOutputStream(p.getOutputStream());
  os.writeBytes("find "+someFolderPath+"/* -type f\n");
  os.writeBytes("exit\n");
  os.flush();
  //read from the inputstream (using BufferedReader) as much as I wish, till it ends.

The question

How should I avoid re-creating new processes for root operations? Is this a good way to do it?

How can I fix the above code? What is wrong with it?

Also, Is there a good way to run multiple root operations using a single "su" command, yet without any kind of separator like the one I've used (so that I could differentiate when a new command output starts and the previous one ends).

android developer
  • 114,585
  • 152
  • 739
  • 1,270

2 Answers2

1

I am sure, that your find command is just too slow. You should run it in the different process. There is great library to do this. And a manual.

Alex
  • 592
  • 4
  • 10
  • no, it worked perfectly using the previous way. not only that, but other commands also get stuck (like "ls") . – android developer Jun 20 '14 at 06:09
  • In fact, your link says "Some apps need to make a lot of su calls throughout the app's lifecycle but can't batch these commands together. In such a case you should consider starting an interactive su shell and keeping it alive alongside your app", so it suggests not creating new processes for multiple operations, and use a single one instead (which is what I try to do). – android developer Jun 20 '14 at 06:16
  • However, this library looks promising. maybe I will try it out and see how it goes. I still wish to know what is the answer to my question though, since I want to learn it myself. – android developer Jun 20 '14 at 06:42
  • 1
    Also, you can +! my post. Looks like it helped :) – Alex Jun 25 '14 at 11:59
  • Yes, thank you. it helped, but I'm not marking neither of our answers as the correct ones, since they aren't answering the original question. – android developer Jun 25 '14 at 13:21
0

OK, even though it's not quite what I wanted, and even though I wanted to learn how to do it myself, I've found that "libsuperuser" library (by "ChainFire") does it much easier.

What I've asked for is called "interactive mode" in this library.

Here's a sample class I've made that helps you do it using this library:

public class Root
  {
  private static final Root _instance =new Root();
  private Boolean           _hasRoot  =null;
  private Shell.Interactive _rootSession;

  public interface IGotRootListener
    {
    public void onGotRootResult(boolean hasRoot);
    }

  public static Root getInstance()
    {
    return _instance;
    }

  public boolean hasRoot()
    {
    return _hasRoot!=null&&_hasRoot;
    }

  //should be called on the UI thread
  public void getRoot(final IGotRootListener listener)
    {
    if(_hasRoot!=null&&_hasRoot)
      {
      listener.onGotRootResult(true);
      return;
      }
    final AtomicReference<Interactive> rootSessionRef=new AtomicReference<>();
    rootSessionRef.set(new Shell.Builder().useSU().setWantSTDERR(true).setWatchdogTimeout(5).setMinimalLogging(true).open(new Shell.OnCommandResultListener()
      {
        @Override
        public void onCommandResult(final int commandCode,final int exitCode,final List<String> output)
          {
          final boolean success=exitCode==Shell.OnCommandResultListener.SHELL_RUNNING;
          if(success)
            _rootSession=rootSessionRef.get();
          _hasRoot=success;
          listener.onGotRootResult(success);
          }
      }));
    }

... // TODO add functions to call commands via _rootSession.addCommand , only if got root

Still, if anyone has a solution to my original question, I would love to know about it.

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • have a look at afwall+ code which uses libsuperuser with interactive mode (RootShell.java and api.java) – ukanth Jul 12 '14 at 04:37
  • But I also use this library, and I've already wrote a snippet here of how to do it. I only asked how to do it without the library, as my original question says. – android developer Jul 17 '14 at 11:08