feat: switch to c
This commit is contained in:
parent
20c5006bdb
commit
0e8a692177
21 changed files with 1806 additions and 1280 deletions
139
build.sh
139
build.sh
|
|
@ -13,20 +13,11 @@ BASE_URL="https://github.com/7a72/meta-magic_mount/releases/download"
|
|||
UPDATE_JSON_URL="https://raw.githubusercontent.com/7a72/meta-magic_mount/public/update.json"
|
||||
CHANGELOG_URL="https://raw.githubusercontent.com/7a72/meta-magic_mount/public/changelog.md"
|
||||
|
||||
# Rust targets
|
||||
RUST_TARGETS=(
|
||||
"x86_64-unknown-linux-musl"
|
||||
"armv7-unknown-linux-musleabihf"
|
||||
"aarch64-unknown-linux-musl"
|
||||
)
|
||||
|
||||
# Show usage
|
||||
usage() {
|
||||
echo "Usage: $0 [--release] [--debug] [--url <base_url>] [--update-json-url <url>]"
|
||||
echo "Usage: $0 [--release] [--debug]"
|
||||
echo " --release Build release version"
|
||||
echo " --debug Build debug version"
|
||||
echo " --url <url> Set base URL for downloads (default: $BASE_URL)"
|
||||
echo " --update-json-url Set update JSON URL (default: $UPDATE_JSON_URL)"
|
||||
echo " (no option) Build both release and debug versions"
|
||||
echo ""
|
||||
echo "Version number will be automatically obtained from git tag"
|
||||
|
|
@ -44,14 +35,6 @@ while [[ $# -gt 0 ]]; do
|
|||
BUILD_TYPES+=("debug")
|
||||
shift
|
||||
;;
|
||||
--url)
|
||||
BASE_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--update-json-url)
|
||||
UPDATE_JSON_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
|
|
@ -74,16 +57,9 @@ if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# Check if cargo is installed
|
||||
if ! command -v cargo &> /dev/null; then
|
||||
echo -e "${RED}Error: cargo is not installed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if cargo-zigbuild is installed
|
||||
if ! cargo zigbuild --help &> /dev/null 2>&1; then
|
||||
echo -e "${RED}Error: cargo-zigbuild is not installed${NC}"
|
||||
echo -e "${YELLOW}Install it with: cargo install cargo-zigbuild${NC}"
|
||||
# Check if zig is installed
|
||||
if ! command -v zig &> /dev/null; then
|
||||
echo -e "${RED}Error: zig is not installed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -175,101 +151,21 @@ EOF
|
|||
}
|
||||
|
||||
# Function to build Rust project for all targets
|
||||
build_rust_binaries() {
|
||||
build_binaries() {
|
||||
local BUILD_TYPE=$1
|
||||
local BIN_DIR="src/bin"
|
||||
|
||||
echo -e "\n${YELLOW}Building Rust binaries for all targets...${NC}"
|
||||
|
||||
# Create bin directory if it doesn't exist
|
||||
if [ -d "$BIN_DIR" ]; then
|
||||
rm -rf "$BIN_DIR"
|
||||
fi
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
# Change to src directory
|
||||
if [ ! -d "src" ]; then
|
||||
echo -e "${RED}Error: src directory does not exist${NC}"
|
||||
return 1
|
||||
fi
|
||||
echo -e "\n${YELLOW}Building binaries for all targets...${NC}"
|
||||
|
||||
cd src || return 1
|
||||
|
||||
# Clean previous builds
|
||||
cargo clean
|
||||
make clean
|
||||
|
||||
# Build for each target
|
||||
for target in "${RUST_TARGETS[@]}"; do
|
||||
echo -e "${YELLOW}Building for target: $target${NC}"
|
||||
|
||||
# Determine build command based on build type
|
||||
if [ "$BUILD_TYPE" = "release" ]; then
|
||||
cargo zigbuild --release --target "$target"
|
||||
else
|
||||
cargo zigbuild --target "$target"
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Error: Failed to build for target $target${NC}"
|
||||
cd ..
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Determine build output directory
|
||||
if [ "$BUILD_TYPE" = "release" ]; then
|
||||
BUILD_OUTPUT_DIR="target/$target/release"
|
||||
else
|
||||
BUILD_OUTPUT_DIR="target/$target/debug"
|
||||
fi
|
||||
|
||||
# Get the binary name from Cargo.toml (assuming single binary project)
|
||||
BINARY_NAME=$(cargo metadata --format-version 1 --no-deps 2>/dev/null | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$BINARY_NAME" ]; then
|
||||
echo -e "${RED}Error: Could not determine binary name${NC}"
|
||||
cd ..
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Copy binary to bin directory with target-specific name
|
||||
# Map target to Android ABI
|
||||
case "$target" in
|
||||
"x86_64-unknown-linux-musl")
|
||||
ABI_DIR="x86_64"
|
||||
;;
|
||||
"armv7-unknown-linux-musleabihf")
|
||||
ABI_DIR="armeabi-v7a"
|
||||
;;
|
||||
"aarch64-unknown-linux-musl")
|
||||
ABI_DIR="arm64-v8a"
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Error: Unknown target $target${NC}"
|
||||
cd ..
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create ABI directory
|
||||
mkdir -p "bin/$ABI_DIR"
|
||||
|
||||
# Copy binary
|
||||
if [ -f "$BUILD_OUTPUT_DIR/$BINARY_NAME" ]; then
|
||||
cp "$BUILD_OUTPUT_DIR/$BINARY_NAME" "bin/$ABI_DIR/$BINARY_NAME"
|
||||
# Strip binary to reduce size (for release builds)
|
||||
if [ "$BUILD_TYPE" = "release" ]; then
|
||||
strip "bin/$ABI_DIR/$BINARY_NAME" 2>/dev/null || true
|
||||
fi
|
||||
echo -e "${GREEN}Binary copied to bin/$ABI_DIR/$BINARY_NAME${NC}"
|
||||
else
|
||||
echo -e "${RED}Error: Binary not found at $BUILD_OUTPUT_DIR/$BINARY_NAME${NC}"
|
||||
cd ..
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
make $BUILD_TYPE VERSION="$VERSION"
|
||||
|
||||
cd ..
|
||||
echo -e "${GREEN}All Rust binaries built successfully${NC}"
|
||||
|
||||
echo -e "${GREEN}All binaries built successfully${NC}"
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -310,13 +206,13 @@ build_for_type() {
|
|||
echo -e "${GREEN}Template copied${NC}"
|
||||
|
||||
# Step 2: Build Rust binaries
|
||||
echo -e "\n${YELLOW}[2/7] Building Rust binaries...${NC}"
|
||||
build_rust_binaries "$BUILD_TYPE"
|
||||
echo -e "\n${YELLOW}[2/7] Building binaries...${NC}"
|
||||
build_binaries "$BUILD_TYPE"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Error: Rust build failed${NC}"
|
||||
echo -e "${RED}Error: Build failed${NC}"
|
||||
return 1
|
||||
fi
|
||||
echo -e "${GREEN}Rust build completed${NC}"
|
||||
echo -e "${GREEN}Build completed${NC}"
|
||||
|
||||
# Step 3: Modify log level in metamount.sh (in build directory)
|
||||
echo -e "\n${YELLOW}[3/7] Modifying metamount.sh configuration...${NC}"
|
||||
|
|
@ -395,10 +291,6 @@ build_for_type() {
|
|||
echo -e "\n${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN}Build completed!${NC}"
|
||||
echo -e "${GREEN}Output file: build/$OUTPUT_NAME${NC}"
|
||||
echo -e "${GREEN}Update JSON: build/update_${BUILD_TYPE}.json${NC}"
|
||||
if [ "$BUILD_TYPE" = "release" ]; then
|
||||
echo -e "${GREEN}Main update JSON: build/update.json${NC}"
|
||||
fi
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
|
||||
# Export for summary
|
||||
|
|
@ -445,9 +337,6 @@ fi
|
|||
echo -e "${GREEN}----------------------------------------${NC}"
|
||||
echo -e "${GREEN}Generated files:${NC}"
|
||||
echo -e "${GREEN} - Changelog: build/changelog.md${NC}"
|
||||
for BUILD_TYPE in "${BUILD_TYPES[@]}"; do
|
||||
echo -e "${GREEN} - Update JSON (${BUILD_TYPE}): build/update_${BUILD_TYPE}.json${NC}"
|
||||
done
|
||||
# Check if release was built
|
||||
for BUILD_TYPE in "${BUILD_TYPES[@]}"; do
|
||||
if [ "$BUILD_TYPE" = "release" ]; then
|
||||
|
|
|
|||
4
src/.gitignore
vendored
4
src/.gitignore
vendored
|
|
@ -1,4 +1,2 @@
|
|||
/target
|
||||
/out
|
||||
Cargo.lock
|
||||
/bin
|
||||
*.log
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
[package]
|
||||
name = "magic_mount_rs"
|
||||
version = "0.1.2"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
extattr = "1"
|
||||
log = "0.4"
|
||||
simplelog = "0.12"
|
||||
rustix = { version = "0.38", features = ["fs", "mount"] }
|
||||
libc = "0.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
87
src/Makefile
Normal file
87
src/Makefile
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
PROJECT = magic_mount
|
||||
VERSION ?= 0.1.0
|
||||
|
||||
# use zig cc
|
||||
ZIG ?= zig
|
||||
CC := $(ZIG) cc
|
||||
STRIPPER := strip
|
||||
|
||||
# source files
|
||||
SRCS := utils.c magic_mount.c main.c
|
||||
|
||||
# output directory
|
||||
OUTDIR := bin
|
||||
|
||||
# common flags
|
||||
CFLAGS_COMMON := -D_POSIX_C_SOURCE=200809L -std=c23 -Wall -Wextra -pedantic
|
||||
CFLAGS_COMMON += -DVERSION=\"$(VERSION)\"
|
||||
|
||||
LDFLAGS_COMMON := -static
|
||||
|
||||
# build mode specific flags
|
||||
CFLAGS_RELEASE := -O2 -DNDEBUG
|
||||
CFLAGS_DEBUG := -O0 -g -DDEBUG
|
||||
|
||||
# zig target triples
|
||||
TARGET_AMD64 ?= x86_64-linux
|
||||
TARGET_ARM64 ?= aarch64-linux
|
||||
TARGET_ARMV7 ?= arm-linux
|
||||
|
||||
# output binary names
|
||||
BIN_AMD64 := $(OUTDIR)/mm_amd64
|
||||
BIN_ARM64 := $(OUTDIR)/mm_arm64
|
||||
BIN_ARMV7 := $(OUTDIR)/mm_armv7
|
||||
|
||||
BINS := $(BIN_AMD64) $(BIN_ARM64) $(BIN_ARMV7)
|
||||
|
||||
.PHONY: all clean release debug amd64 arm64 armv7 dirs help strip-bins
|
||||
|
||||
# default target
|
||||
all: release
|
||||
|
||||
help:
|
||||
@echo "Usage:"
|
||||
@echo " make release [version=X.Y.Z] - Build release version (optimized, stripped)"
|
||||
@echo " make debug [version=X.Y.Z] - Build debug version (with symbols)"
|
||||
@echo " make clean - Clean build artifacts"
|
||||
@echo ""
|
||||
@echo "Individual targets:"
|
||||
@echo " make amd64 - Build for x86_64"
|
||||
@echo " make arm64 - Build for aarch64"
|
||||
@echo " make armv7 - Build for armv7"
|
||||
|
||||
dirs:
|
||||
mkdir -p $(OUTDIR)
|
||||
|
||||
# Release build (default)
|
||||
release: export CFLAGS := $(CFLAGS_COMMON) $(CFLAGS_RELEASE)
|
||||
release: dirs $(BINS) strip-bins
|
||||
@echo "Release build completed: $(VERSION)"
|
||||
|
||||
# Debug build
|
||||
debug: export CFLAGS := $(CFLAGS_COMMON) $(CFLAGS_DEBUG)
|
||||
debug: dirs $(BINS)
|
||||
@echo "Debug build completed: $(VERSION)"
|
||||
|
||||
strip-bins:
|
||||
@echo "Stripping binaries..."
|
||||
@for bin in $(BINS); do \
|
||||
echo " Stripping $$bin"; \
|
||||
$(STRIPPER) $$bin; \
|
||||
done
|
||||
|
||||
amd64: $(BIN_AMD64)
|
||||
arm64: $(BIN_ARM64)
|
||||
armv7: $(BIN_ARMV7)
|
||||
|
||||
$(BIN_AMD64): $(SRCS)
|
||||
$(CC) -target $(TARGET_AMD64) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
|
||||
|
||||
$(BIN_ARM64): $(SRCS)
|
||||
$(CC) -target $(TARGET_ARM64) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
|
||||
|
||||
$(BIN_ARMV7): $(SRCS)
|
||||
$(CC) -target $(TARGET_ARMV7) $(CFLAGS) $^ -o $@ $(LDFLAGS_COMMON)
|
||||
|
||||
clean:
|
||||
rm -rf $(OUTDIR)
|
||||
826
src/magic_mount.c
Normal file
826
src/magic_mount.c
Normal file
|
|
@ -0,0 +1,826 @@
|
|||
#include "magic_mount.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/xattr.h>
|
||||
#include <sys/statfs.h>
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
MountStats g_stats = { 0 };
|
||||
|
||||
const char *g_module_dir = DEFAULT_MODULE_DIR;
|
||||
const char *g_mount_source = DEFAULT_MOUNT_SOURCE;
|
||||
|
||||
char **g_failed_modules = NULL;
|
||||
int g_failed_modules_count = 0;
|
||||
|
||||
char **g_extra_parts = NULL;
|
||||
int g_extra_parts_count = 0;
|
||||
|
||||
static void register_module_failure(const char *module_name)
|
||||
{
|
||||
if (!module_name)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < g_failed_modules_count; ++i) {
|
||||
if (strcmp(g_failed_modules[i], module_name) == 0)
|
||||
return;
|
||||
}
|
||||
|
||||
char **arr =
|
||||
realloc(g_failed_modules,
|
||||
(size_t)(g_failed_modules_count + 1) * sizeof(char *));
|
||||
if (!arr)
|
||||
return;
|
||||
|
||||
g_failed_modules = arr;
|
||||
g_failed_modules[g_failed_modules_count] = strdup(module_name);
|
||||
if (!g_failed_modules[g_failed_modules_count])
|
||||
return;
|
||||
|
||||
g_failed_modules_count++;
|
||||
}
|
||||
|
||||
void add_extra_part_token(const char *start, size_t len)
|
||||
{
|
||||
while (len > 0 && (start[0] == ' ' || start[0] == '\t')) {
|
||||
start++;
|
||||
len--;
|
||||
}
|
||||
while (len > 0 && (start[len - 1] == ' ' || start[len - 1] == '\t'))
|
||||
len--;
|
||||
|
||||
if (len == 0)
|
||||
return;
|
||||
|
||||
char *name = malloc(len + 1);
|
||||
if (!name)
|
||||
return;
|
||||
|
||||
memcpy(name, start, len);
|
||||
name[len] = '\0';
|
||||
|
||||
char **arr = realloc(g_extra_parts,
|
||||
(size_t)(g_extra_parts_count + 1) * sizeof(char *));
|
||||
if (!arr) {
|
||||
free(name);
|
||||
return;
|
||||
}
|
||||
|
||||
g_extra_parts = arr;
|
||||
g_extra_parts[g_extra_parts_count++] = name;
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
NFT_REGULAR,
|
||||
NFT_DIRECTORY,
|
||||
NFT_SYMLINK,
|
||||
NFT_WHITEOUT
|
||||
} NodeFileType;
|
||||
|
||||
typedef struct Node {
|
||||
char *name;
|
||||
NodeFileType type;
|
||||
struct Node **children;
|
||||
size_t child_count;
|
||||
char *module_path;
|
||||
char *module_name;
|
||||
bool replace;
|
||||
bool skip;
|
||||
bool done;
|
||||
} Node;
|
||||
|
||||
static Node *node_new(const char *name, NodeFileType t)
|
||||
{
|
||||
Node *n = calloc(1, sizeof(Node));
|
||||
if (!n)
|
||||
return NULL;
|
||||
|
||||
n->name = strdup(name ? name : "");
|
||||
n->type = t;
|
||||
return n;
|
||||
}
|
||||
|
||||
static void node_free(Node *n)
|
||||
{
|
||||
if (!n)
|
||||
return;
|
||||
|
||||
for (size_t i = 0; i < n->child_count; ++i)
|
||||
node_free(n->children[i]);
|
||||
|
||||
free(n->children);
|
||||
free(n->name);
|
||||
free(n->module_path);
|
||||
free(n->module_name);
|
||||
free(n);
|
||||
}
|
||||
|
||||
static NodeFileType node_type_from_stat(const struct stat *st)
|
||||
{
|
||||
if (S_ISCHR(st->st_mode) && st->st_rdev == 0)
|
||||
return NFT_WHITEOUT;
|
||||
if (S_ISREG(st->st_mode))
|
||||
return NFT_REGULAR;
|
||||
if (S_ISDIR(st->st_mode))
|
||||
return NFT_DIRECTORY;
|
||||
if (S_ISLNK(st->st_mode))
|
||||
return NFT_SYMLINK;
|
||||
return NFT_WHITEOUT;
|
||||
}
|
||||
|
||||
static bool dir_is_replace(const char *path)
|
||||
{
|
||||
char buf[8];
|
||||
ssize_t len = lgetxattr(path, REPLACE_DIR_XATTR, buf, sizeof(buf) - 1);
|
||||
if (len <= 0)
|
||||
return false;
|
||||
|
||||
buf[len] = '\0';
|
||||
return (strcmp(buf, "y") == 0);
|
||||
}
|
||||
|
||||
static Node *node_new_module(const char *name, const char *path,
|
||||
const char *module_name)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) < 0)
|
||||
return NULL;
|
||||
|
||||
if (!(S_ISCHR(st.st_mode) || S_ISREG(st.st_mode) ||
|
||||
S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)))
|
||||
return NULL;
|
||||
|
||||
NodeFileType t = node_type_from_stat(&st);
|
||||
Node *n = node_new(name, t);
|
||||
if (!n)
|
||||
return NULL;
|
||||
|
||||
n->module_path = strdup(path);
|
||||
if (module_name)
|
||||
n->module_name = strdup(module_name);
|
||||
n->replace = (t == NFT_DIRECTORY) && dir_is_replace(path);
|
||||
|
||||
g_stats.nodes_total++;
|
||||
return n;
|
||||
}
|
||||
|
||||
static int node_add_child(Node *parent, Node *child)
|
||||
{
|
||||
Node **arr = realloc(parent->children,
|
||||
(parent->child_count + 1) * sizeof(Node *));
|
||||
if (!arr) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
|
||||
parent->children = arr;
|
||||
parent->children[parent->child_count++] = child;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Node *node_find_child(Node *parent, const char *name)
|
||||
{
|
||||
for (size_t i = 0; i < parent->child_count; ++i) {
|
||||
if (strcmp(parent->children[i]->name, name) == 0)
|
||||
return parent->children[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static Node *node_take_child(Node *parent, const char *name)
|
||||
{
|
||||
for (size_t i = 0; i < parent->child_count; ++i) {
|
||||
if (strcmp(parent->children[i]->name, name) == 0) {
|
||||
Node *n = parent->children[i];
|
||||
memmove(&parent->children[i], &parent->children[i + 1],
|
||||
(parent->child_count - i - 1) * sizeof(Node *));
|
||||
parent->child_count--;
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool module_disabled(const char *mod_dir)
|
||||
{
|
||||
char buf[PATH_MAX];
|
||||
|
||||
path_join(mod_dir, DISABLE_FILE_NAME, buf, sizeof(buf));
|
||||
if (path_exists(buf))
|
||||
return true;
|
||||
|
||||
path_join(mod_dir, REMOVE_FILE_NAME, buf, sizeof(buf));
|
||||
if (path_exists(buf))
|
||||
return true;
|
||||
|
||||
path_join(mod_dir, SKIP_MOUNT_FILE_NAME, buf, sizeof(buf));
|
||||
if (path_exists(buf))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static int node_collect(Node *self, const char *dir, const char *module_name,
|
||||
bool *has_any)
|
||||
{
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) {
|
||||
LOGE("opendir %s: %s", dir, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct dirent *de;
|
||||
bool any = false;
|
||||
char path[PATH_MAX];
|
||||
|
||||
while ((de = readdir(d))) {
|
||||
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
|
||||
continue;
|
||||
|
||||
if (path_join(dir, de->d_name, path, sizeof(path)) != 0) {
|
||||
closedir(d);
|
||||
return -1;
|
||||
}
|
||||
|
||||
Node *child = node_find_child(self, de->d_name);
|
||||
if (!child) {
|
||||
Node *n = node_new_module(de->d_name, path, module_name);
|
||||
if (n) {
|
||||
if (node_add_child(self, n) != 0) {
|
||||
node_free(n);
|
||||
closedir(d);
|
||||
return -1;
|
||||
}
|
||||
child = n;
|
||||
}
|
||||
}
|
||||
|
||||
if (child) {
|
||||
if (child->type == NFT_DIRECTORY) {
|
||||
bool sub = false;
|
||||
if (node_collect(child, path, module_name, &sub) != 0) {
|
||||
closedir(d);
|
||||
return -1;
|
||||
}
|
||||
if (sub || child->replace)
|
||||
any = true;
|
||||
} else {
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
*has_any = any;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Node *collect_root(void)
|
||||
{
|
||||
const char *mdir = g_module_dir;
|
||||
Node *root = node_new("", NFT_DIRECTORY);
|
||||
Node *system = node_new("system", NFT_DIRECTORY);
|
||||
|
||||
if (!root || !system) {
|
||||
node_free(root);
|
||||
node_free(system);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
DIR *d = opendir(mdir);
|
||||
if (!d) {
|
||||
LOGE("opendir %s: %s", mdir, strerror(errno));
|
||||
node_free(root);
|
||||
node_free(system);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct dirent *de;
|
||||
bool has_any = false;
|
||||
char mod[PATH_MAX];
|
||||
char mod_sys[PATH_MAX];
|
||||
|
||||
while ((de = readdir(d))) {
|
||||
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
|
||||
continue;
|
||||
|
||||
if (path_join(mdir, de->d_name, mod, sizeof(mod)) != 0) {
|
||||
closedir(d);
|
||||
node_free(root);
|
||||
node_free(system);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (stat(mod, &st) < 0 || !S_ISDIR(st.st_mode))
|
||||
continue;
|
||||
|
||||
if (module_disabled(mod))
|
||||
continue;
|
||||
|
||||
if (path_join(mod, "system", mod_sys, sizeof(mod_sys)) != 0) {
|
||||
closedir(d);
|
||||
node_free(root);
|
||||
node_free(system);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!path_is_dir(mod_sys))
|
||||
continue;
|
||||
|
||||
LOGD("collecting module %s", de->d_name);
|
||||
g_stats.modules_total++;
|
||||
|
||||
bool sub = false;
|
||||
if (node_collect(system, mod_sys, de->d_name, &sub) != 0) {
|
||||
closedir(d);
|
||||
node_free(root);
|
||||
node_free(system);
|
||||
return NULL;
|
||||
}
|
||||
if (sub)
|
||||
has_any = true;
|
||||
}
|
||||
|
||||
closedir(d);
|
||||
|
||||
if (!has_any) {
|
||||
node_free(root);
|
||||
node_free(system);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
g_stats.nodes_total += 2;
|
||||
|
||||
struct {
|
||||
const char *name;
|
||||
bool need_symlink;
|
||||
} builtin_parts[] = {
|
||||
{ "vendor", true },
|
||||
{ "system_ext", true },
|
||||
{ "product", true },
|
||||
{ "odm", false },
|
||||
};
|
||||
|
||||
char rp[PATH_MAX];
|
||||
char sp[PATH_MAX];
|
||||
|
||||
for (size_t i = 0; i < sizeof(builtin_parts) / sizeof(builtin_parts[0]);
|
||||
++i) {
|
||||
if (path_join("/", builtin_parts[i].name, rp, sizeof(rp)) != 0 ||
|
||||
path_join("/system", builtin_parts[i].name, sp, sizeof(sp)) != 0) {
|
||||
node_free(root);
|
||||
node_free(system);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!path_is_dir(rp))
|
||||
continue;
|
||||
|
||||
if (builtin_parts[i].need_symlink && !path_is_symlink(sp))
|
||||
continue;
|
||||
|
||||
Node *child = node_take_child(system, builtin_parts[i].name);
|
||||
if (child && node_add_child(root, child) != 0) {
|
||||
node_free(child);
|
||||
node_free(root);
|
||||
node_free(system);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < g_extra_parts_count; ++i) {
|
||||
const char *name = g_extra_parts[i];
|
||||
|
||||
if (path_join("/", name, rp, sizeof(rp)) != 0) {
|
||||
node_free(root);
|
||||
node_free(system);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!path_is_dir(rp))
|
||||
continue;
|
||||
|
||||
Node *child = node_take_child(system, name);
|
||||
if (child && node_add_child(root, child) != 0) {
|
||||
node_free(child);
|
||||
node_free(root);
|
||||
node_free(system);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (node_add_child(root, system) != 0) {
|
||||
node_free(root);
|
||||
node_free(system);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
static int clone_symlink(const char *src, const char *dst)
|
||||
{
|
||||
char target[PATH_MAX];
|
||||
|
||||
ssize_t len = readlink(src, target, sizeof(target) - 1);
|
||||
if (len < 0) {
|
||||
LOGE("readlink %s: %s", src, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
target[len] = '\0';
|
||||
|
||||
if (symlink(target, dst) < 0) {
|
||||
LOGE("symlink %s->%s: %s", dst, target, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
char *con = NULL;
|
||||
if (get_selinux(src, &con) == 0 && con) {
|
||||
set_selinux(dst, con);
|
||||
free(con);
|
||||
}
|
||||
|
||||
LOGD("clone symlink %s -> %s (%s)", src, dst, target);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mirror(const char *path, const char *work, const char *name);
|
||||
|
||||
static int do_magic(const char *base, const char *wbase, Node *node,
|
||||
bool has_tmpfs)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
char wpath[PATH_MAX];
|
||||
|
||||
if (path_join(base, node->name, path, sizeof(path)) != 0 ||
|
||||
path_join(wbase, node->name, wpath, sizeof(wpath)) != 0)
|
||||
return -1;
|
||||
|
||||
switch (node->type) {
|
||||
case NFT_REGULAR: {
|
||||
const char *target = has_tmpfs ? wpath : path;
|
||||
|
||||
if (has_tmpfs) {
|
||||
if (mkdir_p(wbase) != 0)
|
||||
return -1;
|
||||
|
||||
int fd = open(wpath, O_CREAT | O_WRONLY, 0644);
|
||||
if (fd < 0) {
|
||||
LOGE("create %s: %s", wpath, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
if (!node->module_path) {
|
||||
LOGE("no module file for %s", path);
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOGD("bind %s -> %s", node->module_path, target);
|
||||
|
||||
if (mount(node->module_path, target, NULL, MS_BIND, NULL) < 0) {
|
||||
LOGE("bind %s->%s: %s", node->module_path, target,
|
||||
strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)mount(NULL, target, NULL,
|
||||
MS_REMOUNT | MS_BIND | MS_RDONLY, NULL);
|
||||
|
||||
g_stats.nodes_mounted++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
case NFT_SYMLINK: {
|
||||
if (!node->module_path) {
|
||||
LOGE("no module symlink for %s", path);
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (clone_symlink(node->module_path, wpath) != 0)
|
||||
return -1;
|
||||
|
||||
g_stats.nodes_mounted++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
case NFT_WHITEOUT:
|
||||
LOGD("whiteout %s", path);
|
||||
g_stats.nodes_whiteout++;
|
||||
return 0;
|
||||
|
||||
case NFT_DIRECTORY: {
|
||||
bool create_tmp =
|
||||
(!has_tmpfs && node->replace && node->module_path);
|
||||
|
||||
if (!has_tmpfs && !create_tmp) {
|
||||
for (size_t i = 0; i < node->child_count; ++i) {
|
||||
Node *c = node->children[i];
|
||||
char rp[PATH_MAX];
|
||||
|
||||
if (path_join(path, c->name, rp, sizeof(rp)) != 0)
|
||||
return -1;
|
||||
|
||||
bool need = false;
|
||||
|
||||
if (c->type == NFT_SYMLINK) {
|
||||
need = true;
|
||||
} else if (c->type == NFT_WHITEOUT) {
|
||||
need = path_exists(rp);
|
||||
} else {
|
||||
struct stat st;
|
||||
if (lstat(rp, &st) == 0) {
|
||||
NodeFileType rt = node_type_from_stat(&st);
|
||||
if (rt != c->type || rt == NFT_SYMLINK)
|
||||
need = true;
|
||||
} else {
|
||||
need = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (need) {
|
||||
if (!node->module_path) {
|
||||
LOGE("cannot create tmpfs on %s (%s)",
|
||||
path, c->name);
|
||||
c->skip = true;
|
||||
g_stats.nodes_skipped++;
|
||||
continue;
|
||||
}
|
||||
create_tmp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool now_tmp = has_tmpfs || create_tmp;
|
||||
|
||||
if (now_tmp) {
|
||||
if (mkdir_p(wpath) != 0)
|
||||
return -1;
|
||||
|
||||
struct stat st;
|
||||
const char *meta_path = NULL;
|
||||
|
||||
if (stat(path, &st) == 0) {
|
||||
meta_path = path;
|
||||
} else if (node->module_path &&
|
||||
stat(node->module_path, &st) == 0) {
|
||||
meta_path = node->module_path;
|
||||
} else {
|
||||
LOGE("no dir meta for %s", path);
|
||||
errno = ENOENT;
|
||||
return -1;
|
||||
}
|
||||
|
||||
chmod(wpath, st.st_mode & 07777);
|
||||
chown(wpath, st.st_uid, st.st_gid);
|
||||
|
||||
char *con = NULL;
|
||||
if (get_selinux(meta_path, &con) == 0 && con) {
|
||||
set_selinux(wpath, con);
|
||||
free(con);
|
||||
}
|
||||
}
|
||||
|
||||
if (create_tmp) {
|
||||
if (mount(wpath, wpath, NULL, MS_BIND, NULL) < 0) {
|
||||
LOGE("bind self %s: %s", wpath, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (path_exists(path) && !node->replace) {
|
||||
DIR *d = opendir(path);
|
||||
if (!d) {
|
||||
LOGE("opendir %s: %s", path, strerror(errno));
|
||||
if (now_tmp)
|
||||
return -1;
|
||||
} else {
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d))) {
|
||||
if (!strcmp(de->d_name, ".") ||
|
||||
!strcmp(de->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Node *c = node_find_child(node, de->d_name);
|
||||
int r = 0;
|
||||
|
||||
if (c) {
|
||||
if (c->skip) {
|
||||
c->done = true;
|
||||
continue;
|
||||
}
|
||||
c->done = true;
|
||||
r = do_magic(path, wpath, c, now_tmp);
|
||||
} else if (now_tmp) {
|
||||
r = mirror(path, wpath, de->d_name);
|
||||
}
|
||||
|
||||
if (r != 0) {
|
||||
const char *mn = NULL;
|
||||
|
||||
if (c && c->module_name)
|
||||
mn = c->module_name;
|
||||
else if (node->module_name)
|
||||
mn = node->module_name;
|
||||
|
||||
if (mn) {
|
||||
LOGE("child %s/%s failed (module: %s)",
|
||||
path,
|
||||
c ? c->name : de->d_name,
|
||||
mn);
|
||||
register_module_failure(mn);
|
||||
} else {
|
||||
LOGE("child %s/%s failed (no module_name)",
|
||||
path,
|
||||
c ? c->name : de->d_name);
|
||||
}
|
||||
|
||||
g_stats.nodes_fail++;
|
||||
|
||||
if (now_tmp) {
|
||||
closedir(d);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < node->child_count; ++i) {
|
||||
Node *c = node->children[i];
|
||||
if (c->skip || c->done)
|
||||
continue;
|
||||
|
||||
int r = do_magic(path, wpath, c, now_tmp);
|
||||
if (r != 0) {
|
||||
const char *mn = c->module_name ?
|
||||
c->module_name :
|
||||
node->module_name;
|
||||
|
||||
if (mn) {
|
||||
LOGE("child %s/%s failed (module: %s)",
|
||||
path, c->name, mn);
|
||||
register_module_failure(mn);
|
||||
} else {
|
||||
LOGE("child %s/%s failed (no module_name)",
|
||||
path, c->name);
|
||||
}
|
||||
|
||||
g_stats.nodes_fail++;
|
||||
if (now_tmp)
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (create_tmp) {
|
||||
(void)mount(NULL, wpath, NULL,
|
||||
MS_REMOUNT | MS_BIND | MS_RDONLY, NULL);
|
||||
|
||||
if (mount(wpath, path, NULL, MS_MOVE, NULL) < 0) {
|
||||
LOGE("move %s->%s failed: %s", wpath, path,
|
||||
strerror(errno));
|
||||
if (node->module_name)
|
||||
register_module_failure(node->module_name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOGI("move mountpoint success: %s -> %s", wpath, path);
|
||||
(void)mount(NULL, path, NULL, MS_REC | MS_PRIVATE, NULL);
|
||||
}
|
||||
|
||||
g_stats.nodes_mounted++;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mirror(const char *path, const char *work, const char *name)
|
||||
{
|
||||
char src[PATH_MAX];
|
||||
char dst[PATH_MAX];
|
||||
|
||||
if (path_join(path, name, src, sizeof(src)) != 0 ||
|
||||
path_join(work, name, dst, sizeof(dst)) != 0)
|
||||
return -1;
|
||||
|
||||
struct stat st;
|
||||
if (lstat(src, &st) < 0) {
|
||||
LOGW("lstat %s: %s", src, strerror(errno));
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (S_ISREG(st.st_mode)) {
|
||||
int fd = open(dst, O_CREAT | O_WRONLY, st.st_mode & 07777);
|
||||
if (fd < 0) {
|
||||
LOGE("create %s: %s", dst, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
close(fd);
|
||||
|
||||
if (mount(src, dst, NULL, MS_BIND, NULL) < 0) {
|
||||
LOGE("bind %s->%s: %s", src, dst, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
} else if (S_ISDIR(st.st_mode)) {
|
||||
if (mkdir(dst, st.st_mode & 07777) < 0 && errno != EEXIST) {
|
||||
LOGE("mkdir %s: %s", dst, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
chmod(dst, st.st_mode & 07777);
|
||||
chown(dst, st.st_uid, st.st_gid);
|
||||
|
||||
char *con = NULL;
|
||||
if (get_selinux(src, &con) == 0 && con) {
|
||||
set_selinux(dst, con);
|
||||
free(con);
|
||||
}
|
||||
|
||||
DIR *d = opendir(src);
|
||||
if (!d) {
|
||||
LOGE("opendir %s: %s", src, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d))) {
|
||||
if (!strcmp(de->d_name, ".") ||
|
||||
!strcmp(de->d_name, "..")) {
|
||||
continue;
|
||||
}
|
||||
if (mirror(src, dst, de->d_name) != 0) {
|
||||
closedir(d);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
} else if (S_ISLNK(st.st_mode)) {
|
||||
if (clone_symlink(src, dst) != 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int magic_mount(const char *tmp_root)
|
||||
{
|
||||
Node *root = collect_root();
|
||||
if (!root) {
|
||||
LOGI("no modules, magic_mount skipped");
|
||||
return 0;
|
||||
}
|
||||
|
||||
char tmp_dir[PATH_MAX];
|
||||
if (path_join(tmp_root, "workdir", tmp_dir, sizeof(tmp_dir)) != 0) {
|
||||
node_free(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mkdir_p(tmp_dir) != 0) {
|
||||
node_free(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOGI("starting magic_mount core logic: tmpfs_source=%s tmp_dir=%s",
|
||||
g_mount_source, tmp_dir);
|
||||
|
||||
if (mount(g_mount_source, tmp_dir, "tmpfs", 0, "") < 0) {
|
||||
LOGE("mount tmpfs %s: %s", tmp_dir, strerror(errno));
|
||||
node_free(root);
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)mount(NULL, tmp_dir, NULL, MS_REC | MS_PRIVATE, NULL);
|
||||
|
||||
int rc = do_magic("/", tmp_dir, root, false);
|
||||
if (rc != 0)
|
||||
g_stats.nodes_fail++;
|
||||
|
||||
if (umount2(tmp_dir, MNT_DETACH) < 0)
|
||||
LOGE("umount %s: %s", tmp_dir, strerror(errno));
|
||||
|
||||
(void)rmdir(tmp_dir);
|
||||
|
||||
node_free(root);
|
||||
return rc;
|
||||
}
|
||||
39
src/magic_mount.h
Normal file
39
src/magic_mount.h
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#ifndef MAGIC_MOUNT_H
|
||||
#define MAGIC_MOUNT_H
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#define DISABLE_FILE_NAME "disable"
|
||||
#define REMOVE_FILE_NAME "remove"
|
||||
#define SKIP_MOUNT_FILE_NAME "skip_mount"
|
||||
|
||||
#define REPLACE_DIR_XATTR "trusted.overlay.opaque"
|
||||
|
||||
#define DEFAULT_MOUNT_SOURCE "KSU"
|
||||
#define DEFAULT_MODULE_DIR "/data/adb/modules"
|
||||
|
||||
typedef struct {
|
||||
int modules_total;
|
||||
int nodes_total;
|
||||
int nodes_mounted;
|
||||
int nodes_skipped;
|
||||
int nodes_whiteout;
|
||||
int nodes_fail;
|
||||
} MountStats;
|
||||
|
||||
extern MountStats g_stats;
|
||||
|
||||
extern const char *g_module_dir;
|
||||
extern const char *g_mount_source;
|
||||
|
||||
extern char **g_failed_modules;
|
||||
extern int g_failed_modules_count;
|
||||
|
||||
extern char **g_extra_parts;
|
||||
extern int g_extra_parts_count;
|
||||
|
||||
int magic_mount(const char *tmp_root);
|
||||
|
||||
void add_extra_part_token(const char *start, size_t len);
|
||||
|
||||
#endif /* MAGIC_MOUNT_H */
|
||||
311
src/main.c
Normal file
311
src/main.c
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
#include "magic_mount.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#define DEFAULT_CONFIG_PATH "/data/adb/magic_mount/mm.conf"
|
||||
|
||||
static void usage(const char *prog)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"Magic Mount Version: %s\n"
|
||||
"Usage: %s [options]\n"
|
||||
" -m DIR module dir (default: %s)\n"
|
||||
" -t DIR temp dir (default: auto rw tmpfs + \".magic_mount\")\n"
|
||||
" -s SRC mount src (default: %s)\n"
|
||||
" -p LIST extra partitions, comma-separated (e.g. mi_ext,my_stock)\n"
|
||||
" -l FILE log file (default: stderr, '-'=stdout)\n"
|
||||
" -c FILE config file (default: " DEFAULT_CONFIG_PATH ")\n"
|
||||
" -v enable debug logging (default: info)\n"
|
||||
" -h help\n",
|
||||
VERSION, prog, DEFAULT_MODULE_DIR,
|
||||
DEFAULT_MOUNT_SOURCE);
|
||||
}
|
||||
|
||||
static int enter_pid1_ns(void)
|
||||
{
|
||||
int fd = open("/proc/1/ns/mnt", O_RDONLY);
|
||||
if (fd < 0) {
|
||||
LOGE("open /proc/1/ns/mnt: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (setns(fd, 0) < 0) {
|
||||
LOGE("setns: %s", strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
LOGI("entered pid1 mnt ns");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *str_trim(char *s)
|
||||
{
|
||||
if (!s)
|
||||
return s;
|
||||
|
||||
char *start = s;
|
||||
while (*start && isspace((unsigned char)*start))
|
||||
start++;
|
||||
|
||||
char *end = start + strlen(start);
|
||||
while (end > start && isspace((unsigned char)end[-1]))
|
||||
*--end = '\0';
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
static bool str_is_true(const char *v)
|
||||
{
|
||||
if (!v)
|
||||
return false;
|
||||
if (!strcasecmp(v, "1") ||
|
||||
!strcasecmp(v, "true") ||
|
||||
!strcasecmp(v, "yes") ||
|
||||
!strcasecmp(v, "on"))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void load_config(const char *config_path,
|
||||
const char **tmp_dir_out,
|
||||
const char **log_path_out,
|
||||
bool *debug_out)
|
||||
{
|
||||
FILE *fp = fopen(config_path, "r");
|
||||
if (!fp) {
|
||||
LOGD("config file %s not used: %s",
|
||||
config_path, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
LOGI("loading config file: %s", config_path);
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
char *p = line;
|
||||
|
||||
while (*p && isspace((unsigned char)*p))
|
||||
p++;
|
||||
|
||||
if (*p == '\0' || *p == '\n' || *p == '#')
|
||||
continue;
|
||||
|
||||
char *eq = strchr(p, '=');
|
||||
if (!eq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
*eq = '\0';
|
||||
char *key = str_trim(p);
|
||||
char *val = str_trim(eq + 1);
|
||||
|
||||
size_t vlen = strlen(val);
|
||||
if (vlen > 0 && (val[vlen - 1] == '\n' || val[vlen - 1] == '\r'))
|
||||
val[vlen - 1] = '\0';
|
||||
val = str_trim(val);
|
||||
|
||||
if (*key == '\0' || *val == '\0')
|
||||
continue;
|
||||
|
||||
if (!strcasecmp(key, "module_dir")) {
|
||||
char *dup = strdup(val);
|
||||
if (dup)
|
||||
g_module_dir = dup;
|
||||
} else if (!strcasecmp(key, "temp_dir")) {
|
||||
char *dup = strdup(val);
|
||||
if (dup && tmp_dir_out)
|
||||
*tmp_dir_out = dup;
|
||||
} else if (!strcasecmp(key, "mount_source")) {
|
||||
char *dup = strdup(val);
|
||||
if (dup)
|
||||
g_mount_source = dup;
|
||||
} else if (!strcasecmp(key, "partitions")) {
|
||||
const char *list = val;
|
||||
const char *q = list;
|
||||
while (*q) {
|
||||
const char *start = q;
|
||||
while (*q && *q != ',')
|
||||
q++;
|
||||
size_t len = (size_t)(q - start);
|
||||
add_extra_part_token(start, len);
|
||||
if (*q == ',')
|
||||
q++;
|
||||
}
|
||||
} else if (!strcasecmp(key, "log_file")) {
|
||||
char *dup = strdup(val);
|
||||
if (dup && log_path_out)
|
||||
*log_path_out = dup;
|
||||
} else if (!strcasecmp(key, "debug")) {
|
||||
if (debug_out && str_is_true(val))
|
||||
*debug_out = true;
|
||||
} else {
|
||||
LOGW("unknown config key: %s", key);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *tmp_dir = NULL;
|
||||
const char *log_path = NULL;
|
||||
const char *config_path = DEFAULT_CONFIG_PATH;
|
||||
char auto_tmp[PATH_MAX] = { 0 };
|
||||
bool debug_from_cfg = false;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const char *arg = argv[i];
|
||||
|
||||
if ((strcmp(arg, "-c") == 0 || strcmp(arg, "--config") == 0) &&
|
||||
i + 1 < argc) {
|
||||
config_path = argv[++i];
|
||||
}
|
||||
}
|
||||
|
||||
load_config(config_path, &tmp_dir, &log_path, &debug_from_cfg);
|
||||
|
||||
if (debug_from_cfg) {
|
||||
g_log_level = LV_DEBUG;
|
||||
}
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const char *arg = argv[i];
|
||||
|
||||
if ((strcmp(arg, "-c") == 0 || strcmp(arg, "--config") == 0) &&
|
||||
i + 1 < argc) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((strcmp(arg, "-m") == 0 ||
|
||||
strcmp(arg, "--module-dir") == 0) &&
|
||||
i + 1 < argc) {
|
||||
g_module_dir = argv[++i];
|
||||
|
||||
} else if ((strcmp(arg, "-t") == 0 ||
|
||||
strcmp(arg, "--temp-dir") == 0) &&
|
||||
i + 1 < argc) {
|
||||
tmp_dir = argv[++i];
|
||||
|
||||
} else if ((strcmp(arg, "-s") == 0 ||
|
||||
strcmp(arg, "--mount-source") == 0) &&
|
||||
i + 1 < argc) {
|
||||
g_mount_source = argv[++i];
|
||||
|
||||
} else if ((strcmp(arg, "-l") == 0 ||
|
||||
strcmp(arg, "--log-file") == 0) &&
|
||||
i + 1 < argc) {
|
||||
log_path = argv[++i];
|
||||
|
||||
} else if (!strcmp(arg, "-v") || !strcmp(arg, "--verbose")) {
|
||||
g_log_level = LV_DEBUG;
|
||||
|
||||
} else if ((strcmp(arg, "-p") == 0 ||
|
||||
strcmp(arg, "--partitions") == 0) &&
|
||||
i + 1 < argc) {
|
||||
const char *list = argv[++i];
|
||||
const char *p = list;
|
||||
while (*p) {
|
||||
const char *start = p;
|
||||
while (*p && *p != ',')
|
||||
p++;
|
||||
size_t len = (size_t)(p - start);
|
||||
add_extra_part_token(start, len);
|
||||
if (*p == ',')
|
||||
p++;
|
||||
}
|
||||
|
||||
} else if (!strcmp(arg, "-h") || !strcmp(arg, "--help")) {
|
||||
usage(argv[0]);
|
||||
return 0;
|
||||
|
||||
} else {
|
||||
fprintf(stderr, "Unknown arg: %s\n", arg);
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (log_path) {
|
||||
if (!strcmp(log_path, "-")) {
|
||||
g_log_file = stdout;
|
||||
} else {
|
||||
g_log_file = fopen(log_path, "a");
|
||||
if (!g_log_file) {
|
||||
fprintf(stderr, "open log %s: %s\n", log_path,
|
||||
strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!tmp_dir) {
|
||||
tmp_dir = select_auto_tempdir(auto_tmp);
|
||||
}
|
||||
|
||||
if (geteuid() != 0) {
|
||||
LOGE("must run as root (euid=%d)", (int)geteuid());
|
||||
if (g_log_file && g_log_file != stdout && g_log_file != stderr)
|
||||
fclose(g_log_file);
|
||||
free_string_array(&g_extra_parts, &g_extra_parts_count);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (enter_pid1_ns() != 0) {
|
||||
if (g_log_file && g_log_file != stdout && g_log_file != stderr)
|
||||
fclose(g_log_file);
|
||||
free_string_array(&g_extra_parts, &g_extra_parts_count);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int rc = 0;
|
||||
|
||||
LOGI("starting magic_mount process: module_dir=%s temp_dir=%s "
|
||||
"mount_source=%s log_level=%d",
|
||||
g_module_dir, tmp_dir, g_mount_source, g_log_level);
|
||||
|
||||
rc = magic_mount(tmp_dir);
|
||||
|
||||
if (rc == 0) {
|
||||
LOGI("magic_mount completed successfully");
|
||||
} else {
|
||||
LOGE("magic_mount failed with rc=%d "
|
||||
"(check previous logs for details)",
|
||||
rc);
|
||||
}
|
||||
|
||||
LOGI("magic_mount summary: modules=%d nodes_total=%d mounted=%d "
|
||||
"skipped=%d whiteouts=%d failures=%d",
|
||||
g_stats.modules_total, g_stats.nodes_total, g_stats.nodes_mounted,
|
||||
g_stats.nodes_skipped, g_stats.nodes_whiteout, g_stats.nodes_fail);
|
||||
|
||||
if (g_failed_modules_count > 0) {
|
||||
LOGW("modules with failures (%d):", g_failed_modules_count);
|
||||
for (int i = 0; i < g_failed_modules_count; ++i) {
|
||||
LOGW(" failed module: %s", g_failed_modules[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (g_log_file && g_log_file != stdout && g_log_file != stderr)
|
||||
fclose(g_log_file);
|
||||
|
||||
free_string_array(&g_failed_modules, &g_failed_modules_count);
|
||||
free_string_array(&g_extra_parts, &g_extra_parts_count);
|
||||
|
||||
return rc == 0 ? 0 : 1;
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const CONFIG_FILE_DEFAULT: &str = "/data/adb/magic_mount/config.toml";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_moduledir")]
|
||||
pub moduledir: PathBuf,
|
||||
|
||||
#[serde(default)]
|
||||
pub tempdir: Option<PathBuf>,
|
||||
|
||||
#[serde(default = "default_mountsource")]
|
||||
pub mountsource: String,
|
||||
|
||||
#[serde(default = "default_logfile")]
|
||||
pub logfile: PathBuf,
|
||||
|
||||
#[serde(default)]
|
||||
pub verbose: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub partitions: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_moduledir() -> PathBuf {
|
||||
PathBuf::from("/data/adb/modules/")
|
||||
}
|
||||
|
||||
fn default_mountsource() -> String {
|
||||
"MaGIcMounT".to_string()
|
||||
}
|
||||
|
||||
fn default_logfile() -> PathBuf {
|
||||
PathBuf::from("/data/adb/magic_mount/mm.log")
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
moduledir: default_moduledir(),
|
||||
tempdir: None,
|
||||
mountsource: default_mountsource(),
|
||||
logfile: default_logfile(),
|
||||
verbose: false,
|
||||
partitions: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let content = fs::read_to_string(path.as_ref())
|
||||
.context("failed to read config file")?;
|
||||
|
||||
let config: Config = toml::from_str(&content)
|
||||
.context("failed to parse config file")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn load_default() -> Option<Self> {
|
||||
Self::from_file(CONFIG_FILE_DEFAULT).ok()
|
||||
}
|
||||
|
||||
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
||||
let content = toml::to_string_pretty(self)
|
||||
.context("failed to serialize config")?;
|
||||
|
||||
if let Some(parent) = path.as_ref().parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.context("failed to create config directory")?;
|
||||
}
|
||||
|
||||
fs::write(path.as_ref(), content)
|
||||
.context("failed to write config file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn example() -> String {
|
||||
let example = Self::default();
|
||||
toml::to_string_pretty(&example).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn merge_with_cli(&mut self,
|
||||
moduledir: Option<PathBuf>,
|
||||
tempdir: Option<PathBuf>,
|
||||
mountsource: Option<String>,
|
||||
logfile: Option<PathBuf>,
|
||||
verbose: bool,
|
||||
partitions: Vec<String>,
|
||||
) {
|
||||
if let Some(dir) = moduledir {
|
||||
self.moduledir = dir;
|
||||
}
|
||||
if tempdir.is_some() {
|
||||
self.tempdir = tempdir;
|
||||
}
|
||||
if let Some(source) = mountsource {
|
||||
self.mountsource = source;
|
||||
}
|
||||
if let Some(log) = logfile {
|
||||
self.logfile = log;
|
||||
}
|
||||
if verbose {
|
||||
self.verbose = true;
|
||||
}
|
||||
if !partitions.is_empty() {
|
||||
self.partitions = partitions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,489 +0,0 @@
|
|||
use crate::magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout};
|
||||
use crate::utils::{lgetfilecon, lsetfilecon, ensure_dir_exists};
|
||||
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,
|
||||
};
|
||||
use rustix::mount::mount_change;
|
||||
use rustix::path::Arg;
|
||||
use std::cmp::PartialEq;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::fs;
|
||||
use std::fs::{DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link};
|
||||
use std::os::unix::fs::{FileTypeExt, symlink};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const DISABLE_FILE_NAME: &str = "disable";
|
||||
const REMOVE_FILE_NAME: &str = "remove";
|
||||
const SKIP_MOUNT_FILE_NAME: &str = "skip_mount";
|
||||
|
||||
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<T: AsRef<Path>>(&mut self, module_dir: T) -> Result<bool> {
|
||||
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: ToString>(name: T) -> Self {
|
||||
Node {
|
||||
name: name.to_string(),
|
||||
file_type: Directory,
|
||||
children: Default::default(),
|
||||
module_path: None,
|
||||
replace: false,
|
||||
skip: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_module<T: ToString>(name: T, entry: &DirEntry) -> Option<Self> {
|
||||
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(module_dir: &Path, extra_partitions: &[String]) -> Result<Option<Node>> {
|
||||
let mut root = Node::new_root("");
|
||||
let mut system = Node::new_root("system");
|
||||
let module_root = 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(REMOVE_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 {
|
||||
const BUILTIN_PARTITIONS: [(&str, bool); 4] = [
|
||||
("vendor", true),
|
||||
("system_ext", true),
|
||||
("product", true),
|
||||
("odm", false),
|
||||
];
|
||||
|
||||
for (partition, require_symlink) in BUILTIN_PARTITIONS {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for partition in extra_partitions {
|
||||
if BUILTIN_PARTITIONS.iter().any(|(p, _)| p == partition) {
|
||||
continue;
|
||||
}
|
||||
if partition == "system" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path_of_root = Path::new("/").join(partition);
|
||||
let path_of_system = Path::new("/system").join(partition);
|
||||
let require_symlink = false;
|
||||
|
||||
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) {
|
||||
log::debug!("attach extra partition '{}' to root", name);
|
||||
root.children.insert(name, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.children.insert("system".to_string(), system);
|
||||
Ok(Some(root))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_symlink<Src: AsRef<Path>, Dst: AsRef<Path>>(src: Src, dst: Dst) -> Result<()> {
|
||||
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: AsRef<Path>, WP: AsRef<Path>>(
|
||||
path: P,
|
||||
work_dir_path: WP,
|
||||
entry: &DirEntry,
|
||||
) -> Result<()> {
|
||||
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: AsRef<Path>, WP: AsRef<Path>>(
|
||||
path: P,
|
||||
work_dir_path: WP,
|
||||
current: Node,
|
||||
has_tmpfs: bool,
|
||||
) -> Result<()> {
|
||||
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<T: AsRef<Path>>(
|
||||
tmp_path: T,
|
||||
module_dir: &Path,
|
||||
mount_source: &str,
|
||||
extra_partitions: &[String],
|
||||
) -> Result<()> {
|
||||
if let Some(root) = collect_module_files(module_dir, extra_partitions)? {
|
||||
log::debug!("collected: {:#?}", root);
|
||||
|
||||
let tmp_root = tmp_path.as_ref();
|
||||
let tmp_dir = tmp_root.join("workdir");
|
||||
ensure_dir_exists(&tmp_dir)?;
|
||||
|
||||
mount(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(())
|
||||
}
|
||||
}
|
||||
179
src/src/main.rs
179
src/src/main.rs
|
|
@ -1,179 +0,0 @@
|
|||
mod config;
|
||||
mod magic_mount;
|
||||
mod utils;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
use config::Config;
|
||||
|
||||
const CONFIG_FILE_DEFAULT: &str = "/data/adb/magic_mount/config.toml";
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "magic_mount", version, about = "Magic Mount Metamodule")]
|
||||
struct Cli {
|
||||
/// Config file path
|
||||
#[arg(short = 'c', long = "config")]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// Module directory path
|
||||
#[arg(short = 'm', long = "moduledir")]
|
||||
moduledir: Option<PathBuf>,
|
||||
|
||||
/// Temporary directory path (auto-selected if not specified)
|
||||
#[arg(short = 't', long = "tempdir")]
|
||||
tempdir: Option<PathBuf>,
|
||||
|
||||
/// Mount source name
|
||||
#[arg(short = 's', long = "mountsource")]
|
||||
mountsource: Option<String>,
|
||||
|
||||
/// Log file path
|
||||
#[arg(short = 'l', long = "logfile")]
|
||||
logfile: Option<PathBuf>,
|
||||
|
||||
/// Enable verbose (debug) logging
|
||||
#[arg(short = 'v', long = "verbose")]
|
||||
verbose: bool,
|
||||
|
||||
/// Extra partitions, comma-separated, eg: -p mi_ext,my_stock
|
||||
#[arg(short = 'p', long = "partitions", value_delimiter = ',')]
|
||||
partitions: Vec<String>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
/// Generate example config file
|
||||
GenConfig {
|
||||
/// Output path for config file
|
||||
#[arg(short = 'o', long = "output", default_value = CONFIG_FILE_DEFAULT)]
|
||||
output: PathBuf,
|
||||
},
|
||||
/// Show current effective configuration
|
||||
ShowConfig,
|
||||
}
|
||||
|
||||
fn load_config(cli: &Cli) -> Result<Config> {
|
||||
// 1. 尝试从指定的配置文件加载
|
||||
if let Some(config_path) = &cli.config {
|
||||
log::info!("Loading config from: {}", config_path.display());
|
||||
return Config::from_file(config_path)
|
||||
.context("failed to load specified config file");
|
||||
}
|
||||
|
||||
// 2. 尝试从默认位置加载
|
||||
if let Some(config) = Config::load_default() {
|
||||
log::info!("Loaded config from default location: {}", CONFIG_FILE_DEFAULT);
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// 3. 使用默认配置
|
||||
log::info!("Using default configuration (no config file found)");
|
||||
Ok(Config::default())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// 处理子命令
|
||||
if let Some(command) = &cli.command {
|
||||
match command {
|
||||
Commands::GenConfig { output } => {
|
||||
return generate_config(output);
|
||||
}
|
||||
Commands::ShowConfig => {
|
||||
let config = load_config(&cli)?;
|
||||
return show_config(&config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载配置
|
||||
let mut config = load_config(&cli)?;
|
||||
|
||||
// 命令行参数覆盖配置文件
|
||||
config.merge_with_cli(
|
||||
cli.moduledir,
|
||||
cli.tempdir,
|
||||
cli.mountsource,
|
||||
cli.logfile.clone(),
|
||||
cli.verbose,
|
||||
cli.partitions,
|
||||
);
|
||||
|
||||
// 初始化日志
|
||||
utils::init_logger(&config.logfile, config.verbose)?;
|
||||
|
||||
log::info!("Magic Mount Starting");
|
||||
log::info!("module dir : {}", config.moduledir.display());
|
||||
|
||||
let tempdir = if let Some(temp) = config.tempdir {
|
||||
log::info!("temp dir (cfg) : {}", temp.display());
|
||||
temp
|
||||
} else {
|
||||
let temp = utils::select_temp_dir()
|
||||
.context("failed to select temp dir automatically")?;
|
||||
log::info!("temp dir (auto) : {}", temp.display());
|
||||
temp
|
||||
};
|
||||
|
||||
log::info!("mount source : {}", config.mountsource);
|
||||
log::info!("log file : {}", config.logfile.display());
|
||||
log::info!("verbose mode : {}", config.verbose);
|
||||
if !config.partitions.is_empty() {
|
||||
log::info!("extra partitions: {:?}", config.partitions);
|
||||
}
|
||||
|
||||
utils::ensure_temp_dir(&tempdir)?;
|
||||
|
||||
let result = magic_mount::magic_mount(
|
||||
&tempdir,
|
||||
&config.moduledir,
|
||||
&config.mountsource,
|
||||
&config.partitions,
|
||||
);
|
||||
|
||||
utils::cleanup_temp_dir(&tempdir);
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
log::info!("Magic Mount Completed Successfully");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Magic Mount Failed");
|
||||
log::error!("error: {:#}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_config(output: &PathBuf) -> Result<()> {
|
||||
let config = Config::default();
|
||||
config.save_to_file(output)
|
||||
.context("failed to generate config file")?;
|
||||
|
||||
println!("✓ Config file generated at: {}", output.display());
|
||||
println!("\nExample content:");
|
||||
println!("{}", Config::example());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_config(config: &Config) -> Result<()> {
|
||||
println!("Current Configuration:");
|
||||
println!("=====================");
|
||||
println!("Module Dir : {}", config.moduledir.display());
|
||||
println!("Temp Dir : {}",
|
||||
config.tempdir.as_ref()
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|| "(auto)".to_string())
|
||||
);
|
||||
println!("Mount Source : {}", config.mountsource);
|
||||
println!("Log File : {}", config.logfile.display());
|
||||
println!("Verbose : {}", config.verbose);
|
||||
println!("Partitions : {:?}", config.partitions);
|
||||
Ok(())
|
||||
}
|
||||
159
src/src/utils.rs
159
src/src/utils.rs
|
|
@ -1,159 +0,0 @@
|
|||
use anyhow::{bail, Context, Result};
|
||||
use log::LevelFilter;
|
||||
use simplelog::{Config, WriteLogger};
|
||||
use std::fs::{OpenOptions, write, remove_file, create_dir_all, remove_dir_all, read_to_string};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
use extattr::{lsetxattr, Flags as XattrFlags};
|
||||
|
||||
const SELINUX_XATTR: &str = "security.selinux";
|
||||
|
||||
const TEMP_DIR_SUFFIX: &str = ".magic_mount";
|
||||
|
||||
const TMPFS_CANDIDATES: &[&str] = &["/mnt/vendor", "/mnt", "/debug_ramdisk"];
|
||||
|
||||
pub fn lsetfilecon<P: AsRef<Path>>(path: P, con: &str) -> Result<()> {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
lsetxattr(&path, SELINUX_XATTR, con, XattrFlags::empty()).with_context(|| {
|
||||
format!(
|
||||
"Failed to change SELinux context for {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
let con = extattr::lgetxattr(&path, SELINUX_XATTR).with_context(|| {
|
||||
format!(
|
||||
"Failed to get SELinux context for {}",
|
||||
path.as_ref().display()
|
||||
)
|
||||
})?;
|
||||
let con = String::from_utf8_lossy(&con);
|
||||
Ok(con.to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
pub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn ensure_dir_exists<T: AsRef<Path>>(dir: T) -> Result<()> {
|
||||
let result = create_dir_all(&dir);
|
||||
if dir.as_ref().is_dir() && result.is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("{} is not a regular directory", dir.as_ref().display())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_logger(logfile: &Path, verbose: bool) -> Result<()> {
|
||||
let level = if verbose {
|
||||
LevelFilter::Debug
|
||||
} else if cfg!(debug_assertions) {
|
||||
LevelFilter::Debug
|
||||
} else {
|
||||
LevelFilter::Info
|
||||
};
|
||||
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(logfile)
|
||||
.with_context(|| format!("failed to open log file {logfile:?}"))?;
|
||||
|
||||
WriteLogger::init(level, Config::default(), file)
|
||||
.context("failed to init file logger")?;
|
||||
|
||||
log::info!("log level: {:?}", level);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_writable_tmpfs(path: &Path) -> bool {
|
||||
if !path.is_dir() {
|
||||
log::debug!(" {} is not a directory", path.display());
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Ok(mounts) = read_to_string("/proc/mounts") {
|
||||
let path_str = path.to_string_lossy();
|
||||
let is_tmpfs = mounts.lines().any(|line| {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
parts.len() >= 3 && parts[1] == path_str && parts[2] == "tmpfs"
|
||||
});
|
||||
|
||||
if !is_tmpfs {
|
||||
log::debug!(" {} is not a tmpfs", path.display());
|
||||
return false;
|
||||
}
|
||||
log::debug!(" {} is a tmpfs", path.display());
|
||||
} else {
|
||||
log::debug!(" failed to read /proc/mounts");
|
||||
}
|
||||
|
||||
let test_file = path.join(format!(".mm_test_{}", std::process::id()));
|
||||
let writable = write(&test_file, b"test").is_ok();
|
||||
|
||||
if writable {
|
||||
let _ = remove_file(&test_file);
|
||||
log::debug!(" {} is writable", path.display());
|
||||
} else {
|
||||
log::debug!(" {} is not writable", path.display());
|
||||
}
|
||||
|
||||
writable
|
||||
}
|
||||
|
||||
pub fn select_temp_dir() -> Result<PathBuf> {
|
||||
log::debug!("searching for suitable tmpfs mount point...");
|
||||
|
||||
for candidate in TMPFS_CANDIDATES {
|
||||
let path = Path::new(candidate);
|
||||
log::debug!("checking tmpfs candidate: {}", path.display());
|
||||
|
||||
if is_writable_tmpfs(path) {
|
||||
let temp_dir = path.join(TEMP_DIR_SUFFIX);
|
||||
log::info!(
|
||||
"selected tmpfs: {} -> {}",
|
||||
path.display(),
|
||||
temp_dir.display()
|
||||
);
|
||||
return Ok(temp_dir);
|
||||
}
|
||||
}
|
||||
|
||||
bail!(
|
||||
"no writable tmpfs found in candidates: {}",
|
||||
TMPFS_CANDIDATES.join(", ")
|
||||
)
|
||||
}
|
||||
|
||||
pub fn ensure_temp_dir(temp_dir: &Path) -> Result<()> {
|
||||
if temp_dir.exists() {
|
||||
log::debug!("cleaning existing temp dir: {}", temp_dir.display());
|
||||
remove_dir_all(temp_dir)
|
||||
.with_context(|| format!("failed to clean temp dir {temp_dir:?}"))?;
|
||||
}
|
||||
|
||||
create_dir_all(temp_dir)
|
||||
.with_context(|| format!("failed to create temp dir {temp_dir:?}"))?;
|
||||
|
||||
log::debug!("temp dir ready: {}", temp_dir.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cleanup_temp_dir(temp_dir: &Path) {
|
||||
if let Err(e) = remove_dir_all(temp_dir) {
|
||||
log::warn!(
|
||||
"failed to clean up temp dir {}: {:#}",
|
||||
temp_dir.display(),
|
||||
e
|
||||
);
|
||||
} else {
|
||||
log::debug!("cleaned up temp dir: {}", temp_dir.display());
|
||||
}
|
||||
}
|
||||
234
src/utils.c
Normal file
234
src/utils.c
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
#include "utils.h"
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/xattr.h>
|
||||
#include <sys/statfs.h>
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
FILE *g_log_file = NULL; /* stderr */
|
||||
int g_log_level = 2; /* INFO */
|
||||
|
||||
static void vlog(const char *lv, const char *fmt, va_list ap)
|
||||
{
|
||||
FILE *out = g_log_file ? g_log_file : stderr;
|
||||
fprintf(out, "[%s] ", lv);
|
||||
vfprintf(out, fmt, ap);
|
||||
fputc('\n', out);
|
||||
fflush(out);
|
||||
}
|
||||
|
||||
void log_msg(const char *lv, const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vlog(lv, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
int path_join(const char *base, const char *name, char *buf, size_t n)
|
||||
{
|
||||
if (!base) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!name || !name[0]) {
|
||||
if (snprintf(buf, n, "%s", base) >= (int)n) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!base[0] || (base[0] == '/' && !base[1])) {
|
||||
if (snprintf(buf, n, "/%s", name) >= (int)n) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
size_t len = strlen(base);
|
||||
if (base[len - 1] == '/') {
|
||||
if (snprintf(buf, n, "%s%s", base, name) >= (int)n) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
if (snprintf(buf, n, "%s/%s", base, name) >= (int)n) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool path_exists(const char *p)
|
||||
{
|
||||
struct stat st;
|
||||
return (stat(p, &st) == 0);
|
||||
}
|
||||
|
||||
bool path_is_dir(const char *p)
|
||||
{
|
||||
struct stat st;
|
||||
return (stat(p, &st) == 0) && S_ISDIR(st.st_mode);
|
||||
}
|
||||
|
||||
bool path_is_symlink(const char *p)
|
||||
{
|
||||
struct stat st;
|
||||
return (lstat(p, &st) == 0) && S_ISLNK(st.st_mode);
|
||||
}
|
||||
|
||||
int mkdir_p(const char *dir)
|
||||
{
|
||||
if (!dir || !dir[0]) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
if (stat(dir, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode))
|
||||
return 0;
|
||||
LOGE("%s is not directory", dir);
|
||||
errno = ENOTDIR;
|
||||
return -1;
|
||||
}
|
||||
|
||||
char tmp[PATH_MAX];
|
||||
if (strlen(dir) >= sizeof(tmp)) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
strcpy(tmp, dir);
|
||||
char *s = strrchr(tmp, '/');
|
||||
if (s && s != tmp) {
|
||||
*s = 0;
|
||||
if (mkdir_p(tmp) != 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mkdir(dir, 0755) == 0 || errno == EEXIST)
|
||||
return 0;
|
||||
|
||||
LOGE("mkdir %s: %s", dir, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool is_rw_tmpfs(const char *path)
|
||||
{
|
||||
if (!path_is_dir(path))
|
||||
return false;
|
||||
|
||||
struct statfs s;
|
||||
if (statfs(path, &s) < 0)
|
||||
return false;
|
||||
if ((unsigned long)s.f_type != (unsigned long)TMPFS_MAGIC)
|
||||
return false;
|
||||
|
||||
char tmpl[PATH_MAX];
|
||||
if (path_join(path, ".magic_mount_testXXXXXX", tmpl, sizeof(tmpl)) != 0)
|
||||
return false;
|
||||
|
||||
int fd = mkstemp(tmpl);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
close(fd);
|
||||
unlink(tmpl);
|
||||
return true;
|
||||
}
|
||||
|
||||
void free_string_array(char ***arr, int *count)
|
||||
{
|
||||
if (!arr || !*arr || !count)
|
||||
return;
|
||||
for (int i = 0; i < *count; ++i)
|
||||
free((*arr)[i]);
|
||||
free(*arr);
|
||||
*arr = NULL;
|
||||
*count = 0;
|
||||
}
|
||||
|
||||
const char *select_auto_tempdir(char buf[PATH_MAX])
|
||||
{
|
||||
const char *tmp_list[] = { "/mnt/vendor", "/mnt", "/debug_ramdisk" };
|
||||
size_t n = sizeof(tmp_list) / sizeof(tmp_list[0]);
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const char *base = tmp_list[i];
|
||||
if (!is_rw_tmpfs(base))
|
||||
continue;
|
||||
if (path_join(base, ".magic_mount", buf, PATH_MAX) != 0)
|
||||
continue;
|
||||
|
||||
LOGI("auto tempdir selected: %s (from %s)", buf, base);
|
||||
return buf;
|
||||
}
|
||||
|
||||
LOGW("no rw tmpfs found in candidates, fallback to %s",
|
||||
DEFAULT_TEMP_DIR);
|
||||
return DEFAULT_TEMP_DIR;
|
||||
}
|
||||
|
||||
int set_selinux(const char *path, const char *con)
|
||||
{
|
||||
if (!path || !con) {
|
||||
LOGD("set_selinux called with path=%p, con=%p (skip)",
|
||||
(void *)path, (void *)con);
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOGD("set_selinux(%s, \"%s\")", path, con);
|
||||
|
||||
if (lsetxattr(path, SELINUX_XATTR, con, strlen(con), 0) < 0) {
|
||||
LOGW("setcon %s: %s", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOGD("set_selinux(%s) ok", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int get_selinux(const char *path, char **out)
|
||||
{
|
||||
*out = NULL;
|
||||
if (!path) {
|
||||
LOGD("get_selinux called with NULL path");
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOGD("get_selinux(%s)", path);
|
||||
|
||||
ssize_t sz = lgetxattr(path, SELINUX_XATTR, NULL, 0);
|
||||
if (sz < 0) {
|
||||
LOGW("getcon %s: %s", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
char *buf = malloc((size_t)sz + 1);
|
||||
if (!buf) {
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
|
||||
ssize_t r = lgetxattr(path, SELINUX_XATTR, buf, (size_t)sz);
|
||||
if (r < 0) {
|
||||
LOGW("getcon %s (read): %s", path, strerror(errno));
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
buf[r] = '\0';
|
||||
*out = buf;
|
||||
|
||||
LOGD("get_selinux(%s) -> \"%s\"", path, buf);
|
||||
return 0;
|
||||
}
|
||||
61
src/utils.h
Normal file
61
src/utils.h
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#ifndef UTILS_H
|
||||
#define UTILS_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#define SELINUX_XATTR "security.selinux"
|
||||
#define DEFAULT_TEMP_DIR "/dev/.magic_mount"
|
||||
|
||||
#ifndef TMPFS_MAGIC
|
||||
#define TMPFS_MAGIC 0x01021994
|
||||
#endif
|
||||
|
||||
#ifndef PATH_MAX
|
||||
#define PATH_MAX 4096
|
||||
#endif
|
||||
|
||||
/* from linux/sched.h normally */
|
||||
int setns(int fd, int nstype);
|
||||
|
||||
extern FILE *g_log_file;
|
||||
extern int g_log_level; /* 0=ERROR,1=WARN,2=INFO,3=DEBUG */
|
||||
|
||||
void log_msg(const char *lv, const char *fmt, ...);
|
||||
|
||||
#define LV_ERROR 0
|
||||
#define LV_WARN 1
|
||||
#define LV_INFO 2
|
||||
#define LV_DEBUG 3
|
||||
|
||||
#define LOG(lv, ...) \
|
||||
do { \
|
||||
if ((lv) <= g_log_level) \
|
||||
log_msg((lv) == LV_ERROR ? "ERROR" : \
|
||||
(lv) == LV_WARN ? "WARN" : \
|
||||
(lv) == LV_INFO ? "INFO" : \
|
||||
"DEBUG", \
|
||||
__VA_ARGS__); \
|
||||
} while (0)
|
||||
|
||||
#define LOGE(...) LOG(LV_ERROR, __VA_ARGS__)
|
||||
#define LOGW(...) LOG(LV_WARN, __VA_ARGS__)
|
||||
#define LOGI(...) LOG(LV_INFO, __VA_ARGS__)
|
||||
#define LOGD(...) LOG(LV_DEBUG, __VA_ARGS__)
|
||||
|
||||
int path_join(const char *base, const char *name, char *buf, size_t n);
|
||||
bool path_exists(const char *p);
|
||||
bool path_is_dir(const char *p);
|
||||
bool path_is_symlink(const char *p);
|
||||
int mkdir_p(const char *dir);
|
||||
|
||||
const char *select_auto_tempdir(char buf[PATH_MAX]);
|
||||
|
||||
void free_string_array(char ***arr, int *count);
|
||||
|
||||
int set_selinux(const char *path, const char *con);
|
||||
int get_selinux(const char *path, char **out);
|
||||
|
||||
#endif /* UTILS_H */
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
moduledir = "/data/adb/modules/"
|
||||
mountsource = "KSU"
|
||||
logfile = "/data/adb/magic_mount/mm.log"
|
||||
verbose = false
|
||||
partitions = []
|
||||
|
|
@ -6,19 +6,21 @@ ui_print "- Detecting device architecture..."
|
|||
ABI=$(grep_get_prop ro.product.cpu.abi)
|
||||
ui_print "- Detected ABI: $ABI"
|
||||
|
||||
# Select the correct binary based on architecture
|
||||
|
||||
# Select the correct binary based on architecture
|
||||
case "$ABI" in
|
||||
arm64-v8a)
|
||||
ui_print "- Selected architecture: ARM64"
|
||||
ARCH_BINARY="arm64-v8a/magic_mount_rs"
|
||||
ARCH_BINARY="mm_arm64"
|
||||
;;
|
||||
armeabi-v7a)
|
||||
ui_print "- Selected architecture: ARM"
|
||||
ARCH_BINARY="armeabi-v7a/magic_mount_rs"
|
||||
ARCH_BINARY="mm_armv7"
|
||||
;;
|
||||
x86_64)
|
||||
ui_print "- Selected architecture: AMD64"
|
||||
ARCH_BINARY="x86_64/magic_mount_rs"
|
||||
ARCH_BINARY="mm_amd64"
|
||||
;;
|
||||
*)
|
||||
abort "! Unsupported architecture: $ABI"
|
||||
|
|
@ -45,9 +47,9 @@ ui_print "- Architecture-specific binary installed successfully"
|
|||
|
||||
mkdir -p /data/adb/magic_mount
|
||||
|
||||
if [ ! -f /data/adb/magic_mount/config.toml ] ; then
|
||||
if [ ! -f /data/adb/magic_mount/mm.conf ] ; then
|
||||
ui_print "- Add default config"
|
||||
cat "$MODPATH/config.toml" > /data/adb/magic_mount/config.toml
|
||||
cat "$MODPATH/mm.conf" > /data/adb/magic_mount/mm.conf
|
||||
fi
|
||||
|
||||
ui_print "- Installation complete"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,24 @@ if [ ! -f "$BINARY" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
MM_LOG_FILE=$(busybox awk -F= '
|
||||
/^[[:space:]]*log_file[[:space:]]*=/ {
|
||||
val=$2
|
||||
sub(/#.*/, "", val)
|
||||
gsub(/^[ \t"]+|[ \t"]+$/, "", val)
|
||||
print val
|
||||
}' /data/adb/magic_mount/mm.conf)
|
||||
|
||||
if [ -f "$MM_LOG_FILE" ]; then
|
||||
mv "$MM_LOG_FILE" "$MM_LOG_FILE".old
|
||||
fi
|
||||
|
||||
# Set environment variables
|
||||
export MODULE_METADATA_DIR="/data/adb/modules"
|
||||
|
||||
log "Metadata directory: $MODULE_METADATA_DIR"
|
||||
log "Executing $BINARY"
|
||||
|
||||
$BINARY
|
||||
|
||||
EXIT_CODE=$?
|
||||
|
|
|
|||
5
template/mm.conf
Normal file
5
template/mm.conf
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Magic Mount Configuration File
|
||||
module_dir=/data/adb/modules
|
||||
mount_source=KSU
|
||||
log_file=/data/adb/magic_mount/mm.log
|
||||
debug=true
|
||||
|
|
@ -16,7 +16,7 @@ type ConfigState = {
|
|||
};
|
||||
|
||||
const DEFAULT_CONFIG: ConfigState = {
|
||||
moduledir: "/data/adb/modules/",
|
||||
moduledir: "/data/adb/modules",
|
||||
tempdir: "",
|
||||
mountsource: "KSU",
|
||||
logfile: "/data/adb/magic_mount/mm.log",
|
||||
|
|
@ -24,8 +24,7 @@ const DEFAULT_CONFIG: ConfigState = {
|
|||
partitions: [],
|
||||
};
|
||||
|
||||
const CONFIG_PATH = "/data/adb/magic_mount/config.toml";
|
||||
const LOG_BASE = "/data/adb/magic_mount";
|
||||
const CONFIG_PATH = "/data/adb/magic_mount/mm.conf";
|
||||
|
||||
const App: React.FC = () => {
|
||||
// ---------- i18n ----------
|
||||
|
|
@ -54,7 +53,8 @@ const App: React.FC = () => {
|
|||
const [configMessage, setConfigMessage] = useState<string | null>(null);
|
||||
|
||||
// ---------- log state ----------
|
||||
const [logFile, setLogFile] = useState<"mm.log" | "mm.log.old">("mm.log");
|
||||
type LogSelection = "current" | "old";
|
||||
const [logSelection, setLogSelection] = useState<LogSelection>("current");
|
||||
const [logContent, setLogContent] = useState("");
|
||||
const [logLoading, setLogLoading] = useState(false);
|
||||
const [logError, setLogError] = useState<string | null>(null);
|
||||
|
|
@ -63,80 +63,88 @@ const App: React.FC = () => {
|
|||
const [partitionInput, setPartitionInput] = useState("");
|
||||
|
||||
// ---------- helpers ----------
|
||||
const isTrueValue = (v: string): boolean => {
|
||||
const s = v.trim().toLowerCase();
|
||||
return s === "1" || s === "true" || s === "yes" || s === "on";
|
||||
};
|
||||
|
||||
const parseTOMLConfig = (text: string): ConfigState | null => {
|
||||
const parseKvConfig = (text: string): ConfigState | null => {
|
||||
try {
|
||||
const result: ConfigState = { ...DEFAULT_CONFIG };
|
||||
const lines = text.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
|
||||
const match = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
|
||||
if (!match) continue;
|
||||
const eqIndex = line.indexOf("=");
|
||||
if (eqIndex < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, key, value] = match;
|
||||
const cleanValue = value.replace(/^["']|["']$/g, "").trim();
|
||||
let key = line.slice(0, eqIndex).trim();
|
||||
let value = line.slice(eqIndex + 1).trim();
|
||||
|
||||
if (!key || !value) continue;
|
||||
|
||||
switch (key) {
|
||||
case "moduledir":
|
||||
case "mountsource":
|
||||
case "logfile":
|
||||
result[key] = cleanValue;
|
||||
case "module_dir":
|
||||
result.moduledir = value;
|
||||
break;
|
||||
case "tempdir":
|
||||
result.tempdir = cleanValue;
|
||||
case "temp_dir":
|
||||
result.tempdir = value;
|
||||
break;
|
||||
case "verbose":
|
||||
result.verbose = cleanValue === "true";
|
||||
case "mount_source":
|
||||
result.mountsource = value;
|
||||
break;
|
||||
case "log_file":
|
||||
result.logfile = value;
|
||||
break;
|
||||
case "debug":
|
||||
result.verbose = isTrueValue(value);
|
||||
break;
|
||||
case "partitions":
|
||||
// 解析数组格式: ["item1", "item2"] 或 []
|
||||
const arrayMatch = cleanValue.match(/^\[(.*)\]$/);
|
||||
if (arrayMatch) {
|
||||
const items = arrayMatch[1]
|
||||
.split(",")
|
||||
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
||||
.filter((s) => s);
|
||||
result.partitions = items;
|
||||
}
|
||||
result.partitions = value
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
break;
|
||||
default:
|
||||
// unknown key: ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error("Failed to parse TOML:", e);
|
||||
console.error("Failed to parse config:", e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const serializeTOMLConfig = (cfg: ConfigState): string => {
|
||||
const lines: string[] = [
|
||||
"# Magic Mount Configuration File",
|
||||
"# Generated by Web UI",
|
||||
"",
|
||||
`moduledir = "${cfg.moduledir}"`,
|
||||
];
|
||||
const serializeKvConfig = (cfg: ConfigState): string => {
|
||||
const lines: string[] = ["# Magic Mount Configuration File", "# Generated by Web UI", ""];
|
||||
|
||||
const moduleDir = cfg.moduledir || DEFAULT_CONFIG.moduledir;
|
||||
const mountSource = cfg.mountsource || DEFAULT_CONFIG.mountsource;
|
||||
const logFile = cfg.logfile || DEFAULT_CONFIG.logfile;
|
||||
|
||||
lines.push(`module_dir=${moduleDir}`);
|
||||
|
||||
// 只有非空值才写入
|
||||
if (cfg.tempdir) {
|
||||
lines.push(`tempdir = "${cfg.tempdir}"`);
|
||||
lines.push(`temp_dir=${cfg.tempdir}`);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
`mountsource = "${cfg.mountsource}"`,
|
||||
`logfile = "${cfg.logfile}"`,
|
||||
`verbose = ${cfg.verbose}`
|
||||
);
|
||||
lines.push(`mount_source=${mountSource}`);
|
||||
|
||||
if (logFile) {
|
||||
lines.push(`log_file=${logFile}`);
|
||||
}
|
||||
|
||||
lines.push(`debug=${cfg.verbose ? "true" : "false"}`);
|
||||
|
||||
// 分区数组
|
||||
if (cfg.partitions.length > 0) {
|
||||
const partStr = cfg.partitions.map((p) => `"${p}"`).join(", ");
|
||||
lines.push(`partitions = [${partStr}]`);
|
||||
} else {
|
||||
lines.push("partitions = []");
|
||||
lines.push(`partitions=${cfg.partitions.join(",")}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
|
|
@ -149,39 +157,39 @@ const App: React.FC = () => {
|
|||
|
||||
try {
|
||||
const { errno, stdout, stderr } = await exec(
|
||||
`[ -f "${CONFIG_PATH}" ] && cat "${CONFIG_PATH}" || echo ""`
|
||||
`[ -f "${CONFIG_PATH}" ] && cat "${CONFIG_PATH}" || echo ""`,
|
||||
);
|
||||
|
||||
if (errno !== 0) {
|
||||
console.error(stderr);
|
||||
setConfig(DEFAULT_CONFIG);
|
||||
setPartitionInput("");
|
||||
setConfigMessage(L.configReadError);
|
||||
setConfigMessage(L.config.loadError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stdout.trim()) {
|
||||
setConfig(DEFAULT_CONFIG);
|
||||
setPartitionInput("");
|
||||
setConfigMessage(L.configDefaultUsed);
|
||||
setConfigMessage(L.config.loadDefault);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseTOMLConfig(stdout);
|
||||
const parsed = parseKvConfig(stdout);
|
||||
if (parsed) {
|
||||
setConfig(parsed);
|
||||
setPartitionInput(parsed.partitions.join(", "));
|
||||
setConfigMessage(L.configLoaded);
|
||||
setConfigMessage(L.config.loadSuccess);
|
||||
} else {
|
||||
setConfig(DEFAULT_CONFIG);
|
||||
setPartitionInput("");
|
||||
setConfigMessage("Invalid TOML format");
|
||||
setConfigMessage(L.config.loadError);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setConfig(DEFAULT_CONFIG);
|
||||
setPartitionInput("");
|
||||
setConfigMessage(L.configReadError);
|
||||
setConfigMessage(L.config.loadError);
|
||||
} finally {
|
||||
setConfigLoading(false);
|
||||
}
|
||||
|
|
@ -198,9 +206,9 @@ const App: React.FC = () => {
|
|||
.map((s) => s.trim())
|
||||
.filter((s) => s);
|
||||
|
||||
const configToSave = { ...config, partitions };
|
||||
const configToSave: ConfigState = { ...config, partitions };
|
||||
|
||||
const content = serializeTOMLConfig(configToSave);
|
||||
const content = serializeKvConfig(configToSave);
|
||||
const escapedContent = content.replace(/'/g, "'\\''");
|
||||
|
||||
const shell = `
|
||||
|
|
@ -212,14 +220,14 @@ const App: React.FC = () => {
|
|||
|
||||
if (errno !== 0) {
|
||||
console.error(stderr);
|
||||
setConfigMessage(L.configSaveFailed);
|
||||
setConfigMessage(L.config.saveFailed);
|
||||
} else {
|
||||
setConfig(configToSave);
|
||||
setConfigMessage(L.configSaved);
|
||||
setConfigMessage(L.config.saveSuccess);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setConfigMessage(L.configSaveError);
|
||||
setConfigMessage(L.config.saveError);
|
||||
} finally {
|
||||
setConfigSaving(false);
|
||||
}
|
||||
|
|
@ -231,20 +239,22 @@ const App: React.FC = () => {
|
|||
setLogError(null);
|
||||
setLogContent("");
|
||||
try {
|
||||
const fullPath = `${LOG_BASE}/${logFile}`;
|
||||
const basePath = config.logfile || DEFAULT_CONFIG.logfile;
|
||||
const fullPath = logSelection === "current" ? basePath : `${basePath}.old`;
|
||||
|
||||
const { errno, stdout, stderr } = await exec(
|
||||
`[ -f "${fullPath}" ] && cat "${fullPath}" || echo "Log file not found: ${fullPath}"`
|
||||
`[ -f "${fullPath}" ] && cat "${fullPath}" || echo "Log file not found"`,
|
||||
);
|
||||
|
||||
if (errno !== 0) {
|
||||
console.error(stderr);
|
||||
setLogError(`${L.logReadFailed} (errno=${errno})`);
|
||||
setLogError(L.logs.readFailed + ` (errno=${errno})`);
|
||||
} else {
|
||||
setLogContent(stdout || "");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setLogError(L.logReadException);
|
||||
setLogError(L.logs.readException);
|
||||
} finally {
|
||||
setLogLoading(false);
|
||||
}
|
||||
|
|
@ -262,24 +272,20 @@ const App: React.FC = () => {
|
|||
loadLog();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeTab, logFile]);
|
||||
}, [activeTab, logSelection, config.logfile]);
|
||||
|
||||
// ---------- render tabs ----------
|
||||
|
||||
const renderConfigTab = () => (
|
||||
<div className="card">
|
||||
<h2>{L.config}</h2>
|
||||
<h2>{L.config.title}</h2>
|
||||
|
||||
{configLoading && <p className="hint">Loading...</p>}
|
||||
{configMessage && (
|
||||
<p className={configMessage.includes("Failed") || configMessage.includes("Invalid") ? "error" : "hint"}>
|
||||
{configMessage}
|
||||
</p>
|
||||
)}
|
||||
{configMessage && <p className="hint">{configMessage}</p>}
|
||||
|
||||
{/* Verbose switch */}
|
||||
<div className="field">
|
||||
<label>Verbose Logging</label>
|
||||
<label>{L.config.verboseLabel}</label>
|
||||
<div className="loglevel-switch">
|
||||
<button
|
||||
className={!config.verbose ? "lv-btn active" : "lv-btn"}
|
||||
|
|
@ -290,7 +296,7 @@ const App: React.FC = () => {
|
|||
}))
|
||||
}
|
||||
>
|
||||
Off
|
||||
{L.config.verboseOff}
|
||||
</button>
|
||||
<button
|
||||
className={config.verbose ? "lv-btn active" : "lv-btn"}
|
||||
|
|
@ -301,14 +307,14 @@ const App: React.FC = () => {
|
|||
}))
|
||||
}
|
||||
>
|
||||
On
|
||||
{L.config.verboseOn}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Module Directory */}
|
||||
<div className="field">
|
||||
<label>moduledir</label>
|
||||
<label>{L.config.moduleDir}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.moduledir}
|
||||
|
|
@ -324,7 +330,7 @@ const App: React.FC = () => {
|
|||
|
||||
{/* Temp Directory */}
|
||||
<div className="field">
|
||||
<label>tempdir</label>
|
||||
<label>{L.config.tempDir}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.tempdir}
|
||||
|
|
@ -340,7 +346,7 @@ const App: React.FC = () => {
|
|||
|
||||
{/* Mount Source */}
|
||||
<div className="field">
|
||||
<label>mountsource</label>
|
||||
<label>{L.config.mountSource}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.mountsource}
|
||||
|
|
@ -356,7 +362,7 @@ const App: React.FC = () => {
|
|||
|
||||
{/* Log File */}
|
||||
<div className="field">
|
||||
<label>logfile</label>
|
||||
<label>{L.config.logFile}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={config.logfile}
|
||||
|
|
@ -372,64 +378,60 @@ const App: React.FC = () => {
|
|||
|
||||
{/* Partitions */}
|
||||
<div className="field">
|
||||
<label>partitions</label>
|
||||
<label>{L.config.partitions}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={partitionInput}
|
||||
placeholder="e.g., mi_ext,my_stock"
|
||||
placeholder="mi_ext,my_stock"
|
||||
onChange={(e) => setPartitionInput(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<button onClick={loadConfig} disabled={configLoading || configSaving}>
|
||||
{L.reload}
|
||||
{L.config.reload}
|
||||
</button>
|
||||
<button
|
||||
className="primary"
|
||||
onClick={saveConfig}
|
||||
disabled={configSaving || configLoading}
|
||||
>
|
||||
{configSaving ? "Saving..." : L.saveConfig}
|
||||
<button className="primary" onClick={saveConfig} disabled={configSaving || configLoading}>
|
||||
{configSaving ? "Saving..." : L.config.save}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="path">
|
||||
Config Path: {CONFIG_PATH}
|
||||
{L.config.pathLabel}: {CONFIG_PATH}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderLogTab = () => (
|
||||
<div className="card">
|
||||
<h2>{L.logView}</h2>
|
||||
const renderLogTab = () => {
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>{L.logs.title}</h2>
|
||||
|
||||
<div className="field">
|
||||
<label>{L.selectLog}</label>
|
||||
<div className="log-select-row">
|
||||
<select
|
||||
value={logFile}
|
||||
onChange={(e) =>
|
||||
setLogFile(e.target.value as "mm.log" | "mm.log.old")
|
||||
}
|
||||
>
|
||||
<option value="mm.log">mm.log</option>
|
||||
<option value="mm.log.old">mm.log.old</option>
|
||||
</select>
|
||||
<div className="field">
|
||||
<label>{L.logs.select}</label>
|
||||
<div className="log-select-row">
|
||||
<select
|
||||
value={logSelection}
|
||||
onChange={(e) => setLogSelection(e.target.value as LogSelection)}
|
||||
>
|
||||
<option value="current">{L.logs.current}</option>
|
||||
<option value="old">{L.logs.old}</option>
|
||||
</select>
|
||||
|
||||
<button className="refresh-btn" onClick={loadLog} disabled={logLoading}>
|
||||
{logLoading ? "Loading..." : L.refreshLog}
|
||||
</button>
|
||||
<button className="refresh-btn" onClick={loadLog} disabled={logLoading}>
|
||||
{logLoading ? "Loading..." : L.logs.refresh}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{logError && <p className="error">{logError}</p>}
|
||||
|
||||
<pre className="log-view">
|
||||
{logLoading && !logContent ? "Loading..." : logContent || L.logs.empty}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{logError && <p className="error">{logError}</p>}
|
||||
|
||||
<pre className="log-view">
|
||||
{logLoading && !logContent ? "Loading..." : logContent || L.emptyLog}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-root">
|
||||
|
|
@ -437,19 +439,13 @@ const App: React.FC = () => {
|
|||
<div className="top-bar">
|
||||
<div className="title">Magic Mount</div>
|
||||
|
||||
<select
|
||||
className="lang-select"
|
||||
value={lang}
|
||||
onChange={handleLangChange}
|
||||
>
|
||||
<select className="lang-select" value={lang} onChange={handleLangChange}>
|
||||
<option value="en">English</option>
|
||||
<option value="zh">中文</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="app-main">
|
||||
{activeTab === "config" ? renderConfigTab() : renderLogTab()}
|
||||
</div>
|
||||
<div className="app-main">{activeTab === "config" ? renderConfigTab() : renderLogTab()}</div>
|
||||
|
||||
{/* bottom tabs */}
|
||||
<div className="bottom-bar">
|
||||
|
|
@ -457,13 +453,13 @@ const App: React.FC = () => {
|
|||
className={activeTab === "config" ? "tab-btn active" : "tab-btn"}
|
||||
onClick={() => setActiveTab("config")}
|
||||
>
|
||||
{L.configTab}
|
||||
{L.tabs.config}
|
||||
</button>
|
||||
<button
|
||||
className={activeTab === "log" ? "tab-btn active" : "tab-btn"}
|
||||
onClick={() => setActiveTab("log")}
|
||||
>
|
||||
{L.logTab}
|
||||
{L.tabs.logs}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* For KernelSU WebUI */
|
||||
@import url('https://mui.kernelsu.org/internal/insets.css');
|
||||
@import url("https://mui.kernelsu.org/internal/insets.css");
|
||||
|
||||
/* ------- Nord Base Colors ------- */
|
||||
:root {
|
||||
|
|
@ -35,8 +35,8 @@ body {
|
|||
margin: 0;
|
||||
background: var(--nord0);
|
||||
color: var(--nord6);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", Inter,
|
||||
"Segoe UI", Arial, sans-serif;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "SF Pro Text", Inter, "Segoe UI", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.app-root {
|
||||
|
|
@ -279,5 +279,5 @@ button:disabled {
|
|||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--nord6);
|
||||
margin-right: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,77 @@
|
|||
{
|
||||
"en": {
|
||||
"config": "Configuration",
|
||||
"reload": "Reload",
|
||||
"saveConfig": "Save",
|
||||
"loglevel": "Log Level",
|
||||
"debug": "debug",
|
||||
"info": "info",
|
||||
"logView": "Log Viewer",
|
||||
"selectLog": "Log File",
|
||||
"refreshLog": "Refresh Log",
|
||||
"emptyLog": "(empty)",
|
||||
"configTab": "Config",
|
||||
"logTab": "Logs",
|
||||
"configLoaded": "Config loaded from file.",
|
||||
"configDefaultUsed": "No config file found, using defaults.",
|
||||
"configReadError": "Failed to read config, using defaults.",
|
||||
"configSaved": "Config saved.",
|
||||
"configSaveFailed": "Failed to save config. Check logs or permissions.",
|
||||
"configSaveError": "Error occurred while saving config.",
|
||||
"logReadFailed": "Failed to read log",
|
||||
"logReadException": "Exception occurred while reading log.",
|
||||
"configPathLabel": "Config file path"
|
||||
"tabs": {
|
||||
"config": "Config",
|
||||
"logs": "Logs"
|
||||
},
|
||||
"config": {
|
||||
"title": "Configuration",
|
||||
"reload": "Reload",
|
||||
"save": "Save",
|
||||
"loadSuccess": "Configuration loaded.",
|
||||
"loadDefault": "No config file found, using defaults.",
|
||||
"loadError": "Failed to read config, using default values.",
|
||||
"saveSuccess": "Configuration saved.",
|
||||
"saveFailed": "Failed to save configuration.",
|
||||
"saveError": "An error occurred while saving.",
|
||||
"pathLabel": "Config file path",
|
||||
|
||||
"moduleDir": "Module Directory",
|
||||
"tempDir": "Temporary Directory",
|
||||
"mountSource": "Mount Source",
|
||||
"logFile": "Log File",
|
||||
"partitions": "Partitions",
|
||||
"verboseLabel": "Log Level",
|
||||
"verboseOff": "Info",
|
||||
"verboseOn": "Debug"
|
||||
},
|
||||
"logs": {
|
||||
"title": "Log Viewer",
|
||||
"select": "Log File",
|
||||
"refresh": "Refresh",
|
||||
"empty": "(empty)",
|
||||
"readFailed": "Failed to read log file.",
|
||||
"readException": "An exception occurred while reading log.",
|
||||
"current": "Current Log",
|
||||
"old": "Old Log"
|
||||
}
|
||||
},
|
||||
|
||||
"zh": {
|
||||
"config": "配置",
|
||||
"reload": "重载",
|
||||
"saveConfig": "保存",
|
||||
"loglevel": "日志级别",
|
||||
"debug": "调试",
|
||||
"info": "信息",
|
||||
"logView": "日志查看",
|
||||
"selectLog": "日志文件",
|
||||
"refreshLog": "刷新日志",
|
||||
"emptyLog": "(空日志)",
|
||||
"configTab": "配置",
|
||||
"logTab": "日志",
|
||||
"configLoaded": "已从配置文件加载。",
|
||||
"configDefaultUsed": "未找到配置文件,已使用默认配置。",
|
||||
"configReadError": "读取配置失败,已使用默认配置。",
|
||||
"configSaved": "配置已保存",
|
||||
"configSaveFailed": "保存失败,请查看日志或检查权限。",
|
||||
"configSaveError": "保存配置时发生错误。",
|
||||
"logReadFailed": "读取日志失败",
|
||||
"logReadException": "读取日志时发生异常。",
|
||||
"configPathLabel": "配置文件路径"
|
||||
"tabs": {
|
||||
"config": "配置",
|
||||
"logs": "日志"
|
||||
},
|
||||
"config": {
|
||||
"title": "配置",
|
||||
"reload": "重载",
|
||||
"save": "保存",
|
||||
"loadSuccess": "已从配置文件加载。",
|
||||
"loadDefault": "未找到配置文件,已使用默认配置。",
|
||||
"loadError": "读取配置失败,已使用默认配置。",
|
||||
"saveSuccess": "配置已保存。",
|
||||
"saveFailed": "保存失败,请检查权限或日志。",
|
||||
"saveError": "保存配置时发生错误。",
|
||||
"pathLabel": "配置文件路径",
|
||||
|
||||
"moduleDir": "模块目录",
|
||||
"tempDir": "临时目录",
|
||||
"mountSource": "挂载源",
|
||||
"logFile": "日志文件",
|
||||
"partitions": "分区列表",
|
||||
"verboseLabel": "日志级别",
|
||||
"verboseOff": "信息",
|
||||
"verboseOn": "调试"
|
||||
},
|
||||
"logs": {
|
||||
"title": "日志查看",
|
||||
"select": "日志文件",
|
||||
"refresh": "刷新",
|
||||
"empty": "(空日志)",
|
||||
"readFailed": "读取日志失败。",
|
||||
"readException": "读取日志时发生异常。",
|
||||
"current": "当前日志",
|
||||
"old": "旧日志"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue