Skip to content

Commit

Permalink
feat: Added root-only adb runner (tested on emulator)
Browse files Browse the repository at this point in the history
I spent almost an entire day on this, you better be happy!
  • Loading branch information
Sculas committed Apr 15, 2022
1 parent c9941fe commit 37ecc5e
Show file tree
Hide file tree
Showing 8 changed files with 487 additions and 19 deletions.
10 changes: 8 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ plugins {
}

group = "app.revanced"
version = "1.0"

repositories {
mavenCentral()
Expand All @@ -16,6 +15,9 @@ repositories {
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") // DO NOT CHANGE!
}
}
maven {
url = uri("https://jitpack.io")
}
}

val patchesDependency = "app.revanced:revanced-patches:1.0.0-dev.4"
Expand All @@ -29,8 +31,12 @@ dependencies {

implementation("com.google.code.gson:gson:2.9.0")
implementation("me.tongfei:progressbar:0.9.3")
implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead.
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
}

val cliMainClass = "app.revanced.cli.Main"

tasks {
build {
dependsOn(shadowJar)
Expand All @@ -42,7 +48,7 @@ tasks {
exclude(dependency(patchesDependency))
}
manifest {
attributes("Main-Class" to "app.revanced.cli.Main")
attributes("Main-Class" to cliMainClass)
attributes("Implementation-Title" to project.name)
attributes("Implementation-Version" to project.version)
}
Expand Down
3 changes: 2 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
kotlin.code.style=official
kotlin.code.style=official
version = 1.0.0-dev
68 changes: 52 additions & 16 deletions src/main/kotlin/app/revanced/cli/Main.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app.revanced.cli

import app.revanced.cli.runner.Emulator
import app.revanced.cli.utils.PatchLoader
import app.revanced.cli.utils.Patches
import app.revanced.cli.utils.Preconditions
Expand All @@ -8,6 +9,7 @@ import app.revanced.patcher.patch.PatchMetadata
import app.revanced.patcher.patch.PatchResult
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.default
import kotlinx.cli.required
import me.tongfei.progressbar.ProgressBarBuilder
import me.tongfei.progressbar.ProgressBarStyle
Expand All @@ -23,7 +25,9 @@ class Main {
inApk: String,
inPatches: String,
inIntegrations: String?,
inOutput: String,
inOutput: String?,
inEmulate: String?,
hideResults: Boolean,
) {
val bar = ProgressBarBuilder()
.setTaskName("Working..")
Expand All @@ -35,12 +39,9 @@ class Main {
.setExtraMessage("Initializing")
val apk = Preconditions.isFile(inApk)
val patchesFile = Preconditions.isFile(inPatches)
val output = Preconditions.isDirectory(inOutput)
bar.step()

val patcher = Patcher(
apk,
)
val patcher = Patcher(apk)

inIntegrations?.let {
bar.reset().maxHint(1)
Expand All @@ -53,12 +54,14 @@ class Main {
bar.reset().maxHint(1)
.extraMessage = "Loading patches"
PatchLoader.injectPatches(patchesFile)
bar.step()

val patches = Patches.loadPatches().map { it() }
patcher.addPatches(patches)
bar.step()

bar.reset().maxHint(1)
.extraMessage = "Resolving signatures"
patcher.resolveSignatures()
bar.step()

val amount = patches.size.toLong()
bar.reset().maxHint(amount)
Expand All @@ -70,24 +73,41 @@ class Main {
bar.reset().maxHint(-1)
.extraMessage = "Generating dex files"
val dexFiles = patcher.save()
bar.reset().maxHint(dexFiles.size.toLong())
.extraMessage = "Saving dex files"
dexFiles.forEach { (dexName, dexData) ->
Files.write(File(output, dexName).toPath(), dexData.data)
bar.step()

inOutput?.let {
val output = Preconditions.isDirectory(it)
val amount = dexFiles.size.toLong()
bar.reset().maxHint(amount)
.extraMessage = "Saving dex files"
dexFiles.forEach { (dexName, dexData) ->
Files.write(File(output, dexName).toPath(), dexData.data)
bar.step()
}
bar.stepTo(amount)
}

bar.close()

inEmulate?.let { device ->
Emulator.emulate(
apk,
dexFiles,
device
)
}

println("All done!")
printResults(results)
if (!hideResults) {
printResults(results)
}
}

private fun printResults(results: Map<PatchMetadata, Result<PatchResult>>) {
for ((metadata, result) in results) {
if (result.isSuccess) {
println("${metadata.name} was applied successfully!")
println("${metadata.shortName} was applied successfully!")
} else {
println("${metadata.name} failed to apply! Cause:")
println("${metadata.shortName} failed to apply! Cause:")
result.exceptionOrNull()!!.printStackTrace()
}
}
Expand All @@ -98,6 +118,9 @@ class Main {
println("$CLI_NAME version $CLI_VERSION")
val parser = ArgParser(CLI_NAME)

// TODO: add some kind of incremental building, so merging integrations can be skipped.
// this can be achieved manually, but doing it automatically is better.

val apk by parser.option(
ArgType.String,
fullName = "apk",
Expand All @@ -121,14 +144,27 @@ class Main {
fullName = "output",
shortName = "o",
description = "Output directory"
).required()
)
val emulate by parser.option(
ArgType.String,
fullName = "run-on",
description = "After the CLI is done building, which ADB device should it run on?"
)
// TODO: package name
val hideResults by parser.option(
ArgType.Boolean,
fullName = "hide-results",
description = "Don't print the patch results."
).default(false)

parser.parse(args)
runCLI(
apk,
patches,
integrations,
output,
emulate,
hideResults,
)
}
}
Expand Down
140 changes: 140 additions & 0 deletions src/main/kotlin/app/revanced/cli/runner/AdbRunner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package app.revanced.cli.runner

import app.revanced.cli.utils.DexReplacer
import app.revanced.cli.utils.Scripts
import app.revanced.cli.utils.signer.Signer
import me.tongfei.progressbar.ProgressBar
import me.tongfei.progressbar.ProgressBarBuilder
import me.tongfei.progressbar.ProgressBarStyle
import org.jf.dexlib2.writer.io.MemoryDataStore
import se.vidstige.jadb.JadbConnection
import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.RemoteFile
import se.vidstige.jadb.ShellProcessBuilder
import java.io.File
import java.nio.file.Files
import java.util.concurrent.Executors

object Emulator {
fun emulate(
apk: File,
dexFiles: Map<String, MemoryDataStore>,
deviceName: String
) {
lateinit var dvc: JadbDevice
pbar("Initializing").use { bar ->
dvc = JadbConnection().findDevice(deviceName)
?: throw IllegalArgumentException("No such device with name $deviceName")
if (!dvc.hasSu())
throw IllegalArgumentException("Device $deviceName is not rooted or does not have su")
bar.step()
}

lateinit var tmpFile: File // we need this file at the end to clean up.
pbar("Generating APK file", 3).use { bar ->
bar.step().extraMessage = "Creating APK file"
tmpFile = Files.createTempFile("rvc-cli", ".apk").toFile()
apk.copyTo(tmpFile, true)

bar.step().extraMessage = "Replacing dex files"
DexReplacer.replaceDex(tmpFile, dexFiles)

bar.step().extraMessage = "Signing APK file"
Signer.signApk(tmpFile)
}

pbar("Running application", 6, false).use { bar ->
bar.step().extraMessage = "Pushing mount scripts"
dvc.push(Scripts.MOUNT_SCRIPT, RemoteFile(Scripts.SCRIPT_PATH))
dvc.cmd(Scripts.CREATE_DIR_COMMAND).assertZero()
dvc.cmd(Scripts.MV_MOUNT_COMMAND).assertZero()
dvc.cmd(Scripts.CHMOD_MOUNT_COMMAND).assertZero()

bar.step().extraMessage = "Pushing APK file"
dvc.push(tmpFile, RemoteFile(Scripts.APK_PATH))

bar.step().extraMessage = "Mounting APK file"
dvc.cmd(Scripts.STOP_APP_COMMAND).startAndWait()
dvc.cmd(Scripts.START_MOUNT_COMMAND).assertZero()

bar.step().extraMessage = "Starting APK file"
dvc.cmd(Scripts.START_APP_COMMAND).assertZero()

bar.step().setExtraMessage("Debugging APK file").refresh()
println("\nWaiting until app is closed.")
val executor = Executors.newSingleThreadExecutor()
val p = dvc.cmd(Scripts.LOGCAT_COMMAND)
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
.redirectError(ProcessBuilder.Redirect.INHERIT)
.useExecutor(executor)
.start()
Thread.sleep(250) // give the app some time to start up.
while (dvc.cmd(Scripts.PIDOF_APP_COMMAND).startAndWait() == 0) {
Thread.sleep(250)
}
println("App closed, continuing.")
p.destroy()
executor.shutdown()

bar.step().extraMessage = "Unmounting APK file"
var exitCode: Int
do {
exitCode = dvc.cmd(Scripts.UNMOUNT_COMMAND).startAndWait()
} while (exitCode != 0)
}

tmpFile.delete()
}
}

private fun JadbDevice.push(s: String, remoteFile: RemoteFile) =
this.push(s.byteInputStream(), System.currentTimeMillis(), 644, remoteFile)

private fun JadbConnection.findDevice(device: String): JadbDevice? {
return devices.find { it.serial == device }
}

private fun JadbDevice.cmd(s: String): ShellProcessBuilder {
val args = s.split(" ") as ArrayList<String>
val cmd = args.removeFirst()
return shellProcessBuilder(cmd, *args.toTypedArray())
}

private fun JadbDevice.hasSu(): Boolean {
return cmd("su -h").startAndWait() == 0
}

private fun ShellProcessBuilder.startAndWait(): Int {
return start().waitFor()
}

private fun ShellProcessBuilder.assertZero() {
if (startAndWait() != 0) {
val cmd = getcmd()
throw IllegalStateException("ADB returned non-zero status code for command: $cmd")
}
}

private fun pbar(task: String, steps: Long = 1, update: Boolean = true): ProgressBar {
val b = ProgressBarBuilder().setTaskName(task)
if (update) b
.setUpdateIntervalMillis(250)
.continuousUpdate()
return b
.setStyle(ProgressBarStyle.ASCII)
.build()
.maxHint(steps + 1)
}

private fun ProgressBar.use(block: (ProgressBar) -> Unit) {
block(this)
stepTo(max) // step to 100%
extraMessage = "" // clear extra message
close()
}

private fun ShellProcessBuilder.getcmd(): String {
val f = this::class.java.getDeclaredField("command")
f.isAccessible = true
return f.get(this) as String
}
31 changes: 31 additions & 0 deletions src/main/kotlin/app/revanced/cli/utils/DexReplacer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package app.revanced.cli.utils

import lanchon.multidexlib2.BasicDexFileNamer
import org.jf.dexlib2.writer.io.MemoryDataStore
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files

val NAMER = BasicDexFileNamer()

object DexReplacer {
fun replaceDex(source: File, dexFiles: Map<String, MemoryDataStore>) {
FileSystems.newFileSystem(
source.toPath(),
null
).use { fs ->
// Delete all classes?.dex files
Files.walk(fs.rootDirectories.first()).forEach {
if (
it.toString().endsWith(".dex") &&
NAMER.isValidName(it.fileName.toString())
) Files.delete(it)
}
// Write new dex files
dexFiles
.forEach { (dexName, dexData) ->
Files.write(fs.getPath("/$dexName"), dexData.data)
}
}
}
}
Loading

0 comments on commit 37ecc5e

Please sign in to comment.