support metamodule, remove built-in overlayfs mount
Co-authored-by: weishu <twsxtd@gmail.com> Co-authored-by: YuKongA <70465933+YuKongA@users.noreply.github.com> Co-authored-by: Ylarod <me@ylarod.cn>
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -1,15 +1,13 @@
|
||||
use anyhow::{Ok, Result};
|
||||
use clap::Parser;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
use android_logger::Config;
|
||||
#[cfg(target_os = "android")]
|
||||
use log::LevelFilter;
|
||||
|
||||
use crate::{
|
||||
apk_sign, assets, debug, defs, defs::KSUD_VERBOSE_LOG_FILE, init_event, ksucalls, module, utils,
|
||||
};
|
||||
use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils};
|
||||
|
||||
/// KernelSU userspace cli
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -17,9 +15,6 @@ use crate::{
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
|
||||
#[arg(short, long, default_value_t = cfg!(debug_assertions))]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
@@ -209,8 +204,6 @@ enum Debug {
|
||||
/// Get kernel version
|
||||
Version,
|
||||
|
||||
Mount,
|
||||
|
||||
/// For testing
|
||||
Test,
|
||||
|
||||
@@ -277,14 +270,14 @@ enum Module {
|
||||
zip: String,
|
||||
},
|
||||
|
||||
/// Uninstall module <id>
|
||||
Uninstall {
|
||||
/// Undo module uninstall mark <id>
|
||||
UndoUninstall {
|
||||
/// module id
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Restore module <id>
|
||||
Restore {
|
||||
/// Uninstall module <id>
|
||||
Uninstall {
|
||||
/// module id
|
||||
id: String,
|
||||
},
|
||||
@@ -498,10 +491,6 @@ pub fn run() -> Result<()> {
|
||||
|
||||
let cli = Args::parse();
|
||||
|
||||
if !cli.verbose && !Path::new(KSUD_VERBOSE_LOG_FILE).exists() {
|
||||
log::set_max_level(LevelFilter::Info);
|
||||
}
|
||||
|
||||
log::info!("command: {:?}", cli.command);
|
||||
|
||||
let result = match cli.command {
|
||||
@@ -515,8 +504,8 @@ pub fn run() -> Result<()> {
|
||||
}
|
||||
match command {
|
||||
Module::Install { zip } => module::install_module(&zip),
|
||||
Module::UndoUninstall { id } => module::undo_uninstall_module(&id),
|
||||
Module::Uninstall { id } => module::uninstall_module(&id),
|
||||
Module::Restore { id } => module::restore_uninstall_module(&id),
|
||||
Module::Enable { id } => module::enable_module(&id),
|
||||
Module::Disable { id } => module::disable_module(&id),
|
||||
Module::Action { id } => module::run_action(&id),
|
||||
@@ -563,7 +552,6 @@ pub fn run() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Debug::Su { global_mnt } => crate::su::grant_root(global_mnt),
|
||||
Debug::Mount => init_event::mount_modules_systemlessly(),
|
||||
Debug::Test => assets::ensure_binaries(false),
|
||||
Debug::Mark { command } => match command {
|
||||
MarkCommand::Get { pid } => debug::mark_get(pid),
|
||||
@@ -672,11 +660,7 @@ pub fn run() -> Result<()> {
|
||||
};
|
||||
|
||||
if let Err(e) = &result {
|
||||
for c in e.chain() {
|
||||
log::error!("{c:#?}");
|
||||
}
|
||||
|
||||
log::error!("{:#?}", e.backtrace());
|
||||
log::error!("Error: {e:?}");
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ pub const PROFILE_SELINUX_DIR: &str = concatcp!(PROFILE_DIR, "selinux/");
|
||||
pub const PROFILE_TEMPLATE_DIR: &str = concatcp!(PROFILE_DIR, "templates/");
|
||||
|
||||
pub const KSURC_PATH: &str = concatcp!(WORKING_DIR, ".ksurc");
|
||||
pub const KSU_MOUNT_SOURCE: &str = "KSU";
|
||||
pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, "ksud");
|
||||
pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot");
|
||||
|
||||
@@ -18,18 +17,19 @@ pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot");
|
||||
pub const DAEMON_LINK_PATH: &str = concatcp!(BINARY_DIR, "ksud");
|
||||
|
||||
pub const MODULE_DIR: &str = concatcp!(ADB_DIR, "modules/");
|
||||
|
||||
// warning: this directory should not change, or you need to change the code in module_installer.sh!!!
|
||||
pub const MODULE_UPDATE_DIR: &str = concatcp!(ADB_DIR, "modules_update/");
|
||||
|
||||
pub const KSUD_VERBOSE_LOG_FILE: &str = concatcp!(ADB_DIR, "verbose");
|
||||
pub const METAMODULE_DIR: &str = concatcp!(ADB_DIR, "metamodule/");
|
||||
|
||||
pub const MODULE_WEB_DIR: &str = "webroot";
|
||||
pub const MODULE_ACTION_SH: &str = "action.sh";
|
||||
pub const DISABLE_FILE_NAME: &str = "disable";
|
||||
pub const UPDATE_FILE_NAME: &str = "update";
|
||||
pub const REMOVE_FILE_NAME: &str = "remove";
|
||||
pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
|
||||
|
||||
// Metamodule support
|
||||
pub const METAMODULE_MOUNT_SCRIPT: &str = "metamount.sh";
|
||||
pub const METAMODULE_METAINSTALL_SCRIPT: &str = "metainstall.sh";
|
||||
pub const METAMODULE_METAUNINSTALL_SCRIPT: &str = "metauninstall.sh";
|
||||
|
||||
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"));
|
||||
@@ -37,6 +37,3 @@ pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_N
|
||||
pub const KSU_BACKUP_DIR: &str = WORKING_DIR;
|
||||
pub const KSU_BACKUP_FILE_PREFIX: &str = "ksu_backup_";
|
||||
pub const BACKUP_FILENAME: &str = "stock_image.sha1";
|
||||
|
||||
pub const NO_TMPFS_PATH: &str = concatcp!(WORKING_DIR, ".notmpfs");
|
||||
pub const NO_MOUNT_PATH: &str = concatcp!(WORKING_DIR, ".nomount");
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
use crate::kpm;
|
||||
use crate::utils::is_safe_mode;
|
||||
use crate::{
|
||||
assets, defs,
|
||||
defs::{KSU_MOUNT_SOURCE, NO_MOUNT_PATH, NO_TMPFS_PATH},
|
||||
ksucalls,
|
||||
module::{handle_updated_modules, prune_modules},
|
||||
restorecon, uid_scanner, utils,
|
||||
utils::find_tmp_path,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use log::{info, warn};
|
||||
use rustix::fs::{MountFlags, mount};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn mount_modules_systemlessly() -> Result<()> {
|
||||
crate::magic_mount::magic_mount(&find_tmp_path())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn mount_modules_systemlessly() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
use crate::kpm;
|
||||
use crate::module::{handle_updated_modules, prune_modules};
|
||||
use crate::utils::is_safe_mode;
|
||||
use crate::{
|
||||
assets, defs, ksucalls, metamodule, restorecon,
|
||||
utils::{self},
|
||||
};
|
||||
|
||||
pub fn on_post_data_fs() -> Result<()> {
|
||||
ksucalls::report_post_fs_data();
|
||||
@@ -39,9 +25,11 @@ pub fn on_post_data_fs() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let safe_mode = utils::is_safe_mode();
|
||||
let safe_mode = crate::utils::is_safe_mode();
|
||||
|
||||
if safe_mode {
|
||||
// we should still ensure module directory exists in safe mode
|
||||
// because we may need to operate the module dir in safe mode
|
||||
warn!("safe mode, skip common post-fs-data.d scripts");
|
||||
} else {
|
||||
// Then exec common post-fs-data scripts
|
||||
@@ -50,18 +38,18 @@ pub fn on_post_data_fs() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let module_dir = defs::MODULE_DIR;
|
||||
|
||||
assets::ensure_binaries(true).with_context(|| "Failed to extract bin assets")?;
|
||||
|
||||
// Start UID scanner daemon with highest priority
|
||||
uid_scanner::start_uid_scanner_daemon()?;
|
||||
crate::uid_scanner::uid_scanner::start_uid_scanner_daemon()?;
|
||||
|
||||
if is_safe_mode() {
|
||||
warn!("safe mode, skip load feature config");
|
||||
} else if let Err(e) = crate::umount_manager::load_and_apply_config() {
|
||||
warn!("Failed to load umount config: {e}");
|
||||
}
|
||||
// tell kernel that we've mount the module, so that it can do some optimization
|
||||
ksucalls::report_module_mounted();
|
||||
|
||||
// if we are in safe mode, we should disable all modules
|
||||
if safe_mode {
|
||||
@@ -72,14 +60,14 @@ pub fn on_post_data_fs() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(e) = prune_modules() {
|
||||
warn!("prune modules failed: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = handle_updated_modules() {
|
||||
warn!("handle updated modules failed: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = prune_modules() {
|
||||
warn!("prune modules failed: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = restorecon::restorecon() {
|
||||
warn!("restorecon failed: {e}");
|
||||
}
|
||||
@@ -110,23 +98,9 @@ pub fn on_post_data_fs() -> Result<()> {
|
||||
warn!("KPM: Failed to load KPM modules: {e}");
|
||||
}
|
||||
|
||||
let tmpfs_path = find_tmp_path();
|
||||
// for compatibility
|
||||
let no_mount = Path::new(NO_TMPFS_PATH).exists() || Path::new(NO_MOUNT_PATH).exists();
|
||||
|
||||
// mount temp dir
|
||||
if !no_mount {
|
||||
if let Err(e) = mount(
|
||||
KSU_MOUNT_SOURCE,
|
||||
&tmpfs_path,
|
||||
"tmpfs",
|
||||
MountFlags::empty(),
|
||||
"",
|
||||
) {
|
||||
warn!("do temp dir mount failed: {e}");
|
||||
}
|
||||
} else {
|
||||
info!("no tmpfs requested");
|
||||
// execute metamodule post-fs-data script first (priority)
|
||||
if let Err(e) = metamodule::exec_stage_script("post-fs-data", true) {
|
||||
warn!("exec metamodule post-fs-data script failed: {e}");
|
||||
}
|
||||
|
||||
// exec modules post-fs-data scripts
|
||||
@@ -140,18 +114,15 @@ pub fn on_post_data_fs() -> Result<()> {
|
||||
warn!("load system.prop failed: {e}");
|
||||
}
|
||||
|
||||
// mount module systemlessly by magic mount
|
||||
#[cfg(target_os = "android")]
|
||||
if !no_mount {
|
||||
if let Err(e) = crate::magic_mount::magic_mount(&tmpfs_path) {
|
||||
warn!("do systemless mount failed: {e}");
|
||||
}
|
||||
} else {
|
||||
info!("no mount requested");
|
||||
// execute metamodule mount script
|
||||
if let Err(e) = metamodule::exec_mount_script(module_dir) {
|
||||
warn!("execute metamodule mount failed: {e}");
|
||||
}
|
||||
|
||||
run_stage("post-mount", true);
|
||||
|
||||
std::env::set_current_dir("/").with_context(|| "failed to chdir to /")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -171,6 +142,13 @@ fn run_stage(stage: &str, block: bool) {
|
||||
if let Err(e) = crate::module::exec_common_scripts(&format!("{stage}.d"), block) {
|
||||
warn!("Failed to exec common {stage} scripts: {e}");
|
||||
}
|
||||
|
||||
// execute metamodule stage script first (priority)
|
||||
if let Err(e) = metamodule::exec_stage_script(stage, block) {
|
||||
warn!("Failed to exec metamodule {stage} script: {e}");
|
||||
}
|
||||
|
||||
// execute regular modules stage scripts
|
||||
if let Err(e) = crate::module::exec_stage_script(stage, block) {
|
||||
warn!("Failed to exec {stage} scripts: {e}");
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ setup_flashable() {
|
||||
$BOOTMODE && return
|
||||
if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then
|
||||
# We will have to manually find out OUTFD
|
||||
for FD in /proc/$$/fd/*; do
|
||||
for FD in `ls /proc/$$/fd`; do
|
||||
if readlink /proc/$$/fd/$FD | grep -q pipe; then
|
||||
if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then
|
||||
OUTFD=$FD
|
||||
@@ -313,14 +313,6 @@ mark_remove() {
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
mark_replace() {
|
||||
# REPLACE must be directory!!!
|
||||
# https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
|
||||
mkdir -p $1 2>/dev/null
|
||||
setfattr -n trusted.overlay.opaque -v y $1
|
||||
chmod 644 $1
|
||||
}
|
||||
|
||||
request_size_check() {
|
||||
reqSizeM=`du -ms "$1" | cut -f1`
|
||||
}
|
||||
@@ -338,16 +330,19 @@ is_legacy_script() {
|
||||
}
|
||||
|
||||
handle_partition() {
|
||||
PARTITION="$1"
|
||||
REQUIRE_SYMLINK="$2"
|
||||
if [ ! -e "$MODPATH/system/$PARTITION" ]; then
|
||||
# if /system/vendor is a symlink, we need to move it out of $MODPATH/system
|
||||
# if /system/vendor is a normal directory, no special handling is needed.
|
||||
if [ ! -e $MODPATH/system/$1 ]; then
|
||||
# no partition found
|
||||
return;
|
||||
fi
|
||||
|
||||
if [ "$REQUIRE_SYMLINK" = "false" ] || [ -L "/system/$PARTITION" ] && [ "$(readlink -f "/system/$PARTITION")" = "/$PARTITION" ]; then
|
||||
ui_print "- Handle partition /$PARTITION"
|
||||
ln -sf "./system/$PARTITION" "$MODPATH/$PARTITION"
|
||||
# we move the folder to / only if it is a native folder that is not a symlink
|
||||
if [ -d "/$1" ] && [ ! -L "/$1" ]; then
|
||||
ui_print "- Handle partition /$1"
|
||||
# we create a symlink if module want to access $MODPATH/system/$1
|
||||
# but it doesn't always work(ie. write it in post-fs-data.sh would fail because it is readonly)
|
||||
mv -f $MODPATH/system/$1 $MODPATH/$1 && ln -sf ../$1 $MODPATH/system/$1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -428,23 +423,23 @@ install_module() {
|
||||
[ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh
|
||||
fi
|
||||
|
||||
handle_partition vendor true
|
||||
handle_partition system_ext true
|
||||
handle_partition product true
|
||||
handle_partition odm false
|
||||
|
||||
# Handle replace folders
|
||||
for TARGET in $REPLACE; do
|
||||
ui_print "- Replace target: $TARGET"
|
||||
mark_replace "$MODPATH$TARGET"
|
||||
mark_replace $MODPATH$TARGET
|
||||
done
|
||||
|
||||
# Handle remove files
|
||||
for TARGET in $REMOVE; do
|
||||
ui_print "- Remove target: $TARGET"
|
||||
mark_remove "$MODPATH$TARGET"
|
||||
mark_remove $MODPATH$TARGET
|
||||
done
|
||||
|
||||
handle_partition vendor
|
||||
handle_partition system_ext
|
||||
handle_partition product
|
||||
handle_partition odm
|
||||
|
||||
if $BOOTMODE; then
|
||||
mktouch $NVBASE/modules/$MODID/update
|
||||
rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
use std::{
|
||||
cmp::PartialEq,
|
||||
collections::{HashMap, hash_map::Entry},
|
||||
fs::{self, DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link},
|
||||
os::unix::fs::{FileTypeExt, symlink},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use extattr::lgetxattr;
|
||||
use rustix::{
|
||||
fs::{
|
||||
Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, bind_mount,
|
||||
chmod, chown, mount, move_mount, remount, unmount,
|
||||
},
|
||||
mount::mount_change,
|
||||
path::Arg,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
defs::{DISABLE_FILE_NAME, KSU_MOUNT_SOURCE, MODULE_DIR, SKIP_MOUNT_FILE_NAME},
|
||||
magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout},
|
||||
restorecon::{lgetfilecon, lsetfilecon},
|
||||
utils::ensure_dir_exists,
|
||||
};
|
||||
|
||||
const REPLACE_DIR_XATTR: &str = "trusted.overlay.opaque";
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
enum NodeFileType {
|
||||
RegularFile,
|
||||
Directory,
|
||||
Symlink,
|
||||
Whiteout,
|
||||
}
|
||||
|
||||
impl NodeFileType {
|
||||
fn from_file_type(file_type: FileType) -> Option<Self> {
|
||||
if file_type.is_file() {
|
||||
Some(RegularFile)
|
||||
} else if file_type.is_dir() {
|
||||
Some(Directory)
|
||||
} else if file_type.is_symlink() {
|
||||
Some(Symlink)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Node {
|
||||
name: String,
|
||||
file_type: NodeFileType,
|
||||
children: HashMap<String, Node>,
|
||||
// the module that owned this node
|
||||
module_path: Option<PathBuf>,
|
||||
replace: bool,
|
||||
skip: bool,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn collect_module_files<P>(&mut self, module_dir: P) -> Result<bool>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let dir = module_dir.as_ref();
|
||||
let mut has_file = false;
|
||||
for entry in dir.read_dir()?.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
let node = match self.children.entry(name.clone()) {
|
||||
Entry::Occupied(o) => Some(o.into_mut()),
|
||||
Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)),
|
||||
};
|
||||
|
||||
if let Some(node) = node {
|
||||
has_file |= if node.file_type == Directory {
|
||||
node.collect_module_files(dir.join(&node.name))? || node.replace
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(has_file)
|
||||
}
|
||||
|
||||
fn new_root<T>(name: T) -> Self
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
Node {
|
||||
name: name.to_string(),
|
||||
file_type: Directory,
|
||||
children: Default::default(),
|
||||
module_path: None,
|
||||
replace: false,
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_module<T>(name: T, entry: &DirEntry) -> Option<Self>
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
if let Ok(metadata) = entry.metadata() {
|
||||
let path = entry.path();
|
||||
let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 {
|
||||
Some(Whiteout)
|
||||
} else {
|
||||
NodeFileType::from_file_type(metadata.file_type())
|
||||
};
|
||||
if let Some(file_type) = file_type {
|
||||
let mut replace = false;
|
||||
if file_type == Directory
|
||||
&& let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR)
|
||||
&& String::from_utf8_lossy(&v) == "y"
|
||||
{
|
||||
replace = true;
|
||||
}
|
||||
return Some(Node {
|
||||
name: name.to_string(),
|
||||
file_type,
|
||||
children: Default::default(),
|
||||
module_path: Some(path),
|
||||
replace,
|
||||
skip: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_module_files() -> Result<Option<Node>> {
|
||||
let mut root = Node::new_root("");
|
||||
let mut system = Node::new_root("system");
|
||||
let module_root = Path::new(MODULE_DIR);
|
||||
let mut has_file = false;
|
||||
for entry in module_root.read_dir()?.flatten() {
|
||||
if !entry.file_type()?.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if entry.path().join(DISABLE_FILE_NAME).exists()
|
||||
|| entry.path().join(SKIP_MOUNT_FILE_NAME).exists()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mod_system = entry.path().join("system");
|
||||
if !mod_system.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
log::debug!("collecting {}", entry.path().display());
|
||||
|
||||
has_file |= system.collect_module_files(&mod_system)?;
|
||||
}
|
||||
|
||||
if has_file {
|
||||
for (partition, require_symlink) in [
|
||||
("vendor", true),
|
||||
("system_ext", true),
|
||||
("product", true),
|
||||
("odm", false),
|
||||
] {
|
||||
let path_of_root = Path::new("/").join(partition);
|
||||
let path_of_system = Path::new("/system").join(partition);
|
||||
if path_of_root.is_dir() && (!require_symlink || path_of_system.is_symlink()) {
|
||||
let name = partition.to_string();
|
||||
if let Some(node) = system.children.remove(&name) {
|
||||
root.children.insert(name, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
root.children.insert("system".to_string(), system);
|
||||
Ok(Some(root))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_symlink<P>(src: P, dst: P) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let src_symlink = read_link(src.as_ref())?;
|
||||
symlink(&src_symlink, dst.as_ref())?;
|
||||
lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?;
|
||||
log::debug!(
|
||||
"clone symlink {} -> {}({})",
|
||||
dst.as_ref().display(),
|
||||
dst.as_ref().display(),
|
||||
src_symlink.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mount_mirror<P>(path: P, work_dir_path: P, entry: &DirEntry) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref().join(entry.file_name());
|
||||
let work_dir_path = work_dir_path.as_ref().join(entry.file_name());
|
||||
let file_type = entry.file_type()?;
|
||||
|
||||
if file_type.is_file() {
|
||||
log::debug!(
|
||||
"mount mirror file {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
fs::File::create(&work_dir_path)?;
|
||||
bind_mount(&path, &work_dir_path)?;
|
||||
} else if file_type.is_dir() {
|
||||
log::debug!(
|
||||
"mount mirror dir {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
create_dir(&work_dir_path)?;
|
||||
let metadata = entry.metadata()?;
|
||||
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
|
||||
unsafe {
|
||||
chown(
|
||||
&work_dir_path,
|
||||
Some(Uid::from_raw(metadata.uid())),
|
||||
Some(Gid::from_raw(metadata.gid())),
|
||||
)?;
|
||||
}
|
||||
lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?;
|
||||
for entry in read_dir(&path)?.flatten() {
|
||||
mount_mirror(&path, &work_dir_path, &entry)?;
|
||||
}
|
||||
} else if file_type.is_symlink() {
|
||||
log::debug!(
|
||||
"create mirror symlink {} -> {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
clone_symlink(&path, &work_dir_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_magic_mount<P, WP>(path: P, work_dir_path: WP, current: Node, has_tmpfs: bool) -> Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
WP: AsRef<Path>,
|
||||
{
|
||||
let mut current = current;
|
||||
let path = path.as_ref().join(¤t.name);
|
||||
let work_dir_path = work_dir_path.as_ref().join(¤t.name);
|
||||
match current.file_type {
|
||||
RegularFile => {
|
||||
let target_path = if has_tmpfs {
|
||||
fs::File::create(&work_dir_path)?;
|
||||
&work_dir_path
|
||||
} else {
|
||||
&path
|
||||
};
|
||||
if let Some(module_path) = ¤t.module_path {
|
||||
log::debug!(
|
||||
"mount module file {} -> {}",
|
||||
module_path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
bind_mount(module_path, target_path).with_context(|| {
|
||||
format!("mount module file {module_path:?} -> {work_dir_path:?}")
|
||||
})?;
|
||||
// we should use MS_REMOUNT | MS_BIND | MS_xxx to change mount flags
|
||||
if let Err(e) = remount(target_path, MountFlags::RDONLY | MountFlags::BIND, "") {
|
||||
log::warn!("make file {target_path:?} ro: {e:#?}");
|
||||
}
|
||||
} else {
|
||||
bail!("cannot mount root file {}!", path.display());
|
||||
}
|
||||
}
|
||||
Symlink => {
|
||||
if let Some(module_path) = ¤t.module_path {
|
||||
log::debug!(
|
||||
"create module symlink {} -> {}",
|
||||
module_path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
clone_symlink(module_path, &work_dir_path).with_context(|| {
|
||||
format!("create module symlink {module_path:?} -> {work_dir_path:?}")
|
||||
})?;
|
||||
} else {
|
||||
bail!("cannot mount root symlink {}!", path.display());
|
||||
}
|
||||
}
|
||||
Directory => {
|
||||
let mut create_tmpfs = !has_tmpfs && current.replace && current.module_path.is_some();
|
||||
if !has_tmpfs && !create_tmpfs {
|
||||
for it in &mut current.children {
|
||||
let (name, node) = it;
|
||||
let real_path = path.join(name);
|
||||
let need = match node.file_type {
|
||||
Symlink => true,
|
||||
Whiteout => real_path.exists(),
|
||||
_ => {
|
||||
if let Ok(metadata) = real_path.symlink_metadata() {
|
||||
let file_type = NodeFileType::from_file_type(metadata.file_type())
|
||||
.unwrap_or(Whiteout);
|
||||
file_type != node.file_type || file_type == Symlink
|
||||
} else {
|
||||
// real path not exists
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
if need {
|
||||
if current.module_path.is_none() {
|
||||
log::error!(
|
||||
"cannot create tmpfs on {}, ignore: {name}",
|
||||
path.display()
|
||||
);
|
||||
node.skip = true;
|
||||
continue;
|
||||
}
|
||||
create_tmpfs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let has_tmpfs = has_tmpfs || create_tmpfs;
|
||||
|
||||
if has_tmpfs {
|
||||
log::debug!(
|
||||
"creating tmpfs skeleton for {} at {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
create_dir_all(&work_dir_path)?;
|
||||
let (metadata, path) = if path.exists() {
|
||||
(path.metadata()?, &path)
|
||||
} else if let Some(module_path) = ¤t.module_path {
|
||||
(module_path.metadata()?, module_path)
|
||||
} else {
|
||||
bail!("cannot mount root dir {}!", path.display());
|
||||
};
|
||||
chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
|
||||
unsafe {
|
||||
chown(
|
||||
&work_dir_path,
|
||||
Some(Uid::from_raw(metadata.uid())),
|
||||
Some(Gid::from_raw(metadata.gid())),
|
||||
)?;
|
||||
}
|
||||
lsetfilecon(&work_dir_path, lgetfilecon(path)?.as_str())?;
|
||||
}
|
||||
|
||||
if create_tmpfs {
|
||||
log::debug!(
|
||||
"creating tmpfs for {} at {}",
|
||||
path.display(),
|
||||
work_dir_path.display()
|
||||
);
|
||||
bind_mount(&work_dir_path, &work_dir_path)
|
||||
.context("bind self")
|
||||
.with_context(|| format!("creating tmpfs for {path:?} at {work_dir_path:?}"))?;
|
||||
}
|
||||
|
||||
if path.exists() && !current.replace {
|
||||
for entry in path.read_dir()?.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let result = if let Some(node) = current.children.remove(&name) {
|
||||
if node.skip {
|
||||
continue;
|
||||
}
|
||||
do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
|
||||
.with_context(|| format!("magic mount {}/{name}", path.display()))
|
||||
} else if has_tmpfs {
|
||||
mount_mirror(&path, &work_dir_path, &entry)
|
||||
.with_context(|| format!("mount mirror {}/{name}", path.display()))
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
if has_tmpfs {
|
||||
return Err(e);
|
||||
} else {
|
||||
log::error!("mount child {}/{name} failed: {e:#?}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if current.replace {
|
||||
if current.module_path.is_none() {
|
||||
bail!(
|
||||
"dir {} is declared as replaced but it is root!",
|
||||
path.display()
|
||||
);
|
||||
} else {
|
||||
log::debug!("dir {} is replaced", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
for (name, node) in current.children.into_iter() {
|
||||
if node.skip {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = do_magic_mount(&path, &work_dir_path, node, has_tmpfs)
|
||||
.with_context(|| format!("magic mount {}/{name}", path.display()))
|
||||
{
|
||||
if has_tmpfs {
|
||||
return Err(e);
|
||||
} else {
|
||||
log::error!("mount child {}/{name} failed: {e:#?}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if create_tmpfs {
|
||||
log::debug!(
|
||||
"moving tmpfs {} -> {}",
|
||||
work_dir_path.display(),
|
||||
path.display()
|
||||
);
|
||||
if let Err(e) = remount(&work_dir_path, MountFlags::RDONLY | MountFlags::BIND, "") {
|
||||
log::warn!("make dir {path:?} ro: {e:#?}");
|
||||
}
|
||||
move_mount(&work_dir_path, &path)
|
||||
.context("move self")
|
||||
.with_context(|| format!("moving tmpfs {work_dir_path:?} -> {path:?}"))?;
|
||||
// make private to reduce peer group count
|
||||
if let Err(e) = mount_change(&path, MountPropagationFlags::PRIVATE) {
|
||||
log::warn!("make dir {path:?} private: {e:#?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Whiteout => {
|
||||
log::debug!("file {} is removed", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn magic_mount(tmp_path: &String) -> Result<()> {
|
||||
if let Some(root) = collect_module_files()? {
|
||||
log::debug!("collected: {:#?}", root);
|
||||
let tmp_dir = Path::new(tmp_path).join("workdir");
|
||||
ensure_dir_exists(&tmp_dir)?;
|
||||
mount(KSU_MOUNT_SOURCE, &tmp_dir, "tmpfs", MountFlags::empty(), "").context("mount tmp")?;
|
||||
mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?;
|
||||
let result = do_magic_mount("/", &tmp_dir, root, false);
|
||||
if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) {
|
||||
log::error!("failed to unmount tmp {}", e);
|
||||
}
|
||||
fs::remove_dir(tmp_dir).ok();
|
||||
result
|
||||
} else {
|
||||
log::info!("no modules to mount, skipping!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@ mod init_event;
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
mod kpm;
|
||||
mod ksucalls;
|
||||
#[cfg(target_os = "android")]
|
||||
mod magic_mount;
|
||||
mod metamodule;
|
||||
mod module;
|
||||
mod profile;
|
||||
mod restorecon;
|
||||
mod sepolicy;
|
||||
mod su;
|
||||
#[cfg(target_os = "android")]
|
||||
mod uid_scanner;
|
||||
mod umount_manager;
|
||||
mod utils;
|
||||
|
||||
287
userspace/ksud/src/metamodule.rs
Normal file
287
userspace/ksud/src/metamodule.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
//! Metamodule management
|
||||
//!
|
||||
//! This module handles all metamodule-related functionality.
|
||||
//! Metamodules are special modules that manage how regular modules are mounted
|
||||
//! and provide hooks for module installation/uninstallation.
|
||||
|
||||
use anyhow::{Context, Result, ensure};
|
||||
use log::{info, warn};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crate::module::ModuleType::All;
|
||||
use crate::{assets, defs};
|
||||
|
||||
/// Determine whether the provided module properties mark it as a metamodule
|
||||
pub fn is_metamodule(props: &HashMap<String, String>) -> bool {
|
||||
props
|
||||
.get("metamodule")
|
||||
.map(|s| {
|
||||
let trimmed = s.trim();
|
||||
trimmed == "1" || trimmed.eq_ignore_ascii_case("true")
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Get metamodule path if it exists
|
||||
/// The metamodule is stored in /data/adb/modules/{id} with a symlink at /data/adb/metamodule
|
||||
pub fn get_metamodule_path() -> Option<PathBuf> {
|
||||
let path = Path::new(defs::METAMODULE_DIR);
|
||||
|
||||
// Check if symlink exists and resolve it
|
||||
if path.is_symlink()
|
||||
&& let Ok(target) = std::fs::read_link(path)
|
||||
{
|
||||
// If target is relative, resolve it
|
||||
let resolved = if target.is_absolute() {
|
||||
target
|
||||
} else {
|
||||
path.parent()?.join(target)
|
||||
};
|
||||
|
||||
if resolved.exists() && resolved.is_dir() {
|
||||
return Some(resolved);
|
||||
} else {
|
||||
warn!(
|
||||
"Metamodule symlink points to non-existent path: {:?}",
|
||||
resolved
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: search for metamodule=1 in modules directory
|
||||
let mut result = None;
|
||||
let _ = crate::module::foreach_module(All, |module_path| {
|
||||
if let Ok(props) = crate::module::read_module_prop(module_path)
|
||||
&& is_metamodule(&props)
|
||||
{
|
||||
info!("Found metamodule in modules directory: {:?}", module_path);
|
||||
result = Some(module_path.to_path_buf());
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Check if metamodule exists
|
||||
pub fn has_metamodule() -> bool {
|
||||
get_metamodule_path().is_some()
|
||||
}
|
||||
|
||||
/// Check if it's safe to install a regular module
|
||||
/// Returns Ok(()) if safe, Err(is_disabled) if blocked
|
||||
/// - Err(true) means metamodule is disabled
|
||||
/// - Err(false) means metamodule is in other unstable state
|
||||
pub fn check_install_safety() -> Result<(), bool> {
|
||||
// No metamodule → safe
|
||||
let Some(metamodule_path) = get_metamodule_path() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// No metainstall.sh → safe (uses default installer)
|
||||
// The staged update directory may contain the latest scripts, so check both locations
|
||||
let has_metainstall = metamodule_path
|
||||
.join(defs::METAMODULE_METAINSTALL_SCRIPT)
|
||||
.exists()
|
||||
|| metamodule_path.file_name().is_some_and(|module_id| {
|
||||
Path::new(defs::MODULE_UPDATE_DIR)
|
||||
.join(module_id)
|
||||
.join(defs::METAMODULE_METAINSTALL_SCRIPT)
|
||||
.exists()
|
||||
});
|
||||
if !has_metainstall {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check for marker files
|
||||
let has_update = metamodule_path.join(defs::UPDATE_FILE_NAME).exists();
|
||||
let has_remove = metamodule_path.join(defs::REMOVE_FILE_NAME).exists();
|
||||
let has_disable = metamodule_path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
|
||||
// Stable state (no markers) → safe
|
||||
if !has_update && !has_remove && !has_disable {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Return true if disabled, false for other unstable states
|
||||
Err(has_disable && !has_update && !has_remove)
|
||||
}
|
||||
|
||||
/// Create or update the metamodule symlink
|
||||
/// Points /data/adb/metamodule -> /data/adb/modules/{module_id}
|
||||
pub(crate) fn ensure_symlink(module_path: &Path) -> Result<()> {
|
||||
// METAMODULE_DIR might have trailing slash, so we need to trim it
|
||||
let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/'));
|
||||
|
||||
info!(
|
||||
"Creating metamodule symlink: {:?} -> {:?}",
|
||||
symlink_path, module_path
|
||||
);
|
||||
|
||||
// Remove existing symlink if it exists
|
||||
if symlink_path.exists() || symlink_path.is_symlink() {
|
||||
info!("Removing old metamodule symlink/path");
|
||||
if symlink_path.is_symlink() {
|
||||
std::fs::remove_file(symlink_path).with_context(|| "Failed to remove old symlink")?;
|
||||
} else {
|
||||
// Could be a directory, remove it
|
||||
std::fs::remove_dir_all(symlink_path)
|
||||
.with_context(|| "Failed to remove old directory")?;
|
||||
}
|
||||
}
|
||||
|
||||
// Create symlink
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::symlink(module_path, symlink_path)
|
||||
.with_context(|| format!("Failed to create symlink to {:?}", module_path))?;
|
||||
|
||||
info!("Metamodule symlink created successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove the metamodule symlink
|
||||
pub(crate) fn remove_symlink() -> Result<()> {
|
||||
let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/'));
|
||||
|
||||
if symlink_path.is_symlink() {
|
||||
std::fs::remove_file(symlink_path)
|
||||
.with_context(|| "Failed to remove metamodule symlink")?;
|
||||
info!("Metamodule symlink removed");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the install script content, using metainstall.sh from metamodule if available
|
||||
/// Returns the script content to be executed
|
||||
pub(crate) fn get_install_script(
|
||||
is_metamodule: bool,
|
||||
installer_content: &str,
|
||||
install_module_script: &str,
|
||||
) -> Result<String> {
|
||||
// Check if there's a metamodule with metainstall.sh
|
||||
// Only apply this logic for regular modules (not when installing metamodule itself)
|
||||
let install_script = if !is_metamodule {
|
||||
if let Some(metamodule_path) = get_metamodule_path() {
|
||||
if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("Metamodule is disabled, using default installer");
|
||||
install_module_script.to_string()
|
||||
} else {
|
||||
let metainstall_path = metamodule_path.join(defs::METAMODULE_METAINSTALL_SCRIPT);
|
||||
|
||||
if metainstall_path.exists() {
|
||||
info!("Using metainstall.sh from metamodule");
|
||||
let metamodule_content = std::fs::read_to_string(&metainstall_path)
|
||||
.with_context(|| "Failed to read metamodule metainstall.sh")?;
|
||||
format!("{}\n{}\nexit 0\n", installer_content, metamodule_content)
|
||||
} else {
|
||||
info!("Metamodule exists but has no metainstall.sh, using default installer");
|
||||
install_module_script.to_string()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("No metamodule found, using default installer");
|
||||
install_module_script.to_string()
|
||||
}
|
||||
} else {
|
||||
info!("Installing metamodule, using default installer");
|
||||
install_module_script.to_string()
|
||||
};
|
||||
|
||||
Ok(install_script)
|
||||
}
|
||||
|
||||
/// Check if metamodule script exists and is ready to execute
|
||||
/// Returns None if metamodule doesn't exist, is disabled, or script is missing
|
||||
/// Returns Some(script_path) if script is ready to execute
|
||||
fn check_metamodule_script(script_name: &str) -> Option<PathBuf> {
|
||||
// Check if metamodule exists
|
||||
let metamodule_path = get_metamodule_path()?;
|
||||
|
||||
// Check if metamodule is disabled
|
||||
if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("Metamodule is disabled, skipping {}", script_name);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check if script exists
|
||||
let script_path = metamodule_path.join(script_name);
|
||||
if !script_path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(script_path)
|
||||
}
|
||||
|
||||
/// Execute metamodule's metauninstall.sh for a specific module
|
||||
pub(crate) fn exec_metauninstall_script(module_id: &str) -> Result<()> {
|
||||
let Some(metauninstall_path) = check_metamodule_script(defs::METAMODULE_METAUNINSTALL_SCRIPT)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!(
|
||||
"Executing metamodule metauninstall.sh for module: {}",
|
||||
module_id
|
||||
);
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", metauninstall_path.to_str().unwrap()])
|
||||
.current_dir(metauninstall_path.parent().unwrap())
|
||||
.envs(crate::module::get_common_script_envs())
|
||||
.env("MODULE_ID", module_id)
|
||||
.status()?;
|
||||
|
||||
ensure!(
|
||||
result.success(),
|
||||
"Metamodule metauninstall.sh failed for module {}: {:?}",
|
||||
module_id,
|
||||
result
|
||||
);
|
||||
|
||||
info!(
|
||||
"Metamodule metauninstall.sh executed successfully for {}",
|
||||
module_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute metamodule mount script
|
||||
pub fn exec_mount_script(module_dir: &str) -> Result<()> {
|
||||
let Some(mount_script) = check_metamodule_script(defs::METAMODULE_MOUNT_SCRIPT) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!("Executing mount script for metamodule");
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", mount_script.to_str().unwrap()])
|
||||
.envs(crate::module::get_common_script_envs())
|
||||
.env("MODULE_DIR", module_dir)
|
||||
.status()?;
|
||||
|
||||
ensure!(
|
||||
result.success(),
|
||||
"Metamodule mount script failed with status: {:?}",
|
||||
result
|
||||
);
|
||||
|
||||
info!("Metamodule mount script executed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute metamodule script for a specific stage
|
||||
pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> {
|
||||
let Some(script_path) = check_metamodule_script(&format!("{}.sh", stage)) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!("Executing metamodule {}.sh", stage);
|
||||
crate::module::exec_script(&script_path, block)?;
|
||||
info!("Metamodule {}.sh executed successfully", stage);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
use std::fs::{copy, rename};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::{prelude::PermissionsExt, process::CommandExt};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::var as env_var,
|
||||
fs::{File, Permissions, remove_dir_all, remove_file, set_permissions},
|
||||
io::Cursor,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
str::FromStr,
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use crate::utils::*;
|
||||
use crate::{
|
||||
assets, defs, ksucalls, metamodule,
|
||||
restorecon::{restore_syscon, setsyscon},
|
||||
sepolicy,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail, ensure};
|
||||
@@ -16,17 +11,23 @@ use const_format::concatcp;
|
||||
use is_executable::is_executable;
|
||||
use java_properties::PropertiesIter;
|
||||
use log::{info, warn};
|
||||
|
||||
use std::fs::{copy, rename};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::var as env_var,
|
||||
fs::{File, Permissions, remove_dir_all, set_permissions},
|
||||
io::Cursor,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
str::FromStr,
|
||||
};
|
||||
use zip_extensions::zip_extract_file_to_memory;
|
||||
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use crate::{
|
||||
assets,
|
||||
defs::{self, MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME},
|
||||
ksucalls,
|
||||
restorecon::{restore_syscon, setsyscon},
|
||||
sepolicy,
|
||||
utils::*,
|
||||
};
|
||||
use crate::defs::{MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME};
|
||||
use crate::module::ModuleType::{Active, All};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::{prelude::PermissionsExt, process::CommandExt};
|
||||
|
||||
const INSTALLER_CONTENT: &str = include_str!("./installer.sh");
|
||||
const INSTALL_MODULE_SCRIPT: &str = concatcp!(
|
||||
@@ -38,27 +39,36 @@ const INSTALL_MODULE_SCRIPT: &str = concatcp!(
|
||||
"\n"
|
||||
);
|
||||
|
||||
fn exec_install_script(module_file: &str) -> Result<()> {
|
||||
let realpath = std::fs::canonicalize(module_file)
|
||||
.with_context(|| format!("realpath: {module_file} failed"))?;
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", "-c", INSTALL_MODULE_SCRIPT])
|
||||
.env("ASH_STANDALONE", "1")
|
||||
.env(
|
||||
/// Get common environment variables for script execution
|
||||
pub(crate) fn get_common_script_envs() -> Vec<(&'static str, String)> {
|
||||
vec![
|
||||
("ASH_STANDALONE", "1".to_string()),
|
||||
("KSU", "true".to_string()),
|
||||
("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()),
|
||||
("KSU_VER_CODE", defs::VERSION_CODE.to_string()),
|
||||
("KSU_VER", defs::VERSION_NAME.to_string()),
|
||||
(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
env_var("PATH").unwrap(),
|
||||
env_var("PATH").unwrap_or_default(),
|
||||
defs::BINARY_DIR.trim_end_matches('/')
|
||||
),
|
||||
)
|
||||
.env("KSU", "true")
|
||||
.env("KSU_SUKISU", "true")
|
||||
.env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string())
|
||||
.env("KSU_VER", defs::VERSION_NAME)
|
||||
.env("KSU_VER_CODE", defs::VERSION_CODE)
|
||||
.env("KSU_MAGIC_MOUNT", "true")
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn exec_install_script(module_file: &str, is_metamodule: bool) -> Result<()> {
|
||||
let realpath = std::fs::canonicalize(module_file)
|
||||
.with_context(|| format!("realpath: {module_file} failed"))?;
|
||||
|
||||
// Get install script from metamodule module
|
||||
let install_script =
|
||||
metamodule::get_install_script(is_metamodule, INSTALLER_CONTENT, INSTALL_MODULE_SCRIPT)?;
|
||||
|
||||
let result = Command::new(assets::BUSYBOX_PATH)
|
||||
.args(["sh", "-c", &install_script])
|
||||
.envs(get_common_script_envs())
|
||||
.env("OUTFD", "1")
|
||||
.env("ZIPFILE", realpath)
|
||||
.status()?;
|
||||
@@ -66,10 +76,7 @@ fn exec_install_script(module_file: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// becuase we use something like A-B update
|
||||
// we need to update the module state after the boot_completed
|
||||
// if someone(such as the module) install a module before the boot_completed
|
||||
// then it may cause some problems, just forbid it
|
||||
// Check if Android boot is completed before installing modules
|
||||
fn ensure_boot_completed() -> Result<()> {
|
||||
// ensure getprop sys.boot_completed == 1
|
||||
if getprop("sys.boot_completed").as_deref() != Some("1") {
|
||||
@@ -78,34 +85,21 @@ fn ensure_boot_completed() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mark_module_state(module: &str, flag_file: &str, create: bool) -> Result<()> {
|
||||
let module_state_file = Path::new(MODULE_DIR).join(module).join(flag_file);
|
||||
if create {
|
||||
ensure_file_exists(module_state_file)
|
||||
} else {
|
||||
if module_state_file.exists() {
|
||||
remove_file(module_state_file)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum ModuleType {
|
||||
pub(crate) enum ModuleType {
|
||||
All,
|
||||
Active,
|
||||
Updated,
|
||||
}
|
||||
|
||||
fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<()>) -> Result<()> {
|
||||
pub(crate) fn foreach_module(
|
||||
module_type: ModuleType,
|
||||
mut f: impl FnMut(&Path) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
let modules_dir = Path::new(match module_type {
|
||||
ModuleType::Updated => MODULE_UPDATE_DIR,
|
||||
_ => defs::MODULE_DIR,
|
||||
});
|
||||
if !modules_dir.is_dir() {
|
||||
warn!("{} is not a directory, skip", modules_dir.display());
|
||||
return Ok(());
|
||||
}
|
||||
let dir = std::fs::read_dir(modules_dir)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
@@ -114,11 +108,11 @@ fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<()
|
||||
continue;
|
||||
}
|
||||
|
||||
if module_type == ModuleType::Active && path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
if module_type == Active && path.join(defs::DISABLE_FILE_NAME).exists() {
|
||||
info!("{} is disabled, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
if module_type == ModuleType::Active && path.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
if module_type == Active && path.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
warn!("{} is removed, skip", path.display());
|
||||
continue;
|
||||
}
|
||||
@@ -130,7 +124,7 @@ fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<()
|
||||
}
|
||||
|
||||
fn foreach_active_module(f: impl FnMut(&Path) -> Result<()>) -> Result<()> {
|
||||
foreach_module(ModuleType::Active, f)
|
||||
foreach_module(Active, f)
|
||||
}
|
||||
|
||||
pub fn load_sepolicy_rule() -> Result<()> {
|
||||
@@ -150,7 +144,7 @@ pub fn load_sepolicy_rule() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
|
||||
pub fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
|
||||
info!("exec {}", path.as_ref().display());
|
||||
|
||||
let mut command = &mut Command::new(assets::BUSYBOX_PATH);
|
||||
@@ -169,21 +163,7 @@ fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {
|
||||
.current_dir(path.as_ref().parent().unwrap())
|
||||
.arg("sh")
|
||||
.arg(path.as_ref())
|
||||
.env("ASH_STANDALONE", "1")
|
||||
.env("KSU", "true")
|
||||
.env("KSU_SUKISU", "true")
|
||||
.env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string())
|
||||
.env("KSU_VER_CODE", defs::VERSION_CODE)
|
||||
.env("KSU_VER", defs::VERSION_NAME)
|
||||
.env("KSU_MAGIC_MOUNT", "true")
|
||||
.env(
|
||||
"PATH",
|
||||
format!(
|
||||
"{}:{}",
|
||||
env_var("PATH").unwrap(),
|
||||
defs::BINARY_DIR.trim_end_matches('/')
|
||||
),
|
||||
);
|
||||
.envs(get_common_script_envs());
|
||||
|
||||
let result = if wait {
|
||||
command.status().map(|_| ())
|
||||
@@ -251,45 +231,89 @@ pub fn load_system_prop() -> Result<()> {
|
||||
}
|
||||
|
||||
pub fn prune_modules() -> Result<()> {
|
||||
foreach_module(ModuleType::All, |module| {
|
||||
if module.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
info!("remove module: {}", module.display());
|
||||
|
||||
let uninstaller = module.join("uninstall.sh");
|
||||
if uninstaller.exists()
|
||||
&& let Err(e) = exec_script(uninstaller, true)
|
||||
{
|
||||
warn!("Failed to exec uninstaller: {}", e);
|
||||
}
|
||||
|
||||
if let Err(e) = remove_dir_all(module) {
|
||||
warn!("Failed to remove {}: {}", module.display(), e);
|
||||
}
|
||||
} else {
|
||||
remove_file(module.join(defs::UPDATE_FILE_NAME)).ok();
|
||||
foreach_module(All, |module| {
|
||||
if !module.join(defs::REMOVE_FILE_NAME).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("remove module: {}", module.display());
|
||||
|
||||
// Execute metamodule's metauninstall.sh first
|
||||
let module_id = module.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
|
||||
// Check if this is a metamodule
|
||||
let is_metamodule = read_module_prop(module)
|
||||
.map(|props| metamodule::is_metamodule(&props))
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_metamodule {
|
||||
info!("Removing metamodule symlink");
|
||||
if let Err(e) = metamodule::remove_symlink() {
|
||||
warn!("Failed to remove metamodule symlink: {}", e);
|
||||
}
|
||||
} else if let Err(e) = metamodule::exec_metauninstall_script(module_id) {
|
||||
warn!(
|
||||
"Failed to exec metamodule uninstall for {}: {}",
|
||||
module_id, e
|
||||
);
|
||||
}
|
||||
|
||||
// Then execute module's own uninstall.sh
|
||||
let uninstaller = module.join("uninstall.sh");
|
||||
if uninstaller.exists()
|
||||
&& let Err(e) = exec_script(uninstaller, true)
|
||||
{
|
||||
warn!("Failed to exec uninstaller: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = remove_dir_all(module) {
|
||||
warn!("Failed to remove {}: {}", module.display(), e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// collect remaining modules, if none, clean up metamodule record
|
||||
let remaining_modules: Vec<_> = std::fs::read_dir(defs::MODULE_DIR)?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().join("module.prop").exists())
|
||||
.collect();
|
||||
|
||||
if remaining_modules.is_empty() {
|
||||
info!("no remaining modules.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_updated_modules() -> Result<()> {
|
||||
let modules_root = Path::new(MODULE_DIR);
|
||||
foreach_module(ModuleType::Updated, |module| {
|
||||
if !module.is_dir() {
|
||||
foreach_module(ModuleType::Updated, |updated_module| {
|
||||
if !updated_module.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(name) = module.file_name() {
|
||||
let old_dir = modules_root.join(name);
|
||||
if old_dir.exists()
|
||||
&& let Err(e) = remove_dir_all(&old_dir)
|
||||
{
|
||||
log::error!("Failed to remove old {}: {}", old_dir.display(), e);
|
||||
if let Some(name) = updated_module.file_name() {
|
||||
let module_dir = modules_root.join(name);
|
||||
let mut disabled = false;
|
||||
let mut removed = false;
|
||||
if module_dir.exists() {
|
||||
// If the old module is disabled, we need to also disable the new one
|
||||
disabled = module_dir.join(defs::DISABLE_FILE_NAME).exists();
|
||||
removed = module_dir.join(defs::REMOVE_FILE_NAME).exists();
|
||||
remove_dir_all(&module_dir)?;
|
||||
}
|
||||
if let Err(e) = rename(module, &old_dir) {
|
||||
log::error!("Failed to move new module {}: {}", module.display(), e);
|
||||
rename(updated_module, &module_dir)?;
|
||||
if removed {
|
||||
let path = module_dir.join(defs::REMOVE_FILE_NAME);
|
||||
if let Err(e) = ensure_file_exists(&path) {
|
||||
warn!("Failed to create {}: {}", path.display(), e);
|
||||
}
|
||||
} else if disabled {
|
||||
let path = module_dir.join(defs::DISABLE_FILE_NAME);
|
||||
if let Err(e) = ensure_file_exists(&path) {
|
||||
warn!("Failed to create {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -297,107 +321,182 @@ pub fn handle_updated_modules() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_module(zip: &str) -> Result<()> {
|
||||
fn inner(zip: &str) -> Result<()> {
|
||||
ensure_boot_completed()?;
|
||||
fn _install_module(zip: &str) -> Result<()> {
|
||||
ensure_boot_completed()?;
|
||||
|
||||
// print banner
|
||||
println!(include_str!("banner"));
|
||||
// print banner
|
||||
println!(include_str!("banner"));
|
||||
|
||||
assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?;
|
||||
assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?;
|
||||
|
||||
// first check if working dir is usable
|
||||
ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?;
|
||||
ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?;
|
||||
// first check if working dir is usable
|
||||
ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?;
|
||||
ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?;
|
||||
|
||||
// read the module_id from zip, if failed it will return early.
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
let entry_path = PathBuf::from_str("module.prop")?;
|
||||
let zip_path = PathBuf::from_str(zip)?;
|
||||
let zip_path = zip_path.canonicalize()?;
|
||||
zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?;
|
||||
// read the module_id from zip, if failed it will return early.
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
let entry_path = PathBuf::from_str("module.prop")?;
|
||||
let zip_path = PathBuf::from_str(zip)?;
|
||||
let zip_path = zip_path.canonicalize()?;
|
||||
zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?;
|
||||
|
||||
let mut module_prop = HashMap::new();
|
||||
PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into(
|
||||
|k, v| {
|
||||
module_prop.insert(k, v);
|
||||
},
|
||||
)?;
|
||||
info!("module prop: {:?}", module_prop);
|
||||
let mut module_prop = HashMap::new();
|
||||
PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into(
|
||||
|k, v| {
|
||||
module_prop.insert(k, v);
|
||||
},
|
||||
)?;
|
||||
info!("module prop: {module_prop:?}");
|
||||
|
||||
let Some(module_id) = module_prop.get("id") else {
|
||||
bail!("module id not found in module.prop!");
|
||||
};
|
||||
let module_id = module_id.trim();
|
||||
let Some(module_id) = module_prop.get("id") else {
|
||||
bail!("module id not found in module.prop!");
|
||||
};
|
||||
let module_id = module_id.trim();
|
||||
|
||||
let zip_uncompressed_size = get_zip_uncompressed_size(zip)?;
|
||||
// Check if this module is a metamodule
|
||||
let is_metamodule = metamodule::is_metamodule(&module_prop);
|
||||
|
||||
info!(
|
||||
"zip uncompressed size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
|
||||
println!("- Preparing Zip");
|
||||
println!(
|
||||
"- Module size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
|
||||
// ensure modules_update exists
|
||||
ensure_dir_exists(MODULE_UPDATE_DIR)?;
|
||||
setsyscon(MODULE_UPDATE_DIR)?;
|
||||
|
||||
let update_module_dir = Path::new(MODULE_UPDATE_DIR).join(module_id);
|
||||
ensure_clean_dir(&update_module_dir)?;
|
||||
info!("module dir: {}", update_module_dir.display());
|
||||
|
||||
let do_install = || -> Result<()> {
|
||||
// unzip the image and move it to modules_update/<id> dir
|
||||
let file = File::open(zip)?;
|
||||
let mut archive = zip::ZipArchive::new(file)?;
|
||||
archive.extract(&update_module_dir)?;
|
||||
|
||||
// set permission and selinux context for $MOD/system
|
||||
let module_system_dir = update_module_dir.join("system");
|
||||
if module_system_dir.exists() {
|
||||
#[cfg(unix)]
|
||||
set_permissions(&module_system_dir, Permissions::from_mode(0o755))?;
|
||||
restore_syscon(&module_system_dir)?;
|
||||
}
|
||||
|
||||
exec_install_script(zip)?;
|
||||
|
||||
let module_dir = Path::new(MODULE_DIR).join(module_id);
|
||||
ensure_dir_exists(&module_dir)?;
|
||||
copy(
|
||||
update_module_dir.join("module.prop"),
|
||||
module_dir.join("module.prop"),
|
||||
)?;
|
||||
ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?;
|
||||
|
||||
info!("Module install successfully!");
|
||||
|
||||
Ok(())
|
||||
};
|
||||
let result = do_install();
|
||||
if result.is_err() {
|
||||
remove_dir_all(&update_module_dir).ok();
|
||||
// Check if it's safe to install regular module
|
||||
if !is_metamodule && let Err(is_disabled) = metamodule::check_install_safety() {
|
||||
println!("\n❌ Installation Blocked");
|
||||
println!("┌────────────────────────────────");
|
||||
println!("│ A metamodule with custom installer is active");
|
||||
println!("│");
|
||||
if is_disabled {
|
||||
println!("│ Current state: Disabled");
|
||||
println!("│ Action required: Re-enable or uninstall it, then reboot");
|
||||
} else {
|
||||
println!("│ Current state: Pending changes");
|
||||
println!("│ Action required: Reboot to apply changes first");
|
||||
}
|
||||
result
|
||||
println!("└─────────────────────────────────\n");
|
||||
bail!("Metamodule installation blocked");
|
||||
}
|
||||
let result = inner(zip);
|
||||
|
||||
// All modules (including metamodules) are installed to MODULE_UPDATE_DIR
|
||||
let updated_dir = Path::new(defs::MODULE_UPDATE_DIR).join(module_id);
|
||||
|
||||
if is_metamodule {
|
||||
info!("Installing metamodule: {}", module_id);
|
||||
|
||||
// Check if there's already a metamodule installed
|
||||
if metamodule::has_metamodule()
|
||||
&& let Some(existing_path) = metamodule::get_metamodule_path()
|
||||
{
|
||||
let existing_id = read_module_prop(&existing_path)
|
||||
.ok()
|
||||
.and_then(|m| m.get("id").cloned())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
if existing_id != module_id {
|
||||
println!("\n❌ Installation Failed");
|
||||
println!("┌────────────────────────────────");
|
||||
println!("│ A metamodule is already installed");
|
||||
println!("│ Current metamodule: {}", existing_id);
|
||||
println!("│");
|
||||
println!("│ Only one metamodule can be active at a time.");
|
||||
println!("│");
|
||||
println!("│ To install this metamodule:");
|
||||
println!("│ 1. Uninstall the current metamodule");
|
||||
println!("│ 2. Reboot your device");
|
||||
println!("│ 3. Install the new metamodule");
|
||||
println!("└─────────────────────────────────\n");
|
||||
bail!("Cannot install multiple metamodules");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let zip_uncompressed_size = get_zip_uncompressed_size(zip)?;
|
||||
info!(
|
||||
"zip uncompressed size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
println!(
|
||||
"- Module size: {}",
|
||||
humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)
|
||||
);
|
||||
|
||||
// Ensure module directory exists and set SELinux context
|
||||
ensure_dir_exists(defs::MODULE_UPDATE_DIR)?;
|
||||
setsyscon(defs::MODULE_UPDATE_DIR)?;
|
||||
|
||||
// Prepare target directory
|
||||
println!("- Installing to {}", updated_dir.display());
|
||||
ensure_clean_dir(&updated_dir)?;
|
||||
info!("target dir: {}", updated_dir.display());
|
||||
|
||||
// Extract zip to target directory
|
||||
println!("- Extracting module files");
|
||||
let file = File::open(zip)?;
|
||||
let mut archive = zip::ZipArchive::new(file)?;
|
||||
archive.extract(&updated_dir)?;
|
||||
|
||||
// Set permission and selinux context for $MOD/system
|
||||
let module_system_dir = updated_dir.join("system");
|
||||
if module_system_dir.exists() {
|
||||
#[cfg(unix)]
|
||||
set_permissions(&module_system_dir, Permissions::from_mode(0o755))?;
|
||||
restore_syscon(&module_system_dir)?;
|
||||
}
|
||||
|
||||
// Execute install script
|
||||
println!("- Running module installer");
|
||||
exec_install_script(zip, is_metamodule)?;
|
||||
|
||||
let module_dir = Path::new(MODULE_DIR).join(module_id);
|
||||
ensure_dir_exists(&module_dir)?;
|
||||
copy(
|
||||
updated_dir.join("module.prop"),
|
||||
module_dir.join("module.prop"),
|
||||
)?;
|
||||
ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?;
|
||||
|
||||
// Create symlink for metamodule
|
||||
if is_metamodule {
|
||||
println!("- Creating metamodule symlink");
|
||||
metamodule::ensure_symlink(&module_dir)?;
|
||||
}
|
||||
|
||||
println!("- Module installed successfully!");
|
||||
info!("Module {} installed successfully!", module_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_module(zip: &str) -> Result<()> {
|
||||
let result = _install_module(zip);
|
||||
if let Err(ref e) = result {
|
||||
println!("- Error: {e}");
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn uninstall_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::REMOVE_FILE_NAME, true)
|
||||
pub fn undo_uninstall_module(id: &str) -> Result<()> {
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
// Remove the remove mark
|
||||
let remove_file = module_path.join(defs::REMOVE_FILE_NAME);
|
||||
if remove_file.exists() {
|
||||
std::fs::remove_file(&remove_file)
|
||||
.with_context(|| format!("Failed to delete remove file for module '{}'", id))?;
|
||||
info!("Removed the remove mark for module {}", id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restore_uninstall_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::REMOVE_FILE_NAME, false)
|
||||
pub fn uninstall_module(id: &str) -> Result<()> {
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
// Mark for removal
|
||||
let remove_file = module_path.join(defs::REMOVE_FILE_NAME);
|
||||
File::create(remove_file).with_context(|| "Failed to create remove file")?;
|
||||
|
||||
info!("Module {} marked for removal", id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_action(id: &str) -> Result<()> {
|
||||
@@ -406,11 +505,30 @@ pub fn run_action(id: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
pub fn enable_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::DISABLE_FILE_NAME, false)
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
let disable_path = module_path.join(defs::DISABLE_FILE_NAME);
|
||||
if disable_path.exists() {
|
||||
std::fs::remove_file(&disable_path).with_context(|| {
|
||||
format!("Failed to remove disable file: {}", disable_path.display())
|
||||
})?;
|
||||
info!("Module {} enabled", id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable_module(id: &str) -> Result<()> {
|
||||
mark_module_state(id, defs::DISABLE_FILE_NAME, true)
|
||||
let module_path = Path::new(defs::MODULE_DIR).join(id);
|
||||
ensure!(module_path.exists(), "Module {} not found", id);
|
||||
|
||||
let disable_path = module_path.join(defs::DISABLE_FILE_NAME);
|
||||
ensure_file_exists(disable_path)?;
|
||||
|
||||
info!("Module {} disabled", id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable_all_modules() -> Result<()> {
|
||||
@@ -418,11 +536,13 @@ pub fn disable_all_modules() -> Result<()> {
|
||||
}
|
||||
|
||||
pub fn uninstall_all_modules() -> Result<()> {
|
||||
info!("Uninstalling all modules");
|
||||
mark_all_modules(defs::REMOVE_FILE_NAME)
|
||||
}
|
||||
|
||||
fn mark_all_modules(flag_file: &str) -> Result<()> {
|
||||
let dir = std::fs::read_dir(MODULE_DIR)?;
|
||||
// we assume the module dir is already mounted
|
||||
let dir = std::fs::read_dir(defs::MODULE_DIR)?;
|
||||
for entry in dir.flatten() {
|
||||
let path = entry.path();
|
||||
let flag = path.join(flag_file);
|
||||
@@ -472,6 +592,7 @@ fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
|
||||
if !path.join("module.prop").exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut module_prop_map = match read_module_prop(&path) {
|
||||
Ok(prop) => prop,
|
||||
Err(e) => {
|
||||
@@ -481,26 +602,33 @@ fn _list_modules(path: &str) -> Vec<HashMap<String, String>> {
|
||||
};
|
||||
|
||||
// If id is missing or empty, use directory name as fallback
|
||||
let dir_id = entry.file_name().to_string_lossy().to_string();
|
||||
module_prop_map.insert("dir_id".to_owned(), dir_id.clone());
|
||||
|
||||
if !module_prop_map.contains_key("id") || module_prop_map["id"].is_empty() {
|
||||
info!("Use dir name as module id: {dir_id}");
|
||||
module_prop_map.insert("id".to_owned(), dir_id.clone());
|
||||
match entry.file_name().to_str() {
|
||||
Some(id) => {
|
||||
info!("Use dir name as module id: {id}");
|
||||
module_prop_map.insert("id".to_owned(), id.to_owned());
|
||||
}
|
||||
_ => {
|
||||
info!("Failed to get module id from dir name");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add enabled, update, remove flags
|
||||
// Add enabled, update, remove, web, action flags
|
||||
let enabled = !path.join(defs::DISABLE_FILE_NAME).exists();
|
||||
let update = path.join(defs::UPDATE_FILE_NAME).exists();
|
||||
let remove = path.join(defs::REMOVE_FILE_NAME).exists();
|
||||
let web = path.join(defs::MODULE_WEB_DIR).exists();
|
||||
let action = path.join(defs::MODULE_ACTION_SH).exists();
|
||||
let need_mount = path.join("system").exists() && !path.join("skip_mount").exists();
|
||||
|
||||
module_prop_map.insert("enabled".to_owned(), enabled.to_string());
|
||||
module_prop_map.insert("update".to_owned(), update.to_string());
|
||||
module_prop_map.insert("remove".to_owned(), remove.to_string());
|
||||
module_prop_map.insert("web".to_owned(), web.to_string());
|
||||
module_prop_map.insert("action".to_owned(), action.to_string());
|
||||
module_prop_map.insert("mount".to_owned(), need_mount.to_string());
|
||||
|
||||
modules.push(module_prop_map);
|
||||
}
|
||||
|
||||
@@ -62,11 +62,11 @@ pub fn restore_syscon<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_modules_con<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
fn restore_syscon_if_unlabeled<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
for dir_entry in WalkDir::new(dir).parallelism(Serial) {
|
||||
if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path())
|
||||
&& let Result::Ok(con) = lgetfilecon(&path)
|
||||
&& (con == ADB_CON || con == UNLABEL_CON || con.is_empty())
|
||||
&& (con == UNLABEL_CON || con.is_empty())
|
||||
{
|
||||
lsetfilecon(&path, SYSTEM_CON)?;
|
||||
}
|
||||
@@ -76,6 +76,6 @@ fn restore_modules_con<P: AsRef<Path>>(dir: P) -> Result<()> {
|
||||
|
||||
pub fn restorecon() -> Result<()> {
|
||||
lsetfilecon(defs::DAEMON_PATH, ADB_CON)?;
|
||||
restore_modules_con(defs::MODULE_DIR)?;
|
||||
restore_syscon_if_unlabeled(defs::MODULE_DIR)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
use anyhow::{Context, Error, Ok, Result, bail};
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions, create_dir_all, remove_file, write},
|
||||
fs::{Permissions, set_permissions},
|
||||
fs::{File, OpenOptions, create_dir_all, remove_file, write},
|
||||
io::{
|
||||
ErrorKind::{AlreadyExists, NotFound},
|
||||
Write,
|
||||
},
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Error, Ok, Result, bail};
|
||||
use crate::{assets, boot_patch, defs, ksucalls, module, restorecon};
|
||||
#[allow(unused_imports)]
|
||||
use std::fs::{Permissions, set_permissions};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::prelude::PermissionsExt;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use rustix::{
|
||||
process,
|
||||
thread::{LinkNameSpaceType, move_into_link_name_space},
|
||||
};
|
||||
|
||||
use crate::{assets, boot_patch, defs, ksucalls, module, restorecon};
|
||||
|
||||
pub fn ensure_clean_dir(dir: impl AsRef<Path>) -> Result<()> {
|
||||
let path = dir.as_ref();
|
||||
log::debug!("ensure_clean_dir: {}", path.display());
|
||||
@@ -32,7 +35,7 @@ pub fn ensure_clean_dir(dir: impl AsRef<Path>) -> Result<()> {
|
||||
|
||||
pub fn ensure_file_exists<T: AsRef<Path>>(file: T) -> Result<()> {
|
||||
match File::options().write(true).create_new(true).open(&file) {
|
||||
Result::Ok(_) => Ok(()),
|
||||
std::result::Result::Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
if err.kind() == AlreadyExists && file.as_ref().is_file() {
|
||||
Ok(())
|
||||
@@ -172,27 +175,6 @@ pub fn has_magisk() -> bool {
|
||||
which::which("magisk").is_ok()
|
||||
}
|
||||
|
||||
fn is_ok_empty(dir: &str) -> bool {
|
||||
use std::result::Result::Ok;
|
||||
|
||||
match fs::read_dir(dir) {
|
||||
Ok(mut entries) => entries.next().is_none(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_tmp_path() -> String {
|
||||
let dirs = ["/debug_ramdisk", "/patch_hw", "/oem", "/root", "/sbin"];
|
||||
|
||||
// find empty directory
|
||||
for dir in dirs {
|
||||
if is_ok_empty(dir) {
|
||||
return dir.to_string();
|
||||
}
|
||||
}
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn link_ksud_to_bin() -> Result<()> {
|
||||
let ksu_bin = PathBuf::from(defs::DAEMON_PATH);
|
||||
|
||||
Reference in New Issue
Block a user