ksud: backup stock image and use it when restore (#1619)

This commit is contained in:
5ec1cff
2024-04-14 00:45:06 +08:00
committed by GitHub
parent 1be266b6f6
commit 60dd52afd1
8 changed files with 243 additions and 63 deletions

View File

@@ -12,9 +12,21 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
@@ -37,9 +49,17 @@ import me.weishu.kernelsu.ui.util.LocalSnackbarHost
import me.weishu.kernelsu.ui.util.installBoot
import me.weishu.kernelsu.ui.util.installModule
import me.weishu.kernelsu.ui.util.reboot
import me.weishu.kernelsu.ui.util.restoreBoot
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
enum class FlashingStatus {
FLASHING,
SUCCESS,
FAILED
}
/**
* @author weishu
@@ -57,19 +77,24 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
val snackBarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
var flashing by rememberSaveable {
mutableStateOf(FlashingStatus.FLASHING)
}
LaunchedEffect(Unit) {
if (text.isNotEmpty()) {
return@LaunchedEffect
}
withContext(Dispatchers.IO) {
flashIt(flashIt, onFinish = { showReboot ->
flashIt(flashIt, onFinish = { showReboot, code ->
if (code != 0) {
text += "Error: exit code = $code.\nPlease save and check the log.\n"
}
if (showReboot) {
for (i in 0..2) {
text += "\n"
}
text += "\n\n\n"
showFloatAction = true
}
flashing = if (code == 0) FlashingStatus.SUCCESS else FlashingStatus.FAILED
}, onStdout = {
text += "$it\n"
logContent.append(it).append("\n")
@@ -82,6 +107,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
Scaffold(
topBar = {
TopBar(
flashing,
onBack = {
navigator.popBackStack()
},
@@ -146,10 +172,12 @@ sealed class FlashIt : Parcelable {
FlashIt()
data class FlashModule(val uri: Uri) : FlashIt()
data object FlashRestore : FlashIt()
}
fun flashIt(
flashIt: FlashIt, onFinish: (Boolean) -> Unit,
flashIt: FlashIt, onFinish: (Boolean, Int) -> Unit,
onStdout: (String) -> Unit,
onStderr: (String) -> Unit
) {
@@ -164,14 +192,26 @@ fun flashIt(
)
is FlashIt.FlashModule -> installModule(flashIt.uri, onFinish, onStdout, onStderr)
FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
private fun TopBar(status: FlashingStatus, onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
TopAppBar(
title = { Text(stringResource(R.string.install)) },
title = {
Text(
stringResource(
when (status) {
FlashingStatus.FLASHING -> R.string.flashing
FlashingStatus.SUCCESS -> R.string.flash_success
FlashingStatus.FAILED -> R.string.flash_failed
}
)
)
},
navigationIcon = {
IconButton(
onClick = onBack

View File

@@ -123,11 +123,19 @@ fun InstallScreen(navigator: DestinationsNavigator) {
installMethod = method
}
Row(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
(lkmSelection as? LkmSelection.LkmUri)?.let {
Text(
stringResource(
id = R.string.selected_lkm,
it.uri.lastPathSegment ?: "(file)"
)
)
}
Button(modifier = Modifier.fillMaxWidth(),
enabled = installMethod != null,
onClick = {

View File

@@ -63,6 +63,7 @@ import me.weishu.kernelsu.ui.component.rememberConfirmDialog
import me.weishu.kernelsu.ui.component.rememberCustomDialog
import me.weishu.kernelsu.ui.component.rememberLoadingDialog
import me.weishu.kernelsu.ui.screen.destinations.AppProfileTemplateScreenDestination
import me.weishu.kernelsu.ui.screen.destinations.FlashScreenDestination
import me.weishu.kernelsu.ui.util.getBugreportFile
import me.weishu.kernelsu.ui.util.shrinkModules
@@ -214,7 +215,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
if (lkmMode) {
UninstallItem {
UninstallItem(navigator) {
loadingDialog.withLoading(it)
}
}
@@ -237,7 +238,10 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
@Composable
fun UninstallItem(withLoading: suspend (suspend () -> Unit) -> Unit) {
fun UninstallItem(
navigator: DestinationsNavigator,
withLoading: suspend (suspend () -> Unit) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val uninstallConfirmDialog = rememberConfirmDialog()
@@ -255,7 +259,9 @@ fun UninstallItem(withLoading: suspend (suspend () -> Unit) -> Unit) {
when (uninstallType) {
UninstallType.TEMPORARY -> showTodo()
UninstallType.PERMANENT -> showTodo()
UninstallType.RESTORE_STOCK_IMAGE -> showTodo()
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
FlashScreenDestination(FlashIt.FlashRestore)
)
UninstallType.NONE -> Unit
}
}

View File

@@ -106,7 +106,10 @@ fun uninstallModule(id: String): Boolean {
}
fun installModule(
uri: Uri, onFinish: (Boolean) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
uri: Uri,
onFinish: (Boolean, Int) -> Unit,
onStdout: (String) -> Unit,
onStderr: (String) -> Unit
): Boolean {
val resolver = ksuApp.contentResolver
with(resolver.openInputStream(uri)) {
@@ -137,11 +140,38 @@ fun installModule(
file.delete()
onFinish(result.isSuccess)
onFinish(result.isSuccess, result.code)
return result.isSuccess
}
}
fun restoreBoot(
onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
): Boolean {
val shell = createRootShell()
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so")
val stdoutCallback: CallbackList<String?> = object : CallbackList<String?>() {
override fun onAddElement(s: String?) {
onStdout(s ?: "")
}
}
val stderrCallback: CallbackList<String?> = object : CallbackList<String?>() {
override fun onAddElement(s: String?) {
onStderr(s ?: "")
}
}
val result =
shell.newJob().add("${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot")
.to(stdoutCallback, stderrCallback)
.exec()
onFinish(result.isSuccess, result.code)
return result.isSuccess
}
suspend fun shrinkModules(): Boolean = withContext(Dispatchers.IO) {
execKsud("module shrink", true)
}
@@ -157,7 +187,7 @@ fun installBoot(
bootUri: Uri?,
lkm: LkmSelection,
ota: Boolean,
onFinish: (Boolean) -> Unit,
onFinish: (Boolean, Int) -> Unit,
onStdout: (String) -> Unit,
onStderr: (String) -> Unit,
): Boolean {
@@ -238,7 +268,7 @@ fun installBoot(
lkmFile?.delete()
// if boot uri is empty, it is direct install, when success, we should show reboot button
onFinish(bootUri == null && result.isSuccess)
onFinish(bootUri == null && result.isSuccess, result.code)
return result.isSuccess
}

View File

@@ -124,4 +124,8 @@
<string name="settings_uninstall_temporary_message">临时卸载 KernelSU下次重启后恢复</string>
<string name="settings_uninstall_permanent_message">完全并永久移除 KernelSU 和所有模块</string>
<string name="settings_restore_stock_image_message">恢复原厂镜像,一般在 OTA 前使用;如需卸载请使用“永久卸载”</string>
<string name="flashing">刷写中</string>
<string name="flash_success">刷写完成</string>
<string name="flash_failed">刷写失败</string>
<string name="selected_lkm">选择的 LKM %s</string>
</resources>

View File

@@ -126,4 +126,8 @@
<string name="settings_uninstall_temporary_message">Temporarily uninstall KernelSU, restore to original state after next reboot.</string>
<string name="settings_uninstall_permanent_message">Uninstalling KernelSU(Root and all modules) completely and permanently.</string>
<string name="settings_restore_stock_image_message">Restore the stock factory image (if a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Permanent Uninstall\".</string>
<string name="flashing">Flashing</string>
<string name="flash_success">Flash success</string>
<string name="flash_failed">Flash failed</string>
<string name="selected_lkm">Selected lkm: %s</string>
</resources>

View File

@@ -1,17 +1,18 @@
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::ensure;
use anyhow::Context;
use anyhow::Result;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;
use which::which;
use crate::defs::{KSU_BACKUP_DIR, KSU_BACKUP_FILE_PREFIX};
use crate::{assets, utils};
#[cfg(target_os = "android")]
@@ -152,8 +153,7 @@ pub fn restore(
magiskboot_path: Option<PathBuf>,
flash: bool,
) -> Result<()> {
let workding_dir =
tempdir::TempDir::new("KernelSU").with_context(|| "create temp dir failed")?;
let workding_dir = tempdir::TempDir::new("KernelSU").context("create temp dir failed")?;
let magiskboot = find_magiskboot(magiskboot_path, workding_dir.path())?;
let (bootimage, bootdevice) = find_boot_image(&image, false, false, workding_dir.path())?;
@@ -171,29 +171,57 @@ pub fn restore(
let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workding_dir.path())?;
ensure!(is_kernelsu_patched, "boot image is not patched by KernelSU");
// remove kernelsu.ko
do_cpio_cmd(&magiskboot, workding_dir.path(), "rm kernelsu.ko")?;
let mut new_boot = None;
let mut from_backup = false;
// if init.real is exist, restore it
let status = do_cpio_cmd(&magiskboot, workding_dir.path(), "exists init.real").is_ok();
if status {
do_cpio_cmd(&magiskboot, workding_dir.path(), "mv init.real init")?;
#[cfg(target_os = "android")]
if do_cpio_cmd(&magiskboot, workding_dir.path(), "exists orig.ksu").is_ok() {
do_cpio_cmd(
&magiskboot,
workding_dir.path(),
"extract orig.ksu orig.ksu",
)?;
let sha = std::fs::read(workding_dir.path().join("orig.ksu"))?;
let sha = String::from_utf8(sha)?;
let sha = sha.trim();
let backup_path = format!("{KSU_BACKUP_DIR}/{KSU_BACKUP_FILE_PREFIX}{sha}");
if Path::new(&backup_path).is_file() {
new_boot = Some(PathBuf::from(backup_path));
from_backup = true;
} else {
println!("Warning: no backup {KSU_BACKUP_DIR}/{KSU_BACKUP_FILE_PREFIX}{sha} found!");
}
} else {
let ramdisk = workding_dir.path().join("ramdisk.cpio");
std::fs::remove_file(ramdisk)?;
println!("Warning: no backup found!");
}
println!("- Repacking boot image");
let status = Command::new(&magiskboot)
.current_dir(workding_dir.path())
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("repack")
.arg(bootimage.display().to_string())
.status()?;
ensure!(status.success(), "magiskboot repack failed");
if new_boot.is_none() {
// remove kernelsu.ko
do_cpio_cmd(&magiskboot, workding_dir.path(), "rm kernelsu.ko")?;
// if init.real exists, restore it
let status = do_cpio_cmd(&magiskboot, workding_dir.path(), "exists init.real").is_ok();
if status {
do_cpio_cmd(&magiskboot, workding_dir.path(), "mv init.real init")?;
} else {
let ramdisk = workding_dir.path().join("ramdisk.cpio");
std::fs::remove_file(ramdisk)?;
}
println!("- Repacking boot image");
let status = Command::new(&magiskboot)
.current_dir(workding_dir.path())
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("repack")
.arg(bootimage.display().to_string())
.status()?;
ensure!(status.success(), "magiskboot repack failed");
new_boot = Some(workding_dir.path().join("new-boot.img"));
}
let new_boot = new_boot.unwrap();
let new_boot = workding_dir.path().join("new-boot.img");
if image.is_some() {
// if image is specified, write to output file
let output_dir = std::env::current_dir()?;
@@ -203,16 +231,19 @@ pub fn restore(
now.format("%Y%m%d_%H%M%S")
));
if std::fs::rename(&new_boot, &output_image).is_err() {
std::fs::copy(&new_boot, &output_image)
.with_context(|| "copy out new boot failed".to_string())?;
if from_backup || std::fs::rename(&new_boot, &output_image).is_err() {
std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?;
}
println!("- Output file is written to");
println!("- {}", output_image.display().to_string().trim_matches('"'));
}
if flash {
println!("- Flashing new boot image");
flash_boot(bootdevice, new_boot)?;
if from_backup {
println!("- Flashing new boot image from {}", new_boot.display());
} else {
println!("- Flashing new boot image");
}
flash_boot(&bootdevice, new_boot)?;
}
println!("- Done!");
Ok(())
@@ -265,12 +296,13 @@ fn do_patch(
);
}
let workding_dir =
tempdir::TempDir::new("KernelSU").with_context(|| "create temp dir failed")?;
let workding_dir = tempdir::TempDir::new("KernelSU").context("create temp dir failed")?;
let (bootimage, bootdevice) =
find_boot_image(&image, ota, is_replace_kernel, workding_dir.path())?;
let bootimage = bootimage.display().to_string();
// try extract magiskboot/bootctl
let _ = assets::ensure_binaries(false);
@@ -279,20 +311,20 @@ fn do_patch(
if let Some(kernel) = kernel {
std::fs::copy(kernel, workding_dir.path().join("kernel"))
.with_context(|| "copy kernel from failed".to_string())?;
.context("copy kernel from failed")?;
}
println!("- Preparing assets");
let kmod_file = workding_dir.path().join("kernelsu.ko");
if let Some(kmod) = kmod {
std::fs::copy(kmod, kmod_file).with_context(|| "copy kernel module failed".to_string())?;
std::fs::copy(kmod, kmod_file).context("copy kernel module failed")?;
} else {
// If kmod is not specified, extract from assets
let kmi = if let Some(kmi) = kmi {
kmi
} else {
get_current_kmi().with_context(|| "Unknown KMI, please choose LKM manually")?
get_current_kmi().context("Unknown KMI, please choose LKM manually")?
};
println!("- KMI: {kmi}");
let name = format!("{kmi}_kernelsu.ko");
@@ -302,9 +334,9 @@ fn do_patch(
let init_file = workding_dir.path().join("init");
if let Some(init) = init {
std::fs::copy(init, init_file).with_context(|| "copy init failed".to_string())?;
std::fs::copy(init, init_file).context("copy init failed")?;
} else {
assets::copy_assets_to_file("ksuinit", init_file).with_context(|| "copy ksuinit failed")?;
assets::copy_assets_to_file("ksuinit", init_file).context("copy ksuinit failed")?;
}
// magiskboot unpack boot.img
@@ -318,7 +350,7 @@ fn do_patch(
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("unpack")
.arg(bootimage.display().to_string())
.arg(&bootimage)
.status()?;
ensure!(status.success(), "magiskboot unpack failed");
@@ -331,12 +363,45 @@ fn do_patch(
println!("- Adding KernelSU LKM");
let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workding_dir.path())?;
#[cfg(target_os = "android")]
let mut backup = None;
if !is_kernelsu_patched {
// kernelsu.ko is not exist, backup init if necessary
let status = do_cpio_cmd(&magiskboot, workding_dir.path(), "exists init");
if status.is_ok() {
do_cpio_cmd(&magiskboot, workding_dir.path(), "mv init init.real")?;
}
println!("- Backup stock boot image");
// magiskboot cpio ramdisk.cpio 'add 0755 orig.ksu'
let output = Command::new(&magiskboot)
.current_dir(workding_dir.path())
.arg("sha1")
.arg(&bootimage)
.output()?;
ensure!(
output.status.success(),
"Cannot calculate sha1 of original boot!"
);
#[cfg(target_os = "android")]
{
let output = String::from_utf8(output.stdout)?;
let output = output.trim();
let output = format!("{KSU_BACKUP_FILE_PREFIX}{output}");
let target = format!("{KSU_BACKUP_DIR}/{output}");
std::fs::copy(&bootimage, &target).with_context(|| format!("backup to {target}"))?;
std::fs::write(workding_dir.path().join("orig.ksu"), output.as_bytes())
.context("write sha1")?;
do_cpio_cmd(
&magiskboot,
workding_dir.path(),
"add 0755 orig.ksu orig.ksu",
)?;
println!("- Stock image has been backup to");
println!("- {target}");
backup = Some(output);
}
}
do_cpio_cmd(&magiskboot, workding_dir.path(), "add 0755 init init")?;
@@ -353,7 +418,7 @@ fn do_patch(
.stdout(Stdio::null())
.stderr(Stdio::null())
.arg("repack")
.arg(bootimage.display().to_string())
.arg(&bootimage)
.status()?;
ensure!(status.success(), "magiskboot repack failed");
let new_boot = workding_dir.path().join("new-boot.img");
@@ -368,8 +433,7 @@ fn do_patch(
));
if std::fs::rename(&new_boot, &output_image).is_err() {
std::fs::copy(&new_boot, &output_image)
.with_context(|| "copy out new boot failed".to_string())?;
std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?;
}
println!("- Output file is written to");
println!("- {}", output_image.display().to_string().trim_matches('"'));
@@ -377,27 +441,48 @@ fn do_patch(
if flash {
println!("- Flashing new boot image");
flash_boot(bootdevice, new_boot)?;
flash_boot(&bootdevice, new_boot)?;
if ota {
post_ota()?;
}
}
#[cfg(target_os = "android")]
if let Some(backup) = backup {
println!("- Clean up backup");
if let Ok(dir) = std::fs::read_dir("/data") {
for entry in dir.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(name) = path.file_name() {
let name = name.to_string_lossy().to_string();
if name != backup
&& name.starts_with(KSU_BACKUP_FILE_PREFIX)
&& std::fs::remove_file(path).is_ok()
{
println!("- removed {name}");
}
}
}
}
}
}
println!("- Done!");
Ok(())
}
fn flash_boot(bootdevice: Option<String>, new_boot: PathBuf) -> Result<()> {
fn flash_boot(bootdevice: &Option<String>, new_boot: PathBuf) -> Result<()> {
let Some(bootdevice) = bootdevice else {
bail!("boot device not found")
};
let status = Command::new("blockdev")
.arg("--setrw")
.arg(&bootdevice)
.arg(bootdevice)
.status()?;
ensure!(status.success(), "set boot device rw failed");
dd(new_boot, &bootdevice).with_context(|| "flash boot failed")?;
dd(new_boot, bootdevice).context("flash boot failed")?;
Ok(())
}
@@ -413,7 +498,7 @@ fn find_magiskboot(magiskboot_path: Option<PathBuf>, workding_dir: &Path) -> Res
} else {
let magiskboot_path = workding_dir.join("magiskboot");
assets::copy_assets_to_file("magiskboot", &magiskboot_path)
.with_context(|| "copy magiskboot failed")?;
.context("copy magiskboot failed")?;
magiskboot_path
};
ensure!(magiskboot.exists(), "{magiskboot:?} is not exist");

View File

@@ -38,3 +38,6 @@ pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
pub const VERSION_CODE: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_CODE"));
pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_NAME"));
pub const KSU_BACKUP_DIR: &str = "/data";
pub const KSU_BACKUP_FILE_PREFIX: &str = "ksu_backup_";