Make all I/O suspendable

This commit is contained in:
topjohnwu 2024-03-12 03:24:42 -07:00
parent 21d374214f
commit 050a073771
10 changed files with 85 additions and 65 deletions

View File

@ -67,8 +67,6 @@ public final class APKInstall {
public interface Session { public interface Session {
// @WorkerThread // @WorkerThread
OutputStream openStream(Context context) throws IOException; OutputStream openStream(Context context) throws IOException;
// @WorkerThread
void install(Context context, File apk) throws IOException;
// @WorkerThread @Nullable // @WorkerThread @Nullable
Intent waitIntent(); Intent waitIntent();
} }
@ -167,13 +165,5 @@ public final class APKInstall {
} }
}; };
} }
@Override
public void install(Context context, File apk) throws IOException {
try (var src = new FileInputStream(apk);
var out = openStream(context)) {
transfer(src, out);
}
}
} }
} }

View File

@ -12,8 +12,11 @@ import com.topjohnwu.magisk.core.repository.DBConfig
import com.topjohnwu.magisk.core.repository.PreferenceConfig import com.topjohnwu.magisk.core.repository.PreferenceConfig
import com.topjohnwu.magisk.core.utils.refreshLocale import com.topjohnwu.magisk.core.utils.refreshLocale
import com.topjohnwu.magisk.ui.theme.Theme import com.topjohnwu.magisk.ui.theme.Theme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.runBlocking
import java.io.File import java.io.File
import java.io.IOException
object Config : PreferenceConfig, DBConfig { object Config : PreferenceConfig, DBConfig {
@ -171,8 +174,14 @@ object Config : PreferenceConfig, DBConfig {
fun load(pkg: String?) { fun load(pkg: String?) {
// Only try to load prefs when fresh install and a previous package name is set // Only try to load prefs when fresh install and a previous package name is set
if (pkg != null && prefs.all.isEmpty()) runCatching { if (pkg != null && prefs.all.isEmpty()) {
context.contentResolver.openInputStream(Provider.preferencesUri(pkg))?.writeTo(prefsFile) runBlocking {
try {
context.contentResolver
.openInputStream(Provider.preferencesUri(pkg))
?.writeTo(prefsFile, dispatcher = Dispatchers.Unconfined)
} catch (ignored: IOException) {}
}
return return
} }

View File

@ -28,6 +28,7 @@ import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.intent import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.copyAndClose import com.topjohnwu.magisk.core.ktx.copyAndClose
import com.topjohnwu.magisk.core.ktx.copyAll
import com.topjohnwu.magisk.core.ktx.forEach import com.topjohnwu.magisk.core.ktx.forEach
import com.topjohnwu.magisk.core.ktx.selfLaunchIntent import com.topjohnwu.magisk.core.ktx.selfLaunchIntent
import com.topjohnwu.magisk.core.ktx.set import com.topjohnwu.magisk.core.ktx.set
@ -178,7 +179,7 @@ class DownloadEngine(
notifyFinish(subject) notifyFinish(subject)
} }
subject.postDownload?.invoke() subject.postDownload?.invoke()
} catch (e: Exception) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
notifyFail(subject) notifyFail(subject)
} }
@ -269,8 +270,8 @@ class DownloadEngine(
return n return n
} }
private fun handleApp(stream: InputStream, subject: Subject.App) { private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
fun writeTee(output: OutputStream) { suspend fun writeTee(output: OutputStream) {
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
val external = uri.outputStream() val external = uri.outputStream()
stream.copyAndClose(TeeOutputStream(external, output)) stream.copyAndClose(TeeOutputStream(external, output))
@ -326,7 +327,7 @@ class DownloadEngine(
} }
} }
private fun handleModule(src: InputStream, file: Uri) { private suspend fun handleModule(src: InputStream, file: Uri) {
val input = ZipInputStream(src.buffered()) val input = ZipInputStream(src.buffered())
val output = ZipOutputStream(file.outputStream().buffered()) val output = ZipOutputStream(file.outputStream().buffered())
@ -336,7 +337,7 @@ class DownloadEngine(
zout.putNextEntry(ZipEntry("META-INF/com/google/")) zout.putNextEntry(ZipEntry("META-INF/com/google/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/")) zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary")) zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
context.assets.open("module_installer.sh").copyTo(zout) context.assets.open("module_installer.sh").copyAll(zout)
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script")) zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray()) zout.write("#MAGISK\n".toByteArray())
@ -346,7 +347,7 @@ class DownloadEngine(
if (path.isNotEmpty() && !path.startsWith("META-INF")) { if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path)) zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) { if (!entry.isDirectory) {
zin.copyTo(zout) zin.copyAll(zout)
} }
} }
} }

View File

@ -2,10 +2,15 @@ package com.topjohnwu.magisk.core.ktx
import androidx.collection.SparseArrayCompat import androidx.collection.SparseArrayCompat
import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.core.utils.currentLocale
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.lang.reflect.Field import java.lang.reflect.Field
@ -35,9 +40,38 @@ inline fun <In : InputStream, Out : OutputStream> withStreams(
} }
} }
fun InputStream.copyAndClose(out: OutputStream) = withStreams(this, out) { i, o -> i.copyTo(o) } @Throws(IOException::class)
suspend fun InputStream.copyAll(
out: OutputStream,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): Long {
return withContext(dispatcher) {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = read(buffer)
while (isActive && bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = read(buffer)
}
bytesCopied
}
}
fun InputStream.writeTo(file: File) = copyAndClose(file.outputStream()) @Throws(IOException::class)
suspend inline fun InputStream.copyAndClose(
out: OutputStream,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) = withStreams(this, out) { i, o -> i.copyAll(o, bufferSize, dispatcher) }
@Throws(IOException::class)
suspend inline fun InputStream.writeTo(
file: File,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) = copyAndClose(file.outputStream(), bufferSize, dispatcher)
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) { operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
put(key, value) put(key, value)

View File

@ -26,7 +26,7 @@ open class FlashZip(
private lateinit var zipFile: File private lateinit var zipFile: File
@Throws(IOException::class) @Throws(IOException::class)
private fun flash(): Boolean { private suspend fun flash(): Boolean {
installDir.deleteRecursively() installDir.deleteRecursively()
installDir.mkdirs() installDir.mkdirs()
@ -47,13 +47,13 @@ open class FlashZip(
} }
} }
val isValid = runCatching { val isValid = try {
zipFile.unzip(installDir, "META-INF/com/google/android", true) zipFile.unzip(installDir, "META-INF/com/google/android", true)
val script = File(installDir, "updater-script") val script = File(installDir, "updater-script")
script.readText().contains("#MAGISK") script.readText().contains("#MAGISK")
}.getOrElse { } catch (e: IOException) {
console.add("! Unzip error") console.add("! Unzip error")
throw it throw e
} }
if (!isValid) { if (!isValid) {

View File

@ -12,6 +12,7 @@ import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Provider import com.topjohnwu.magisk.core.Provider
import com.topjohnwu.magisk.core.ktx.await import com.topjohnwu.magisk.core.ktx.await
import com.topjohnwu.magisk.core.ktx.copyAndClose
import com.topjohnwu.magisk.core.ktx.toast import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.ktx.writeTo import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.utils.AXML import com.topjohnwu.magisk.core.utils.AXML
@ -168,7 +169,7 @@ object HideAPK {
activity.finish() activity.finish()
} }
private fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean { private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
val stub = File(activity.cacheDir, "stub.apk") val stub = File(activity.cacheDir, "stub.apk")
try { try {
activity.assets.open("stub.apk").writeTo(stub) activity.assets.open("stub.apk").writeTo(stub)
@ -195,7 +196,7 @@ object HideAPK {
if (Shell.cmd(cmd).exec().isSuccess) return true if (Shell.cmd(cmd).exec().isSuccess) return true
try { try {
session.install(activity, repack) repack.inputStream().copyAndClose(session.openStream(activity))
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
return false return false
@ -244,7 +245,7 @@ object HideAPK {
if (Shell.cmd(cmd).await().isSuccess) return if (Shell.cmd(cmd).await().isSuccess) return
val success = withContext(Dispatchers.IO) { val success = withContext(Dispatchers.IO) {
try { try {
session.install(activity, apk) apk.inputStream().copyAndClose(session.openStream(activity))
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
return@withContext false return@withContext false

View File

@ -18,6 +18,7 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.di.ServiceLocator import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.copyAndClose import com.topjohnwu.magisk.core.ktx.copyAndClose
import com.topjohnwu.magisk.core.ktx.copyAll
import com.topjohnwu.magisk.core.ktx.reboot import com.topjohnwu.magisk.core.ktx.reboot
import com.topjohnwu.magisk.core.ktx.toast import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.ktx.writeTo import com.topjohnwu.magisk.core.ktx.writeTo
@ -93,7 +94,7 @@ abstract class MagiskInstallImpl protected constructor(
return true return true
} }
private fun extractFiles(): Boolean { private suspend fun extractFiles(): Boolean {
console.add("- Device platform: ${Const.CPU_ABI}") console.add("- Device platform: ${Const.CPU_ABI}")
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
@ -174,7 +175,7 @@ abstract class MagiskInstallImpl protected constructor(
return true return true
} }
private fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyTo(it) } private suspend fun InputStream.copyAndCloseOut(out: OutputStream) = out.use { copyAll(it) }
private fun newTarEntry(name: String, size: Long): TarEntry { private fun newTarEntry(name: String, size: Long): TarEntry {
console.add("-- Writing: $name") console.add("-- Writing: $name")
@ -191,7 +192,7 @@ abstract class MagiskInstallImpl protected constructor(
private class NoBootException : IOException() private class NoBootException : IOException()
@Throws(IOException::class) @Throws(IOException::class)
private fun processTar(tarIn: TarInputStream, tarOut: TarOutputStream): ExtendedFile { private suspend fun processTar(tarIn: TarInputStream, tarOut: TarOutputStream): ExtendedFile {
console.add("- Processing tar file") console.add("- Processing tar file")
lateinit var entry: TarEntry lateinit var entry: TarEntry
@ -228,7 +229,7 @@ abstract class MagiskInstallImpl protected constructor(
} else { } else {
console.add("-- Copying: ${entry.name}") console.add("-- Copying: ${entry.name}")
tarOut.putNextEntry(entry) tarOut.putNextEntry(entry)
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024) tarIn.copyAll(tarOut, bufferSize = 1024 * 1024)
} }
} }
@ -236,10 +237,10 @@ abstract class MagiskInstallImpl protected constructor(
val initBoot = installDir.getChildFile("init_boot.img") val initBoot = installDir.getChildFile("init_boot.img")
val recovery = installDir.getChildFile("recovery.img") val recovery = installDir.getChildFile("recovery.img")
fun ExtendedFile.copyToTar() { suspend fun ExtendedFile.copyToTar() {
newInputStream().use { newInputStream().use {
tarOut.putNextEntry(newTarEntry(name, length())) tarOut.putNextEntry(newTarEntry(name, length()))
it.copyTo(tarOut) it.copyAll(tarOut)
} }
delete() delete()
} }
@ -273,7 +274,7 @@ abstract class MagiskInstallImpl protected constructor(
} }
@Throws(IOException::class) @Throws(IOException::class)
private fun processZip(zipIn: ZipInputStream): ExtendedFile { private suspend fun processZip(zipIn: ZipInputStream): ExtendedFile {
console.add("- Processing zip file") console.add("- Processing zip file")
val boot = installDir.getChildFile("boot.img") val boot = installDir.getChildFile("boot.img")
val initBoot = installDir.getChildFile("init_boot.img") val initBoot = installDir.getChildFile("init_boot.img")
@ -373,7 +374,7 @@ abstract class MagiskInstallImpl protected constructor(
} }
} }
private fun handleFile(uri: Uri): Boolean { private suspend fun handleFile(uri: Uri): Boolean {
val outStream: OutputStream val outStream: OutputStream
val outFile: MediaStoreUtils.UriFile val outFile: MediaStoreUtils.UriFile
@ -510,7 +511,7 @@ abstract class MagiskInstallImpl protected constructor(
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
private fun postOTA(): Boolean { private suspend fun postOTA(): Boolean {
try { try {
val bootctl = File.createTempFile("bootctl", null, context.cacheDir) val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
context.assets.open("bootctl").writeTo(bootctl) context.assets.open("bootctl").writeTo(bootctl)
@ -534,14 +535,14 @@ abstract class MagiskInstallImpl protected constructor(
private fun String.fsh() = ShellUtils.fastCmd(shell, this) private fun String.fsh() = ShellUtils.fastCmd(shell, this)
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this) private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
protected fun patchFile(file: Uri) = extractFiles() && handleFile(file) protected suspend fun patchFile(file: Uri) = extractFiles() && handleFile(file)
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot() protected suspend fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
protected fun secondSlot() = protected suspend fun secondSlot() =
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA() findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess protected suspend fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
protected fun uninstall() = "run_uninstaller $AppApkPath".sh().isSuccess protected fun uninstall() = "run_uninstaller $AppApkPath".sh().isSuccess

View File

@ -15,9 +15,6 @@ import com.topjohnwu.magisk.core.di.AppContext
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.OutputStream
import java.security.MessageDigest
import kotlin.experimental.and
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
object MediaStoreUtils { object MediaStoreUtils {
@ -120,24 +117,6 @@ object MediaStoreUtils {
return this.toString() return this.toString()
} }
fun Uri.checkSum(alg: String, reference: String) = runCatching {
this.inputStream().use {
val digest = MessageDigest.getInstance(alg)
it.copyTo(object : OutputStream() {
override fun write(b: Int) {
digest.update(b.toByte())
}
override fun write(b: ByteArray, off: Int, len: Int) {
digest.update(b, off, len)
}
})
val sb = StringBuilder()
digest.digest().forEach { b -> sb.append("%02x".format(b and 0xff.toByte())) }
sb.toString() == reference
}
}.getOrElse { false }
interface UriFile { interface UriFile {
val uri: Uri val uri: Uri
fun delete(): Boolean fun delete(): Boolean

View File

@ -13,6 +13,8 @@ import com.topjohnwu.magisk.core.ktx.rawResource
import com.topjohnwu.magisk.core.ktx.writeTo import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import java.io.File import java.io.File
import java.util.jar.JarFile import java.util.jar.JarFile
@ -34,7 +36,9 @@ class ShellInit : Shell.Initializer() {
val bb = jar.getJarEntry("lib/${Const.CPU_ABI}/libbusybox.so") val bb = jar.getJarEntry("lib/${Const.CPU_ABI}/libbusybox.so")
localBB = context.deviceProtectedContext.cachedFile("busybox") localBB = context.deviceProtectedContext.cachedFile("busybox")
localBB.delete() localBB.delete()
jar.getInputStream(bb).writeTo(localBB) runBlocking {
jar.getInputStream(bb).writeTo(localBB, dispatcher = Dispatchers.Unconfined)
}
localBB.setExecutable(true) localBB.setExecutable(true)
} else { } else {
localBB = File(context.applicationInfo.nativeLibraryDir, "libbusybox.so") localBB = File(context.applicationInfo.nativeLibraryDir, "libbusybox.so")

View File

@ -1,5 +1,6 @@
package com.topjohnwu.magisk.core.utils package com.topjohnwu.magisk.core.utils
import com.topjohnwu.magisk.core.ktx.copyAll
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -7,14 +8,14 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@Throws(IOException::class) @Throws(IOException::class)
fun File.unzip(folder: File, path: String = "", junkPath: Boolean = false) { suspend fun File.unzip(folder: File, path: String = "", junkPath: Boolean = false) {
inputStream().buffered().use { inputStream().buffered().use {
it.unzip(folder, path, junkPath) it.unzip(folder, path, junkPath)
} }
} }
@Throws(IOException::class) @Throws(IOException::class)
fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) { suspend fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
try { try {
val zin = ZipInputStream(this) val zin = ZipInputStream(this)
var entry: ZipEntry var entry: ZipEntry
@ -34,7 +35,7 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
if (!it.exists()) if (!it.exists())
it.mkdirs() it.mkdirs()
} }
dest.outputStream().use { out -> zin.copyTo(out) } dest.outputStream().use { out -> zin.copyAll(out) }
} }
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
throw IOException(e) throw IOException(e)