Skip to content

Commit 7ac3338

Browse files
committed
Debugging support
1 parent d7a957d commit 7ac3338

File tree

9 files changed

+285
-28
lines changed

9 files changed

+285
-28
lines changed

librootjava/README.md

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Run Java (and Kotlin) code as root!
66
- Access to all the classes in your projects
77
- Access to Android classes
88
- Easy Binder-based IPC/RPC
9+
- Debugging support
910

1011
## License
1112

@@ -69,14 +70,6 @@ While this library was originally built to support Android 4.2+ devices,
6970
it only officially supports 5.0+. The first public GitHub release was
7071
tested specifically on 5.0, 7.0, 8.0, and 9.0.
7172

72-
## Debugging
73-
74-
Debugging the code running as root is currently not supported. I made
75-
some headway getting the jdwp server running, but I've not been able
76-
to successfully connect jdb or AndroidStudio to it. If you want to take
77-
a stab at it, there are some comments in
78-
```RootJava.getLaunchString()``` related to it.
79-
8073
## Recommended reading
8174

8275
I strongly recommend you read the library's source code in its entirety
@@ -270,6 +263,84 @@ non-root code.
270263
(See the [example project](../librootjava_example) for a more elaborate
271264
example for this entire process)
272265

266+
#### Debugging
267+
268+
Debugging is supported since version 1.1.0, but disabled by default.
269+
270+
To enable debugging, first we must tell the non-root process to launch
271+
our root process with debug support enabled. We do this by calling
272+
```Debugger.setEnabled()``` *before* calling
273+
```RootJava.getLaunchScript()```:
274+
275+
```
276+
public class MyActivity {
277+
// ...
278+
private void launchRootProcess() {
279+
// ...
280+
281+
Debugger.setEnabled(BuildConfig.DEBUG);
282+
rootShell.addCommand(RootJava.getLaunchScript(this, RootMain.class, null, null, null, BuildConfig.APPLICATION_ID + ":root"));
283+
}
284+
}
285+
```
286+
287+
We use ```BuildConfig.DEBUG``` instead of ```true``` to prevent
288+
potential issues with release builds.
289+
290+
In the code running as root, we then call ```Debugger.waitFor()```
291+
to pause execution (of the current thread) until a debugger is connected:
292+
293+
```
294+
public class RootMain {
295+
public static void main(String[] args) {
296+
RootJava.restoreOriginalLdLibraryPath(); // call this first!
297+
298+
if (BuildConfig.DEBUG) {
299+
Debugger.waitFor(true); // wait for connection
300+
}
301+
302+
setYourBreakpointHere();
303+
}
304+
}
305+
```
306+
307+
We wrap the call inside a ```BuildConfig.DEBUG``` check, again to prevent
308+
issues with release builds.
309+
310+
Note that for long-running processes (such as daemons) you may not want
311+
to explicitly wait for a debugger connection, in that case you can use
312+
the ```Debugger.setName()``` method instead. That method may also be
313+
called *before* ```Debugger.waitFor()``` to customize the debugger
314+
display name, as by default the process name is used.
315+
316+
Now that debugging is enabled, we still need to actually connect to
317+
the process. You can do this in Android Studio via the *Attach
318+
debugger to Android process* option in the *Run* menu. Once the root
319+
process is running, it will be listed in the popup window.
320+
321+
Note that you can debug *both* the non-root *and* root process at the
322+
same time. While Android Studio can only debug a single application
323+
*launched* in debug mode, you can *attach* to multiple processes. So
324+
you can simply run your application in debug mode, have it launch the
325+
root process, then *attach* to that root process, and debug both
326+
simultaneously.
327+
328+
Android Studio of course has no knowledge of the relation between the
329+
multiple processes being debugged, so stepping into an IPC call in one
330+
process will not automatically break on the implementation code in the
331+
other process. You will have to set those breakpoints manually.
332+
333+
#### BuildConfig
334+
335+
The example snippets and projects make extensive use of the BuildConfig
336+
class. This class is generated during the build process for every
337+
module, library, etc. Double check you are importing the correct
338+
BuildConfig class, the one from your application's package (unless you
339+
know exactly what you're doing). Note that this complicates putting this
340+
code inside libraries and modules, as you cannot reference the
341+
application's BuildConfig class from a library. Work-arounds are beyond
342+
the scope of this README.
343+
273344
#### Daemonizing
274345

275346
For some use-cases you may want to run your root process as a daemon,
@@ -348,7 +419,7 @@ an Android 10 preview comes out!
348419
## Gradle
349420

350421
```
351-
implementation 'eu.chainfire:librootjava:1.0.0'
422+
implementation 'eu.chainfire:librootjava:1.1.0'
352423
```
353424

354425
## Notes

librootjava/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ ext {
5151
gitUrl = 'https://github.com/Chainfire/librootjava.git'
5252
issueTrackerUrl = 'https://github.com/Chainfire/librootjava/issues'
5353

54-
libraryVersion = '1.0.0'
54+
libraryVersion = '1.1.0'
5555

5656
developerId = 'Chainfire'
5757
developerName = 'Jorrit Jongma'
@@ -72,7 +72,7 @@ task installMavenLocal(type: Upload) {
7272
packaging 'aar'
7373
groupId = publishedGroupId
7474
artifactId = artifact
75-
version = '1.0.0-SNAPSHOT'
75+
version = libraryVersion + '-SNAPSHOT'
7676
}
7777
}
7878
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package eu.chainfire.librootjava;
2+
3+
import java.io.BufferedReader;
4+
import java.io.ByteArrayOutputStream;
5+
import java.io.File;
6+
import java.io.FileDescriptor;
7+
import java.io.FileOutputStream;
8+
import java.io.FileReader;
9+
import java.io.IOException;
10+
import java.io.PrintStream;
11+
12+
/**
13+
* Utility methods to support debugging
14+
*/
15+
@SuppressWarnings({"unused", "WeakerAccess"})
16+
public class Debugger {
17+
/**
18+
* Is debugging enabled ?
19+
*/
20+
static volatile boolean enabled = false;
21+
22+
/**
23+
* Is debugging enabled ?<br>
24+
* <br>
25+
* If called from non-root, this will return if we are launching new processes with debugging
26+
* enabled. If called from root, this will return if the current process was launched
27+
* with debugging enabled.
28+
*
29+
* @return Debugging enabled
30+
*/
31+
public static boolean isEnabled() {
32+
if (android.os.Process.myUid() >= 10000) {
33+
return enabled;
34+
} else {
35+
return Reflection.isDebuggingEnabled();
36+
}
37+
}
38+
39+
/**
40+
* Launch root processes with debugging enabled ?
41+
* <br>
42+
* To prevent issues on release builds, BuildConfig.DEBUG should be respected. So instead
43+
* of passing <em>true</em> you would pass <em>BuildConfig.DEBUG</em>, while <em>false</em>
44+
* remains <em>false</em>.
45+
*
46+
* @param enabled Enable debugging (default: false)
47+
*/
48+
public static void setEnabled(boolean enabled) {
49+
Debugger.enabled = enabled;
50+
}
51+
52+
/**
53+
* Cache for name to present to debugger. Really only used to determine if we have manually
54+
* set a name already.
55+
*/
56+
private static volatile String name = null;
57+
58+
/**
59+
* Set name to present to debugger<br>
60+
* <br>
61+
* This method should only be called from the process running as root.<br>
62+
* <br>
63+
* Debugging will <strong>not</strong> work if this method has not been called, but the
64+
* {@link #waitFor(boolean)} method will call it for you, if used.<br>
65+
* <br>
66+
* {@link RootJava#restoreOriginalLdLibraryPath()} should have been called before calling
67+
* this method.<br>
68+
* <br>
69+
* To prevent issues with release builds, this call should be wrapped in a BuildConfig.DEBUG
70+
* check.
71+
*
72+
* @param name Name to present to debugger, or null to use process name
73+
*
74+
* @see #waitFor(boolean)
75+
*/
76+
public static void setName(String name) {
77+
if (Debugger.name == null) {
78+
if (name == null) {
79+
final File cmdline = new File("/proc/" + android.os.Process.myPid() + "/cmdline");
80+
try (BufferedReader reader = new BufferedReader(new FileReader(cmdline))) {
81+
name = reader.readLine();
82+
if (name.indexOf(' ') > 0) name = name.substring(0, name.indexOf(' '));
83+
if (name.indexOf('\0') > 0) name = name.substring(0, name.indexOf('\0'));
84+
} catch (IOException e) {
85+
name = "librootjava:unknown";
86+
}
87+
}
88+
Debugger.name = name;
89+
Reflection.setAppName(name);
90+
}
91+
}
92+
93+
/**
94+
* Wait for debugger to connect<br>
95+
* <br>
96+
* This method should only be called from the process running as root.<br>
97+
* <br>
98+
* If {@link #setName(String)} has not been called manually, the display name for the
99+
* debugger will be set to the current process name.<br>
100+
* <br>
101+
* After this method has been called, you can connect AndroidStudio's debugger to the root
102+
* process via <em>Run-&gt;Attach Debugger to Android process</em>.<br>
103+
* <br>
104+
* {@link RootJava#restoreOriginalLdLibraryPath()} should have been called before calling
105+
* this method.<br>
106+
* <br>
107+
* Android's internal debugger code will print to STDOUT during this call using System.println,
108+
* which may be annoying if your non-root process communicates with the root process through
109+
* STDIN/STDOUT/STDERR. If the <em>swallowOutput</em> parameter is set to true, System.println
110+
* will be temporarily redirected, and reset back to STDOUT afterwards.<br>
111+
* <br>
112+
* To prevent issues with release builds, this call should be wrapped in a BuildConfig.DEBUG
113+
* check:
114+
*
115+
* <pre>
116+
* {@code
117+
* if (BuildConfig.DEBUG) {
118+
* Debugger.waitFor(true);
119+
* }
120+
* }
121+
* </pre>
122+
*
123+
* @param swallowOutput Temporarily redirect STDOUT ?
124+
*/
125+
public static void waitFor(boolean swallowOutput) {
126+
if (Reflection.isDebuggingEnabled()) {
127+
if (swallowOutput) {
128+
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
129+
System.setOut(new PrintStream(buffer));
130+
}
131+
setName(null);
132+
android.os.Debug.waitForDebugger();
133+
if (swallowOutput) {
134+
System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out)));
135+
}
136+
}
137+
}
138+
}

librootjava/src/main/java/eu/chainfire/librootjava/Reflection.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,45 @@ static void sendBroadcast(Intent intent) {
211211
throw new RuntimeException("librootjava: unable to send broadcast");
212212
}
213213

214+
/**
215+
* Determine if debugging is enabled on the VM level<br>
216+
* <br>
217+
* Stability: unlikely to change, this implementation works from 1.6 through 9.0<br>
218+
*
219+
* @see Debugger#isEnabled()
220+
*
221+
* @return Debugging enabled
222+
*/
223+
@SuppressLint("PrivateApi")
224+
static boolean isDebuggingEnabled() {
225+
try {
226+
Class<?> cVMDebug = Class.forName("dalvik.system.VMDebug");
227+
Method mIsDebuggingEnabled = cVMDebug.getDeclaredMethod("isDebuggingEnabled");
228+
return (Boolean)mIsDebuggingEnabled.invoke(null);
229+
} catch (Exception e) {
230+
Logger.ex(e);
231+
return false;
232+
}
233+
}
234+
235+
/**
236+
* Set app name for debugger connection
237+
* <br>
238+
* Stability: unlikely to change, this implementation works from 1.6 through 9.0<br>
239+
*
240+
* @see Debugger#setName(String)
241+
*/
242+
@SuppressLint("PrivateApi")
243+
static void setAppName(String name) {
244+
try {
245+
Class<?> cDdmHandleAppName = Class.forName("android.ddm.DdmHandleAppName");
246+
Method m = cDdmHandleAppName.getDeclaredMethod("setAppName", String.class, int.class);
247+
m.invoke(null, name, 0);
248+
} catch (Exception e) {
249+
Logger.ex(e);
250+
}
251+
}
252+
214253
/**
215254
* Internal class to retrieve an interface from a Binder (Proxy)
216255
*

librootjava/src/main/java/eu/chainfire/librootjava/RootJava.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
* @see #getSystemContext()
5353
* @see #getPackageContext(String)
5454
* @see #getLibraryPath(Context, String)
55+
* @see Debugger#setEnabled(boolean)
5556
*/
5657
@SuppressWarnings({"unused", "WeakerAccess", "Convert2Diamond"})
5758
public class RootJava {
@@ -182,10 +183,15 @@ public static String getLaunchString(String packageCodePath, String clazz, Strin
182183
if (niceName != null) {
183184
extraParams += " --nice-name=" + niceName;
184185
}
185-
//TODO debugging; the next line is successful in creating a jdwp server on Pie, and it's listed in 'adb jdwp', but connecting to it seemingly does nothing
186-
//vmParams += " -XjdwpProvider:adbconnection -XjdwpOptions:transport=dt_android_adb,suspend=n,server=y";
187-
//vmParams += " -agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y";
188-
// android.os.Debug.waitForDebugger(); can be used in root's Main
186+
if (Debugger.enabled) { // we don't use isEnabled() because that has a different meaning when called as root, and though rare we might call this method from root too
187+
vmParams += " -Xcompiler-option --debuggable";
188+
if (Build.VERSION.SDK_INT >= 28) {
189+
// Android 9.0 Pie changed things up a bit
190+
vmParams += " -XjdwpProvider:internal -XjdwpOptions:transport=dt_android_adb,suspend=n,server=y";
191+
} else {
192+
vmParams += " -agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y";
193+
}
194+
}
189195
String ret = String.format("NO_ADDR_COMPAT_LAYOUT_FIXUP=1 %sCLASSPATH=%s %s%s /system/bin%s %s", prefix.toString(), packageCodePath, app_process, vmParams, extraParams, clazz);
190196
if (params != null) {
191197
StringBuilder full = new StringBuilder(ret);

librootjava_example/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ dependencies {
3535
//implementation project(':librootjava')
3636

3737
/* Use local Maven repository version, installed by installMavenLocal Gradle task */
38-
//implementation('eu.chainfire:librootjava:1.0.0-SNAPSHOT') { changing = true }
38+
//implementation('eu.chainfire:librootjava:1.1.0-SNAPSHOT') { changing = true }
3939

4040
/* Use bintray/jcenter version */
41-
implementation 'eu.chainfire:librootjava:1.0.0'
41+
implementation 'eu.chainfire:librootjava:1.1.0'
4242
}

librootjavadaemon/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ on the Android site.
140140
## Gradle
141141

142142
```
143-
implementation 'eu.chainfire:librootjavadaemon:1.0.0'
143+
implementation 'eu.chainfire:librootjavadaemon:1.1.0'
144144
```
145145

146146
You should update to the latest libRootJava and libRootJavaDaemon at the

0 commit comments

Comments
 (0)