diff --git a/build.gradle.kts b/build.gradle.kts index 25815c9..1b22dc1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,6 +27,9 @@ dependencies { implementation("app.revanced:revanced-patcher:+") implementation(patchesDependency) implementation("info.picocli:picocli:+") + + implementation("me.tongfei:progressbar:+") + implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead. implementation("org.bouncycastle:bcpkix-jdk15on:+") } diff --git a/src/main/kotlin/app/revanced/cli/MainCommand.kt b/src/main/kotlin/app/revanced/cli/MainCommand.kt index 0abfe89..29afa52 100644 --- a/src/main/kotlin/app/revanced/cli/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/MainCommand.kt @@ -2,6 +2,7 @@ package app.revanced.cli import app.revanced.patch.PatchLoader import app.revanced.patch.Patches +import app.revanced.utils.adb.Adb import picocli.CommandLine.* import java.io.File @@ -9,24 +10,26 @@ import java.io.File name = "ReVanced-CLI", version = ["1.0.0"], mixinStandardHelpOptions = true ) internal object MainCommand : Runnable { - @Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"]) - internal var patchBundles = arrayOf() - @Parameters( paramLabel = "INCLUDE", description = ["Which patches to include. If none is specified, all compatible patches will be included"] ) internal var includedPatches = arrayOf() - @Option(names = ["-c", "--cache"], description = ["Output resource cache directory"], required = true) + @Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"]) + internal var patchBundles = arrayOf() + + @Option(names = ["-t", "--temp-dir"], description = ["Temporal resource cache directory"], required = true) internal lateinit var cacheDirectory: String @Option(names = ["-r", "--resource-patcher"], description = ["Enable patching resources"]) internal var patchResources: Boolean = false - @Option(names = ["-w", "--wipe-after"], description = ["Wipe the temporal directory before exiting the patcher"]) - internal var wipe: Boolean = false - + @Option( + names = ["-c", "--clean"], + description = ["Clean the temporal resource cache directory. This will be done anyways when running the patcher"] + ) + internal var clean: Boolean = false @Option(names = ["-l", "--list"], description = ["List patches only"]) internal var listOnly: Boolean = false @@ -40,6 +43,9 @@ internal object MainCommand : Runnable { @Option(names = ["-o", "--out"], description = ["Output file path"], required = true) internal lateinit var outputPath: String + @Option(names = ["-d", "--deploy-on"], description = ["If specified, deploy to adb device with given name"]) + internal var deploy: String? = null + override fun run() { if (listOnly) { patchBundles.forEach { @@ -51,9 +57,28 @@ internal object MainCommand : Runnable { return } - Patcher.run() + val patcher = app.revanced.patcher.Patcher( + inputFile, + cacheDirectory, + patchResources + ) - if (!wipe) return - File(cacheDirectory).deleteRecursively() + Patcher.start(patcher) + + if (clean) { + File(cacheDirectory).deleteRecursively() + } + + val outputFile = File(outputPath) + + deploy?.let { + Adb( + outputFile, + patcher.packageName, + deploy!! + ).deploy() + } + + if (clean) outputFile.delete() } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/Patcher.kt b/src/main/kotlin/app/revanced/cli/Patcher.kt index 26c78ce..548ef1c 100644 --- a/src/main/kotlin/app/revanced/cli/Patcher.kt +++ b/src/main/kotlin/app/revanced/cli/Patcher.kt @@ -10,13 +10,7 @@ import java.io.File internal class Patcher { internal companion object { - internal fun run() { - val patcher = app.revanced.patcher.Patcher( - MainCommand.inputFile, - MainCommand.cacheDirectory, - MainCommand.patchResources - ) - + internal fun start(patcher: app.revanced.patcher.Patcher) { // merge files like necessary integrations patcher.addFiles(MainCommand.mergeFiles) // add patches, but filter incompatible or excluded patches diff --git a/src/main/kotlin/app/revanced/utils/Scripts.kt b/src/main/kotlin/app/revanced/utils/Scripts.kt deleted file mode 100644 index 31043fb..0000000 --- a/src/main/kotlin/app/revanced/utils/Scripts.kt +++ /dev/null @@ -1,35 +0,0 @@ -package app.revanced.utils - -// TODO: make this a class with PACKAGE_NAME as argument, then use that everywhere. -// make sure to remove the "const" from all the vals, they won't compile obviously. -internal object Scripts { - private const val PACKAGE_NAME = "com.google.android.apps.youtube.music" - private const val DATA_PATH = "/data/adb/ReVanced" - internal const val APK_PATH = "/sdcard/base.apk" - internal const val SCRIPT_PATH = "/sdcard/mount.sh" - - internal val MOUNT_SCRIPT = - """ - base_path="$DATA_PATH/base.apk" - stock_path=${'$'}{ pm path $PACKAGE_NAME | grep base | sed 's/package://g' } - umount -l ${'$'}stock_path - rm ${'$'}base_path - mv "$APK_PATH" ${'$'}base_path - chmod 644 ${'$'}base_path - chown system:system ${'$'}base_path - chcon u:object_r:apk_data_file:s0 ${'$'}base_path - mount -o bind ${'$'}base_path ${'$'}stock_path - """.trimIndent() - - internal const val PIDOF_APP_COMMAND = "pidof -s $PACKAGE_NAME" - private const val PIDOF_APP = "\$($PIDOF_APP_COMMAND)" - internal const val CREATE_DIR_COMMAND = "su -c \"mkdir -p $DATA_PATH/\"" - internal const val MV_MOUNT_COMMAND = "su -c \"mv /sdcard/mount.sh $DATA_PATH/\"" - internal const val CHMOD_MOUNT_COMMAND = "su -c \"chmod +x $DATA_PATH/mount.sh\"" - internal const val START_MOUNT_COMMAND = "su -c $DATA_PATH/mount.sh" - internal const val UNMOUNT_COMMAND = - "su -c \"umount -l $(pm path $PACKAGE_NAME | grep base | sed 's/package://g')\"" - internal const val LOGCAT_COMMAND = "su -c \"logcat -c && logcat --pid=$PIDOF_APP\"" - internal const val STOP_APP_COMMAND = "su -c \"kill $PIDOF_APP\"" - internal const val START_APP_COMMAND = "monkey -p $PACKAGE_NAME 1" -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/Adb.kt b/src/main/kotlin/app/revanced/utils/adb/Adb.kt new file mode 100644 index 0000000..c03db28 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/Adb.kt @@ -0,0 +1,94 @@ +package app.revanced.utils.adb + +import se.vidstige.jadb.JadbConnection +import se.vidstige.jadb.JadbDevice +import java.io.File +import java.util.concurrent.Executors + +internal class Adb( + private val apk: File, + private val packageName: String, + deviceName: String, + private val logging: Boolean = true +) { + private val device: JadbDevice + + init { + device = JadbConnection().devices.find { it.serial == deviceName } + ?: throw IllegalArgumentException("No such device with name $deviceName") + + if (device.run("su -h", false) != 0) + throw IllegalArgumentException("Root required on $deviceName. Deploying failed.") + } + + private fun String.replacePlaceholder(): String { + return this.replace(Constants.PLACEHOLDER, packageName) + } + + internal fun deploy() { + // create revanced path + device.run("${Constants.COMMAND_CREATE_DIR} ${Constants.PATH_REVANCED}") + + // push patched file + device.copy(Constants.PATH_INIT_PUSH, apk) + // install apk + device.run(Constants.COMMAND_INSTALL_APK.replacePlaceholder()) + + // push mount script + device.createFile( + Constants.PATH_INIT_PUSH, + Constants.CONTENT_MOUNT_SCRIPT.replacePlaceholder() + ) + // install mount script + device.run(Constants.COMMAND_INSTALL_MOUNT.replacePlaceholder()) + + // push umount script + device.createFile( + Constants.PATH_INIT_PUSH, + Constants.CONTENT_UMOUNT_SCRIPT.replacePlaceholder() + ) + // install mount script + device.run(Constants.COMMAND_INSTALL_UMOUNT.replacePlaceholder()) + + // unmount the apk for sanity + device.run(Constants.PATH_UMOUNT.replacePlaceholder()) + // mount the apk + device.run(Constants.PATH_MOUNT.replacePlaceholder()) + + // relaunch app + device.run(Constants.COMMAND_RESTART.replacePlaceholder()) + + // log the app + log() + } + + private fun log() { + val executor = Executors.newSingleThreadExecutor() + val pipe = if (logging) { + ProcessBuilder.Redirect.INHERIT + } else { + ProcessBuilder.Redirect.PIPE + } + + val process = device.buildCommand(Constants.COMMAND_LOGCAT.replacePlaceholder()) + .redirectOutput(pipe) + .redirectError(pipe) + .useExecutor(executor) + .start() + + Thread.sleep(500) // give the app some time to start up. + while (true) { + try { + while (device.run("${Constants.COMMAND_PID_OF} $packageName") == 0) { + Thread.sleep(1000) + } + break + } catch (e: Exception) { + throw RuntimeException("An error occurred while monitoring state of app", e) + } + } + println("App closed, continuing.") + process.destroy() + executor.shutdown() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/Commands.kt b/src/main/kotlin/app/revanced/utils/adb/Commands.kt new file mode 100644 index 0000000..1b3af07 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/Commands.kt @@ -0,0 +1,29 @@ +package app.revanced.utils.adb + +import se.vidstige.jadb.JadbDevice +import se.vidstige.jadb.RemoteFile +import se.vidstige.jadb.ShellProcessBuilder +import java.io.File + +internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder { + if (su) { + return shellProcessBuilder("su -c \'$command\'") + } + + val args = command.split(" ") as ArrayList + val cmd = args.removeFirst() + + return shellProcessBuilder(cmd, *args.toTypedArray()) +} + +internal fun JadbDevice.run(command: String, su: Boolean = true): Int { + return this.buildCommand(command, su).start().waitFor() +} + +internal fun JadbDevice.copy(targetPath: String, file: File) { + push(file, RemoteFile(targetPath)) +} + +internal fun JadbDevice.createFile(targetFile: String, content: String) { + push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile)) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/Constants.kt b/src/main/kotlin/app/revanced/utils/adb/Constants.kt new file mode 100644 index 0000000..32f7bf1 --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/Constants.kt @@ -0,0 +1,57 @@ +package app.revanced.utils.adb + +internal object Constants { + // template placeholder to replace a string in commands + internal const val PLACEHOLDER = "TEMPLATE_PACKAGE_NAME" + + // utility commands + private const val COMMAND_CHMOD_MOUNT = "chmod +x" + internal const val COMMAND_PID_OF = "pidof -s" + internal const val COMMAND_CREATE_DIR = "mkdir -p" + internal const val COMMAND_LOGCAT = "logcat -c && logcat --pid=$($COMMAND_PID_OF $PLACEHOLDER)" + internal const val COMMAND_RESTART = "monkey -p $PLACEHOLDER 1 && kill ${'$'}($COMMAND_PID_OF $PLACEHOLDER)" + + // default mount file name + private const val NAME_MOUNT_SCRIPT = "mount_$PLACEHOLDER.sh" + + // initial directory to push files to via adb push + internal const val PATH_INIT_PUSH = "/sdcard/revanced.delete" + + // revanced path + internal const val PATH_REVANCED = "/data/adb/revanced/" + + // revanced apk path + private const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk" + + // (un)mount script paths + internal const val PATH_MOUNT = "/data/adb/service.d/$NAME_MOUNT_SCRIPT" + internal const val PATH_UMOUNT = "/data/adb/post-fs-data.d/un$NAME_MOUNT_SCRIPT" + + // move to revanced apk path & set permissions + internal const val COMMAND_INSTALL_APK = + "base_path=\"$PATH_REVANCED_APP\" && mv $PATH_INIT_PUSH ${'$'}base_path && chmod 644 ${'$'}base_path && chown system:system ${'$'}base_path && chcon u:object_r:apk_data_file:s0 ${'$'}base_path" + + // install mount script & set permissions + internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && $COMMAND_CHMOD_MOUNT $PATH_MOUNT" + + // install umount script & set permissions + internal const val COMMAND_INSTALL_UMOUNT = "mv $PATH_INIT_PUSH $PATH_UMOUNT && $COMMAND_CHMOD_MOUNT $PATH_UMOUNT" + + // unmount script + internal val CONTENT_UMOUNT_SCRIPT = + """ + #!/system/bin/sh + while read line; do echo ${'$'}{line} | grep $PLACEHOLDER | awk '{print ${'$'}2}' | xargs umount -l; done< /proc/mounts + """.trimIndent() + + // mount script + internal val CONTENT_MOUNT_SCRIPT = + """ + #!/system/bin/sh + while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done + + base_path="$PATH_REVANCED_APP" + stock_path=${'$'}{ pm path $PLACEHOLDER | grep base | sed 's/package://g' } + mount -o bind ${'$'}base_path ${'$'}stock_path + """.trimIndent() +} \ No newline at end of file