| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -6,6 +6,7 @@ Run Java (and Kotlin) code as root! | |||
| 6 | 6 | - Access to all the classes in your projects | |
| 7 | 7 | - Access to Android classes | |
| 8 | 8 | - Easy Binder-based IPC/RPC | |
| 9 | + - Debugging support | ||
| 9 | 10 | ||
| 10 | 11 | ## License | |
| 11 | 12 | ||
@@ -69,14 +70,6 @@ While this library was originally built to support Android 4.2+ devices, | |||
| 69 | 70 | it only officially supports 5.0+. The first public GitHub release was | |
| 70 | 71 | tested specifically on 5.0, 7.0, 8.0, and 9.0. | |
| 71 | 72 | ||
| 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 | - | ||
| 80 | 73 | ## Recommended reading | |
| 81 | 74 | ||
| 82 | 75 | I strongly recommend you read the library's source code in its entirety | |
@@ -270,6 +263,84 @@ non-root code. | |||
| 270 | 263 | (See the [example project](../librootjava_example) for a more elaborate | |
| 271 | 264 | example for this entire process) | |
| 272 | 265 | ||
| 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 | + | ||
| 273 | 344 | #### Daemonizing | |
| 274 | 345 | ||
| 275 | 346 | 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! | |||
| 348 | 419 | ## Gradle | |
| 349 | 420 | ||
| 350 | 421 | ``` | |
| 351 | - implementation 'eu.chainfire:librootjava:1.0.0' | ||
| 422 | + implementation 'eu.chainfire:librootjava:1.1.0' | ||
| 352 | 423 | ``` | |
| 353 | 424 | ||
| 354 | 425 | ## Notes | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -51,7 +51,7 @@ ext { | |||
| 51 | 51 | gitUrl = 'https://github.com/Chainfire/librootjava.git' | |
| 52 | 52 | issueTrackerUrl = 'https://github.com/Chainfire/librootjava/issues' | |
| 53 | 53 | ||
| 54 | - libraryVersion = '1.0.0' | ||
| 54 | + libraryVersion = '1.1.0' | ||
| 55 | 55 | ||
| 56 | 56 | developerId = 'Chainfire' | |
| 57 | 57 | developerName = 'Jorrit Jongma' | |
@@ -72,7 +72,7 @@ task installMavenLocal(type: Upload) { | |||
| 72 | 72 | packaging 'aar' | |
| 73 | 73 | groupId = publishedGroupId | |
| 74 | 74 | artifactId = artifact | |
| 75 | - version = '1.0.0-SNAPSHOT' | ||
| 75 | + version = libraryVersion + '-SNAPSHOT' | ||
| 76 | 76 | } | |
| 77 | 77 | } | |
| 78 | 78 | } | |
| Original file line number | Diff line number | Diff 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->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 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -211,6 +211,45 @@ static void sendBroadcast(Intent intent) { | |||
| 211 | 211 | throw new RuntimeException("librootjava: unable to send broadcast"); | |
| 212 | 212 | } | |
| 213 | 213 | ||
| 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 | + | ||
| 214 | 253 | /** | |
| 215 | 254 | * Internal class to retrieve an interface from a Binder (Proxy) | |
| 216 | 255 | * | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -52,6 +52,7 @@ | |||
| 52 | 52 | * @see #getSystemContext() | |
| 53 | 53 | * @see #getPackageContext(String) | |
| 54 | 54 | * @see #getLibraryPath(Context, String) | |
| 55 | + * @see Debugger#setEnabled(boolean) | ||
| 55 | 56 | */ | |
| 56 | 57 | @SuppressWarnings({"unused", "WeakerAccess", "Convert2Diamond"}) | |
| 57 | 58 | public class RootJava { | |
@@ -182,10 +183,15 @@ public static String getLaunchString(String packageCodePath, String clazz, Strin | |||
| 182 | 183 | if (niceName != null) { | |
| 183 | 184 | extraParams += " --nice-name=" + niceName; | |
| 184 | 185 | } | |
| 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 | + } | ||
| 189 | 195 | 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); | |
| 190 | 196 | if (params != null) { | |
| 191 | 197 | StringBuilder full = new StringBuilder(ret); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -35,8 +35,8 @@ dependencies { | |||
| 35 | 35 | //implementation project(':librootjava') | |
| 36 | 36 | ||
| 37 | 37 | /* 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 } | ||
| 39 | 39 | ||
| 40 | 40 | /* Use bintray/jcenter version */ | |
| 41 | - implementation 'eu.chainfire:librootjava:1.0.0' | ||
| 41 | + implementation 'eu.chainfire:librootjava:1.1.0' | ||
| 42 | 42 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -140,7 +140,7 @@ on the Android site. | |||
| 140 | 140 | ## Gradle | |
| 141 | 141 | ||
| 142 | 142 | ``` | |
| 143 | - implementation 'eu.chainfire:librootjavadaemon:1.0.0' | ||
| 143 | + implementation 'eu.chainfire:librootjavadaemon:1.1.0' | ||
| 144 | 144 | ``` | |
| 145 | 145 | ||
| 146 | 146 | You should update to the latest libRootJava and libRootJavaDaemon at the | |
| Back | FazBrowse Home | New Git URL |
0 commit comments