>(path: P, con: &str) -> Result<()> {
32 | #[cfg(any(target_os = "linux", target_os = "android"))]
33 | {
34 | log::debug!("file: {},con: {}", path.as_ref().display(), con);
35 | xattr_set(&path, SELINUX_XATTR, con.as_bytes()).with_context(|| {
36 | format!(
37 | "Failed to change SELinux context for {}",
38 | path.as_ref().display()
39 | )
40 | })?;
41 | }
42 | Ok(())
43 | }
44 |
45 | #[cfg(any(target_os = "linux", target_os = "android"))]
46 | pub fn lgetfilecon(path: P) -> Result
47 | where
48 | P: AsRef,
49 | {
50 | let con = xattr_get(&path, SELINUX_XATTR)
51 | .with_context(|| {
52 | format!(
53 | "Failed to get SELinux context for {}",
54 | path.as_ref().display()
55 | )
56 | })?
57 | .with_context(|| {
58 | format!(
59 | "Failed to get SELinux context for {}",
60 | path.as_ref().display()
61 | )
62 | })?;
63 | let con = String::from_utf8_lossy(&con);
64 | Ok(con.to_string())
65 | }
66 |
67 | #[cfg(not(any(target_os = "linux", target_os = "android")))]
68 | pub fn lgetfilecon(path: P) -> Result
69 | where
70 | P: AsRef,
71 | {
72 | unimplemented!()
73 | }
74 |
75 | pub fn ensure_dir_exists(dir: P) -> Result<()>
76 | where
77 | P: AsRef,
78 | {
79 | let result = create_dir_all(&dir);
80 | if dir.as_ref().is_dir() && result.is_ok() {
81 | Ok(())
82 | } else {
83 | bail!("{} is not a regular directory", dir.as_ref().display())
84 | }
85 | }
86 |
87 | fn is_ok_empty(dir: P) -> bool
88 | where
89 | P: AsRef,
90 | {
91 | dir.as_ref()
92 | .read_dir()
93 | .is_ok_and(|mut entries| entries.next().is_none())
94 | }
95 |
96 | pub fn select_temp_dir() -> Result {
97 | for candidate in TMPFS_CANDIDATES {
98 | let path = Path::new(candidate);
99 |
100 | if !path.exists() {
101 | continue;
102 | }
103 |
104 | if is_ok_empty(path) {
105 | log::info!("selected tmpfs: {}", path.display(),);
106 | return Ok(path.to_path_buf());
107 | }
108 | }
109 |
110 | bail!(
111 | "no tmpfs found in candidates: {}",
112 | TMPFS_CANDIDATES.join(", ")
113 | )
114 | }
115 |
--------------------------------------------------------------------------------
/webui/src/components/MagicLogo.svelte:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | #![deny(clippy::all, clippy::pedantic)]
2 | #![warn(clippy::nursery)]
3 |
4 | mod config;
5 | mod defs;
6 | mod magic_mount;
7 | mod scanner;
8 | #[cfg(any(target_os = "linux", target_os = "android"))]
9 | mod try_umount;
10 | mod utils;
11 |
12 | use std::io::Write;
13 |
14 | use anyhow::{Context, Result};
15 | use env_logger::Builder;
16 | use mimalloc::MiMalloc;
17 | use rustix::mount::{MountFlags, mount};
18 |
19 | use crate::config::Config;
20 |
21 | #[global_allocator]
22 | static GLOBAL: MiMalloc = MiMalloc;
23 |
24 | fn init_logger(verbose: bool) {
25 | let level = if verbose {
26 | log::LevelFilter::Debug
27 | } else {
28 | log::LevelFilter::Info
29 | };
30 |
31 | let mut builder = Builder::new();
32 |
33 | builder.format(|buf, record| {
34 | writeln!(
35 | buf,
36 | "[{}] [{}] {}",
37 | record.level(),
38 | record.target(),
39 | record.args()
40 | )
41 | });
42 | builder.filter_level(level).init();
43 |
44 | log::info!("log level: {}", level.as_str());
45 | }
46 |
47 | fn main() -> Result<()> {
48 | let config = Config::load_default().unwrap_or_default();
49 |
50 | let args: Vec<_> = std::env::args().collect();
51 |
52 | if args.len() > 1 {
53 | match args[1].as_str() {
54 | "scan" => {
55 | let json_output = args.len() > 2 && args[2] == "--json";
56 |
57 | let modules = scanner::scan_modules(&config.moduledir);
58 |
59 | if json_output {
60 | let json = serde_json::to_string(&modules)?;
61 | println!("{json}");
62 | } else {
63 | for module in modules {
64 | println!("{}", module.id);
65 | }
66 | }
67 | return Ok(());
68 | }
69 | "version" => {
70 | println!("{{ \"version\": \"{}\" }}", env!("CARGO_PKG_VERSION"));
71 | return Ok(());
72 | }
73 | _ => {}
74 | }
75 | }
76 |
77 | init_logger(config.verbose);
78 |
79 | if std::env::var("KSU").is_err() {
80 | log::error!("only support KernelSU!!");
81 | }
82 |
83 | log::info!("Magic Mount Starting");
84 | log::info!("config info:\n{config}");
85 |
86 | log::debug!(
87 | "current selinux: {}",
88 | std::fs::read_to_string("/proc/self/attr/current")?
89 | );
90 |
91 | let tempdir = utils::select_temp_dir().context("failed to select temp dir automatically")?;
92 |
93 | utils::ensure_dir_exists(&tempdir)?;
94 |
95 | if let Err(e) = mount(
96 | &config.mountsource,
97 | &tempdir,
98 | "tmpfs",
99 | MountFlags::empty(),
100 | None,
101 | ) {
102 | log::error!("mount tmpfs failed: {e}");
103 | }
104 |
105 | let result = magic_mount::magic_mount(
106 | &tempdir,
107 | &config.moduledir,
108 | &config.mountsource,
109 | &config.partitions,
110 | config.umount,
111 | );
112 |
113 | match result {
114 | Ok(()) => {
115 | log::info!("Magic Mount Completed Successfully");
116 | Ok(())
117 | }
118 | Err(e) => {
119 | log::error!("Magic Mount Failed");
120 | for cause in e.chain() {
121 | log::error!("{cause:#?}");
122 | }
123 | log::error!("{:#?}", e.backtrace());
124 | Err(e)
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/webui/src/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "lang": { "display": "English" },
3 | "common": {
4 | "appName": "Magic Mount",
5 | "saving": "Saving...",
6 | "theme": "Toggle Theme",
7 | "language": "Language",
8 | "themeAuto": "Auto (System)",
9 | "themeLight": "Light Mode",
10 | "themeDark": "Dark Mode",
11 | "cancel": "Cancel",
12 | "reboot": "Reboot",
13 | "rebootTitle": "Reboot System?",
14 | "rebootConfirm": "Are you sure you want to reboot the device?",
15 | "rebootFailed": "Reboot failed",
16 | "resetSuccess": "Config reset successfully",
17 | "resetFailed": "Failed to reset config"
18 | },
19 | "tabs": {
20 | "status": "Status",
21 | "config": "Config",
22 | "modules": "Modules",
23 | "logs": "Logs",
24 | "info": "Info"
25 | },
26 | "status": {
27 | "sysInfoTitle": "System Details",
28 | "deviceTitle": "Device Info",
29 | "moduleTitle": "Modules",
30 | "moduleActive": "Active Modules",
31 | "modelLabel": "Model",
32 | "androidLabel": "Android",
33 | "kernelLabel": "Kernel",
34 | "selinuxLabel": "SELinux",
35 | "copy": "Copy Info"
36 | },
37 | "config": {
38 | "title": "Configuration",
39 | "verboseLabel": "Verbose Logging",
40 | "verboseOff": "Off",
41 | "verboseOn": "On",
42 | "umountLabel": "Unmount Modules",
43 | "umountOff": "Default",
44 | "umountOn": "Enabled",
45 | "moduleDir": "Module Directory",
46 | "moduleDirDesc": "Set the directory where modules are stored",
47 | "tempDir": "Temp Directory",
48 | "mountSource": "Mount Source",
49 | "mountSourceDesc": "Global mount source namespace (e.g. KSU)",
50 | "logFile": "Log File",
51 | "partitions": "Extra Partitions",
52 | "partitionsDesc": "Add partitions to mount",
53 | "autoPlaceholder": "Auto-select if empty",
54 | "reload": "Reload",
55 | "save": "Save Config",
56 | "reset": "Reset",
57 | "invalidPath": "Invalid path detected",
58 | "loadSuccess": "Config loaded successfully",
59 | "loadError": "Failed to load config",
60 | "loadDefault": "Loaded default config",
61 | "saveSuccess": "Configuration saved",
62 | "saveFailed": "Failed to save configuration",
63 | "fixBottomNav": "Fix Bottom Nav"
64 | },
65 | "modules": {
66 | "title": "Active Modules",
67 | "desc": "Modules listed here are detected and managed by Magic Mount.",
68 | "scanning": "Scanning...",
69 | "reload": "Refresh",
70 | "save": "Save Changes",
71 | "empty": "No active modules found",
72 | "scanError": "Failed to scan modules",
73 | "saveSuccess": "Saved",
74 | "saveFailed": "Failed to save",
75 | "searchPlaceholder": "Search modules...",
76 | "filterLabel": "Filter:",
77 | "filterAll": "All",
78 | "toggleError": "Failed to toggle"
79 | },
80 | "logs": {
81 | "title": "System Logs",
82 | "loading": "Loading logs...",
83 | "refresh": "Refresh",
84 | "empty": "Log file is empty",
85 | "readFailed": "Failed to read logs",
86 | "readException": "Error reading log file",
87 | "searchPlaceholder": "Search logs...",
88 | "filterLabel": "Level:",
89 | "levels": {
90 | "all": "All",
91 | "info": "Info",
92 | "warn": "Warn",
93 | "error": "Error"
94 | },
95 | "copy": "Copy",
96 | "copySuccess": "Copied to clipboard",
97 | "copyFail": "Failed to copy"
98 | },
99 | "info": {
100 | "title": "About",
101 | "projectLink": "Repository",
102 | "donate": "Donate",
103 | "contributors": "Contributors",
104 | "loading": "Loading contributors...",
105 | "loadFail": "Failed to load (API Rate Limit)",
106 | "noBio": "No bio available"
107 | }
108 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report / 错误报告
2 | description: Create a report to help us improve Meta-Hybrid Mount / 创建报告以帮助我们改进 Meta-Hybrid Mount
3 | title: "[Bug]: "
4 | labels: ["bug"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Please ensure you have checked existing issues before reporting.
10 | 请在报告之前确保您已检查过现有的 Issue。
11 |
12 | - type: input
13 | id: device
14 | attributes:
15 | label: Device Model & Android Version / 机型与安卓版本
16 | description: e.g., Xiaomi 13, Android 14
17 | placeholder: Xiaomi 13, Android 14 (HyperOS)
18 | validations:
19 | required: true
20 |
21 | - type: input
22 | id: kernel
23 | attributes:
24 | label: Kernel Version / 内核版本
25 | description: Output of `uname -r` / `uname -r` 的输出结果
26 | placeholder: 5.15.100-android14-11-g123456...
27 | validations:
28 | required: true
29 |
30 | - type: dropdown
31 | id: ksu_type
32 | attributes:
33 | label: KernelSU Type / KernelSU 类型
34 | options:
35 | - "GKI (Official Installer)"
36 | - "LKM (Loadable Kernel Module)"
37 | - "Legacy (Non-GKI / Custom Kernel)"
38 | - "Other / 其它"
39 | validations:
40 | required: true
41 |
42 | - type: dropdown
43 | id: susfs
44 | attributes:
45 | label: Is SUSFS Integrated? / 是否集成 SUSFS?
46 | description: Do you use suSFS in your kernel or as a module? / 您的内核或模块环境中是否集成了 suSFS?
47 | options:
48 | - "Yes / 是"
49 | - "No / 否"
50 | - "Unknown / 不清楚"
51 | validations:
52 | required: true
53 |
54 | - type: input
55 | id: module_version
56 | attributes:
57 | label: Meta-Hybrid Version / 模块版本
58 | description: The version of this module installed / 当前安装的模块版本
59 | placeholder: v1.0.2-r1
60 | validations:
61 | required: true
62 |
63 | - type: textarea
64 | id: description
65 | attributes:
66 | label: Issue Description / 问题描述
67 | description: A clear and concise description of what the bug is. / 请清晰简洁地描述出现的问题。
68 | placeholder: |
69 | When I enabled module X, the system bootlooped...
70 | 当我也启用了模块 X 时,系统发生了无限重启...
71 | validations:
72 | required: true
73 |
74 | - type: textarea
75 | id: Reproduce
76 | attributes:
77 | label: Issue Reproduce / 问题复现
78 | description: Describe how to reproduce the issue. / 描述如何复现该问题。
79 | placeholder: |
80 | 1. Install module X
81 | 2. Reboot the device
82 | 3. Observe the bootloop
83 |
84 | 1. 安装模块 X
85 | 2. 重启设备
86 | 3. 观察到无限重启
87 | validations:
88 | required: true
89 |
90 | - type: textarea
91 | id: logs
92 | attributes:
93 | label: Verbose Hybrid Mount Log / 详细运行日志
94 | description: |
95 | **CRITICAL / 重要**:
96 | 1. Go to WebUI -> Config, enable **"Verbose Log"** and click Save.
97 | 2. Reboot or reproduce the issue.
98 | 3. Paste the content of `/data/adb/meta-hybrid/daemon.log` here.
99 |
100 | 1. 请先在 WebUI -> Config (配置) 中开启 **"Verbose Log" (详细日志)** 并保存。
101 | 2. 重启或复现问题。
102 | 3. 将 `/data/adb/meta-hybrid/daemon.log` 的内容粘贴在下方。
103 | render: shell
104 | validations:
105 | required: true
106 |
107 | - type: markdown
108 | attributes:
109 | value: |
110 | ### Attachments / 附件
111 |
112 | **Please upload the following files by dragging and dropping them into the text area below:**
113 | **请将以下文件拖拽到下方的文本框中上传:**
114 |
115 | 1. **KernelSU Bugreport** (Required/必须):
116 | - Generate via KernelSU Manager -> Settings -> Bug Report.
117 | - 请通过 KernelSU 管理器 -> 设置 -> 调试报告 生成。
118 |
119 | 2. **Problematic Module Sample** (Required/必须):
120 | - The module `.zip` file that caused the issue.
121 | - 导致问题的模块 `.zip` 安装包。
122 |
123 | - type: textarea
124 | id: attachments
125 | attributes:
126 | label: Upload Area / 上传区域
127 | description: Drag & drop your KernelSU bugreport and module zip here. / 请将 KernelSU 调试报告和模块包拖拽至此处。
128 | validations:
129 | required: true
--------------------------------------------------------------------------------
/webui/src/lib/api.mock.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_CONFIG } from './constants';
2 | import type { MagicConfig, MagicModule, StorageUsage, SystemInfo, DeviceStatus } from './api';
3 |
4 | const MOCK_DELAY = 600;
5 |
6 | function delay(ms: number) {
7 | return new Promise(resolve => setTimeout(resolve, ms));
8 | }
9 |
10 | export const MockAPI = {
11 | loadConfig: async (): Promise => {
12 | await delay(MOCK_DELAY);
13 | console.log("[MockAPI] loadConfig");
14 | return {
15 | ...DEFAULT_CONFIG,
16 | moduledir: '/data/adb/modules',
17 | mountsource: 'KSU',
18 | verbose: true,
19 | umount: true,
20 | partitions: ['product', 'system_ext', 'vendor']
21 | };
22 | },
23 |
24 | saveConfig: async (config: MagicConfig) => {
25 | await delay(MOCK_DELAY);
26 | console.log("[MockAPI] saveConfig:", config);
27 | },
28 |
29 | scanModules: async (moduleDir: string): Promise => {
30 | await delay(MOCK_DELAY);
31 | console.log("[MockAPI] scanModules");
32 | return [
33 | {
34 | id: "youtube-revanced",
35 | name: "YouTube ReVanced",
36 | version: "18.20.39",
37 | author: "ReVanced Team",
38 | description: "YouTube ReVanced Module",
39 | is_mounted: true,
40 | mode: 'magic',
41 | rules: { default_mode: 'magic', paths: {} }
42 | },
43 | {
44 | id: "pixelfy-gphotos",
45 | name: "Pixelfy GPhotos",
46 | version: "2.1",
47 | author: "PixelProps",
48 | description: "Unlimited Google Photos backup for Pixel devices.",
49 | is_mounted: true,
50 | mode: 'magic',
51 | rules: { default_mode: 'magic', paths: {} }
52 | },
53 | {
54 | id: "sound-enhancer",
55 | name: "Sound Enhancer",
56 | version: "1.0",
57 | author: "AudioMod",
58 | description: "Improves system audio quality. Currently disabled.",
59 | is_mounted: false,
60 | mode: 'magic',
61 | rules: { default_mode: 'magic', paths: {} }
62 | }
63 | ];
64 | },
65 |
66 | readLogs: async (logPath?: string, lines?: number): Promise => {
67 | await delay(MOCK_DELAY);
68 | console.log("[MockAPI] readLogs");
69 | return `[I] Magic Mount Daemon v1.0.0 started
70 | [I] Mounting source: KSU
71 | [I] Loading config from /data/adb/magic_mount/config.toml
72 | [D] Verbose logging enabled
73 | [I] Scanned 3 modules
74 | [I] Mounting youtube-revanced... Success
75 | [I] Mounting pixelfy-gphotos... Success
76 | [W] Skipping sound-enhancer: disable file found
77 | [I] OverlayFS mounted on /system/product
78 | [I] OverlayFS mounted on /system/vendor
79 | [E] Failed to mount /system/my_custom_partition: No such file or directory
80 | [I] Daemon loop active`;
81 | },
82 |
83 | getStorageUsage: async (): Promise => {
84 | await delay(MOCK_DELAY);
85 | return {
86 | type: 'ext4',
87 | percent: '42%',
88 | size: '118 GB',
89 | used: '50 GB',
90 | hymofs_available: false
91 | };
92 | },
93 |
94 | getSystemInfo: async (): Promise => {
95 | await delay(MOCK_DELAY);
96 | return {
97 | kernel: '5.10.101-android12-9-00001-g532145',
98 | selinux: 'Enforcing',
99 | mountBase: '/data/adb/modules',
100 | activeMounts: ['youtube-revanced', 'pixelfy-gphotos']
101 | };
102 | },
103 |
104 | getDeviceStatus: async (): Promise => {
105 | await delay(MOCK_DELAY);
106 | return {
107 | model: 'Pixel 8 Pro (Mock)',
108 | android: '14',
109 | kernel: '5.10.101-mock',
110 | selinux: 'Enforcing'
111 | };
112 | },
113 |
114 | getVersion: async (): Promise => {
115 | await delay(MOCK_DELAY);
116 | return "1.2.0-mock";
117 | },
118 |
119 | reboot: async (): Promise => {
120 | console.log("[MockAPI] Reboot requested");
121 | alert("Reboot requested (Mock)");
122 | },
123 |
124 | openLink: async (url: string) => {
125 | console.log("[MockAPI] Open link:", url);
126 | window.open(url, '_blank');
127 | },
128 |
129 | fetchSystemColor: async (): Promise => {
130 | await delay(500);
131 | return '#50a48f';
132 | }
133 | };
--------------------------------------------------------------------------------
/webui/src/App.svelte:
--------------------------------------------------------------------------------
1 |
79 |
80 |
81 | {#if !isReady}
82 |
83 |
84 |
Loading...
85 |
86 | {:else}
87 |
88 |
94 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | {/if}
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/webui/src/components/ChipInput.svelte:
--------------------------------------------------------------------------------
1 |
34 |
58 |
--------------------------------------------------------------------------------
/webui/src/routes/StatusTab.css:
--------------------------------------------------------------------------------
1 | .dashboard-grid {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 16px;
5 | padding: 16px;
6 | padding-bottom: 80px;
7 | }
8 |
9 | /* Hero Card */
10 | .hero-card {
11 | background-color: var(--md-sys-color-primary-container);
12 | color: var(--md-sys-color-on-primary-container);
13 | border-radius: 28px;
14 | padding: 24px;
15 | display: flex;
16 | justify-content: space-between;
17 | align-items: flex-start;
18 | position: relative;
19 | overflow: hidden;
20 | /* Add subtle gradient */
21 | background-image: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 100%);
22 | }
23 |
24 | .hero-decoration {
25 | position: absolute;
26 | right: -20px;
27 | bottom: -30px;
28 | width: 140px;
29 | height: 140px;
30 | color: var(--md-sys-color-on-primary-container);
31 | opacity: 0.12;
32 | transform: rotate(-10deg);
33 | pointer-events: none;
34 | z-index: 0;
35 | }
36 |
37 | .hero-content {
38 | display: flex;
39 | flex-direction: column;
40 | gap: 24px;
41 | z-index: 1;
42 | }
43 |
44 | .hero-label-group {
45 | display: flex;
46 | align-items: center;
47 | gap: 12px;
48 | }
49 |
50 | .hero-icon-circle {
51 | width: 32px;
52 | height: 32px;
53 | border-radius: 50%;
54 | background-color: rgba(255, 255, 255, 0.2);
55 | display: flex;
56 | align-items: center;
57 | justify-content: center;
58 | color: inherit;
59 | }
60 |
61 | .hero-icon-circle md-icon {
62 | font-size: 18px;
63 | width: 18px;
64 | height: 18px;
65 | }
66 |
67 | .hero-title {
68 | font-size: 14px;
69 | font-weight: 500;
70 | text-transform: uppercase;
71 | letter-spacing: 0.5px;
72 | opacity: 0.9;
73 | }
74 |
75 | .hero-main-info {
76 | display: flex;
77 | flex-direction: column;
78 | }
79 |
80 | .device-model {
81 | font-size: 28px;
82 | line-height: 34px;
83 | font-weight: 400;
84 | margin-bottom: 8px;
85 | }
86 |
87 | .version-pill {
88 | display: inline-flex;
89 | align-items: center;
90 | background-color: rgba(255, 255, 255, 0.2);
91 | padding: 4px 12px;
92 | border-radius: 16px;
93 | font-size: 12px;
94 | font-weight: 500;
95 | width: fit-content;
96 | backdrop-filter: blur(4px);
97 | }
98 |
99 | .hero-actions {
100 | z-index: 2;
101 | --md-icon-button-icon-color: var(--md-sys-color-on-primary-container);
102 | --md-sys-color-on-surface-variant: var(--md-sys-color-on-primary-container);
103 | }
104 |
105 | /* Stats Row */
106 | .stats-row {
107 | display: grid;
108 | grid-template-columns: 1fr 1fr;
109 | gap: 12px;
110 | }
111 |
112 | .stat-card {
113 | background-color: var(--md-sys-color-surface-container);
114 | border-radius: 20px;
115 | padding: 20px;
116 | display: flex;
117 | flex-direction: column;
118 | justify-content: center;
119 | align-items: center;
120 | text-align: center;
121 | min-height: 100px;
122 | }
123 |
124 | .stat-value {
125 | font-size: 32px;
126 | line-height: 40px;
127 | color: var(--md-sys-color-primary);
128 | font-weight: 400;
129 | }
130 |
131 | .stat-label {
132 | font-size: 12px;
133 | color: var(--md-sys-color-on-surface-variant);
134 | margin-top: 4px;
135 | }
136 |
137 | /* Details Card */
138 | .details-card {
139 | background-color: var(--md-sys-color-surface-container);
140 | border-radius: 24px;
141 | padding: 24px;
142 | }
143 |
144 | .card-title {
145 | font-size: 20px;
146 | color: var(--md-sys-color-on-surface);
147 | margin-bottom: 20px;
148 | font-weight: 400;
149 | }
150 |
151 | .info-list {
152 | display: grid;
153 | grid-template-columns: 1fr 1fr;
154 | gap: 20px;
155 | }
156 |
157 | .info-item {
158 | display: flex;
159 | flex-direction: column;
160 | gap: 4px;
161 | }
162 |
163 | .info-item.full-width {
164 | grid-column: span 2;
165 | }
166 |
167 | .info-label {
168 | font-size: 12px;
169 | color: var(--md-sys-color-on-surface-variant);
170 | }
171 |
172 | .info-val {
173 | font-size: 15px;
174 | color: var(--md-sys-color-on-surface);
175 | font-weight: 500;
176 | word-break: break-all;
177 | }
178 |
179 | .info-val.mono {
180 | font-family: monospace;
181 | font-size: 13px;
182 | line-height: 1.4;
183 | opacity: 0.9;
184 | }
185 |
186 | .info-val.warn {
187 | color: var(--md-sys-color-error);
188 | }
189 | .reboot-btn {
190 | --md-filled-tonal-icon-button-container-color: var(--md-sys-color-error-container);
191 | --md-filled-tonal-icon-button-icon-color: var(--md-sys-color-on-error-container);
192 | --md-filled-tonal-icon-button-hover-state-layer-color: var(--md-sys-color-on-error-container);
193 | margin-right: 8px;
194 | }
--------------------------------------------------------------------------------
/webui/src/routes/LogsTab.css:
--------------------------------------------------------------------------------
1 | .logs-container-page {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 16px;
5 | padding: 16px;
6 | padding-bottom: 80px;
7 | height: 100%;
8 | box-sizing: border-box;
9 | }
10 |
11 | .logs-controls {
12 | display: flex;
13 | align-items: center;
14 | justify-content: space-between;
15 | gap: 12px;
16 | background-color: var(--md-sys-color-surface-container);
17 | border-radius: 9999px;
18 | padding: 0 8px 0 20px;
19 | height: 56px;
20 | flex-shrink: 0;
21 | }
22 |
23 | .search-wrapper {
24 | display: flex;
25 | align-items: center;
26 | gap: 12px;
27 | flex: 1;
28 | min-width: 0;
29 | }
30 |
31 | .search-icon {
32 | color: var(--md-sys-color-on-surface-variant);
33 | font-size: 20px;
34 | width: 20px;
35 | height: 20px;
36 | }
37 |
38 | .log-search-input {
39 | border: none;
40 | background: transparent;
41 | height: 100%;
42 | width: 100%;
43 | font-size: 14px;
44 | color: var(--md-sys-color-on-surface);
45 | outline: none;
46 | font-family: var(--md-ref-typeface-plain);
47 | }
48 |
49 | .log-search-input::placeholder {
50 | color: var(--md-sys-color-on-surface-variant);
51 | opacity: 0.7;
52 | }
53 |
54 | .controls-right {
55 | display: flex;
56 | align-items: center;
57 | gap: 8px;
58 | }
59 |
60 | .log-auto-group {
61 | display: flex;
62 | align-items: center;
63 | gap: 4px;
64 | }
65 |
66 | /* Adjust md-checkbox size implicitly via touch target if needed,
67 | but standard size is usually fine in toolbar */
68 | md-checkbox {
69 | --md-checkbox-container-shape: 4px;
70 | margin: 0;
71 | }
72 |
73 | .log-auto-label {
74 | font-size: 13px;
75 | color: var(--md-sys-color-on-surface-variant);
76 | cursor: pointer;
77 | white-space: nowrap;
78 | font-weight: 500;
79 | margin-right: 8px;
80 | }
81 |
82 | .log-divider {
83 | height: 24px;
84 | width: 1px;
85 | background: var(--md-sys-color-outline-variant);
86 | margin: 0 4px;
87 | opacity: 0.5;
88 | }
89 |
90 | .log-filter-select {
91 | background: transparent;
92 | border: none;
93 | color: var(--md-sys-color-primary);
94 | font-size: 13px;
95 | font-weight: 600;
96 | outline: none;
97 | cursor: pointer;
98 | padding: 0 12px;
99 | height: 40px;
100 | border-radius: 20px;
101 | }
102 |
103 | .log-filter-select:hover {
104 | background-color: var(--md-sys-color-surface-container-high);
105 | }
106 |
107 | /* Log Viewer Container */
108 | .log-viewer {
109 | background-color: var(--md-sys-color-surface-container-low);
110 | border-radius: 24px;
111 | padding: 16px;
112 | font-family: monospace;
113 | font-size: 11px;
114 | color: var(--md-sys-color-on-surface-variant);
115 | border: none;
116 | flex: 1;
117 | min-height: 0; /* Important for flex child scrolling */
118 | overflow-y: auto;
119 | white-space: pre-wrap;
120 | word-break: break-all;
121 | scroll-behavior: smooth;
122 | position: relative;
123 | }
124 |
125 | .log-skeleton-container {
126 | display: flex;
127 | flex-direction: column;
128 | gap: 8px;
129 | }
130 |
131 | .log-entry {
132 | margin-bottom: 4px;
133 | line-height: 1.5;
134 | border-bottom: 1px solid transparent;
135 | }
136 |
137 | /* Colors matching terminal output styles */
138 | .log-info { color: var(--md-sys-color-on-surface); }
139 | .log-warn { color: var(--md-sys-color-tertiary); }
140 | .log-debug { color: var(--md-sys-color-secondary); opacity: 0.8; }
141 | .log-error { color: var(--md-sys-color-error); font-weight: 700; }
142 |
143 | .log-empty-state {
144 | height: 100%;
145 | display: flex;
146 | align-items: center;
147 | justify-content: center;
148 | opacity: 0.6;
149 | font-size: 14px;
150 | }
151 |
152 | .log-footer {
153 | text-align: center;
154 | padding: 16px;
155 | font-size: 12px;
156 | opacity: 0.5;
157 | margin-top: 8px;
158 | }
159 |
160 | /* Scroll FAB */
161 | .scroll-fab {
162 | position: sticky;
163 | bottom: 16px;
164 | left: 50%;
165 | transform: translateX(-50%);
166 | background: var(--md-sys-color-primary-container);
167 | color: var(--md-sys-color-on-primary-container);
168 | border: none;
169 | border-radius: 9999px;
170 | padding: 8px 16px;
171 | display: flex;
172 | align-items: center;
173 | gap: 8px;
174 | cursor: pointer;
175 | font-size: 12px;
176 | font-weight: 600;
177 | z-index: 10;
178 | box-shadow: 0 4px 8px rgba(0,0,0,0.15);
179 | transition: transform 0.2s, box-shadow 0.2s;
180 | }
181 |
182 | .scroll-fab:active {
183 | transform: translateX(-50%) scale(0.95);
184 | }
185 |
186 | .scroll-icon {
187 | width: 16px;
188 | height: 16px;
189 | fill: currentColor;
190 | }
--------------------------------------------------------------------------------
/src/magic_mount/node.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | collections::{HashMap, hash_map::Entry},
3 | fmt,
4 | fs::{DirEntry, FileType},
5 | os::unix::fs::{FileTypeExt, MetadataExt},
6 | path::{Path, PathBuf},
7 | };
8 |
9 | use anyhow::{Context, Result};
10 | use rustix::path::Arg;
11 | use xattr::get as xattr_get;
12 |
13 | use crate::defs::REPLACE_DIR_XATTR;
14 |
15 | #[derive(PartialEq, Eq, Hash, Clone, Debug)]
16 | pub enum NodeFileType {
17 | RegularFile,
18 | Directory,
19 | Symlink,
20 | Whiteout,
21 | }
22 |
23 | impl From for NodeFileType {
24 | fn from(value: FileType) -> Self {
25 | if value.is_file() {
26 | Self::RegularFile
27 | } else if value.is_dir() {
28 | Self::Directory
29 | } else if value.is_symlink() {
30 | Self::Symlink
31 | } else {
32 | Self::Whiteout
33 | }
34 | }
35 | }
36 |
37 | #[derive(Debug, Clone)]
38 | pub struct Node {
39 | pub name: String,
40 | pub file_type: NodeFileType,
41 | pub children: HashMap,
42 | // the module that owned this node
43 | pub module_path: Option,
44 | pub replace: bool,
45 | pub skip: bool,
46 | }
47 |
48 | impl fmt::Display for NodeFileType {
49 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 | match self {
51 | Self::Directory => write!(f, "Directory"),
52 | Self::RegularFile => write!(f, "RegularFile"),
53 | Self::Symlink => write!(f, "Symlink"),
54 | Self::Whiteout => write!(f, "Whiteout"),
55 | }
56 | }
57 | }
58 |
59 | impl fmt::Display for Node {
60 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 | write!(
62 | f,
63 | "u need to send '/data/adb/magic_mount/tree' to developer "
64 | )
65 | }
66 | }
67 |
68 | impl Node {
69 | pub fn collect_module_files(&mut self, module_dir: P) -> Result
70 | where
71 | P: AsRef,
72 | {
73 | let dir = module_dir.as_ref();
74 | let mut has_file = false;
75 | for entry in dir.read_dir()?.flatten() {
76 | let name = entry.file_name().to_string_lossy().to_string();
77 |
78 | let node = match self.children.entry(name.clone()) {
79 | Entry::Occupied(o) => Some(o.into_mut()),
80 | Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)),
81 | };
82 |
83 | if let Some(node) = node {
84 | has_file |= if node.file_type == NodeFileType::Directory {
85 | node.collect_module_files(dir.join(&node.name))? || node.replace
86 | } else {
87 | true
88 | }
89 | }
90 | }
91 |
92 | Ok(has_file)
93 | }
94 |
95 | pub fn new_root(name: S) -> Self
96 | where
97 | S: AsRef + Into,
98 | {
99 | Self {
100 | name: name.into(),
101 | file_type: NodeFileType::Directory,
102 | children: HashMap::default(),
103 | module_path: None,
104 | replace: false,
105 | skip: false,
106 | }
107 | }
108 |
109 | pub fn new_module(name: &S, entry: &DirEntry) -> Option
110 | where
111 | S: AsRef + Into,
112 | std::string::String: for<'a> From<&'a S>,
113 | {
114 | if let Ok(metadata) = entry.metadata() {
115 | let path = entry.path();
116 | let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 {
117 | Some(NodeFileType::Whiteout)
118 | } else {
119 | Some(NodeFileType::from(metadata.file_type()))
120 | };
121 | if let Some(file_type) = file_type {
122 | let replace = if file_type == NodeFileType::Directory
123 | && let Ok(v) = xattr_get(&path, REPLACE_DIR_XATTR).with_context(|| {
124 | format!("Failed to get SELinux context for {}", path.display())
125 | })
126 | && let Some(s) = v
127 | && String::from_utf8_lossy(&s) == "y"
128 | {
129 | true
130 | } else {
131 | false
132 | };
133 | return Some(Self {
134 | name: name.into(),
135 | file_type,
136 | children: HashMap::default(),
137 | module_path: Some(path),
138 | replace,
139 | skip: false,
140 | });
141 | }
142 | }
143 |
144 | None
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/webui/src/app.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --md-sys-shape-corner-medium: 12px;
3 | --md-sys-shape-corner-large: 16px;
4 | --md-sys-shape-corner-full: 9999px;
5 | --md-sys-color-shadow: #000000;
6 | --md-sys-elevation-1: 0 1px 2px 0 rgba(0,0,0,0.3), 0 1px 3px 1px rgba(0,0,0,0.15);
7 | --md-sys-elevation-2: 0 1px 2px 0 rgba(0,0,0,0.3), 0 2px 6px 2px rgba(0,0,0,0.15);
8 | --md-sys-elevation-3: 0 4px 8px 3px rgba(0,0,0,0.15), 0 1px 3px 0 rgba(0,0,0,0.3);
9 | --font-common: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10 | --md-ref-typeface-plain: var(--font-stack, var(--font-common));
11 | --md-ref-typeface-mono: 'JetBrains Mono', 'Fira Code', monospace;
12 | }
13 | * {
14 | box-sizing: border-box;
15 | -webkit-tap-highlight-color: transparent;
16 | }
17 | html, body, #app {
18 | height: 100%;
19 | width: 100%;
20 | margin: 0;
21 | padding: 0;
22 | overflow: hidden;
23 | background-color: var(--md-sys-color-background);
24 | color: var(--md-sys-color-on-background);
25 | font-family: var(--md-ref-typeface-plain);
26 | font-size: 16px;
27 | line-height: 1.5;
28 | -webkit-font-smoothing: antialiased;
29 | -moz-osx-font-smoothing: grayscale;
30 | }
31 | input, select, textarea, button {
32 | font-family: inherit;
33 | }
34 | button {
35 | border: none;
36 | cursor: pointer;
37 | transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
38 | flex-shrink: 0;
39 | white-space: nowrap;
40 | }
41 | button:active {
42 | transform: scale(0.96);
43 | }
44 | .btn-filled {
45 | height: 44px;
46 | min-width: 90px;
47 | padding: 0 20px;
48 | border-radius: 12px;
49 | background-color: var(--md-sys-color-primary);
50 | color: var(--md-sys-color-on-primary);
51 | font-weight: 600;
52 | font-size: 14px;
53 | display: inline-flex;
54 | align-items: center;
55 | justify-content: center;
56 | gap: 8px;
57 | box-shadow: none;
58 | }
59 | .btn-filled:disabled {
60 | background-color: var(--md-sys-color-on-surface);
61 | opacity: 0.12;
62 | color: var(--md-sys-color-surface);
63 | cursor: not-allowed;
64 | box-shadow: none;
65 | }
66 | .btn-tonal {
67 | height: 44px;
68 | min-width: 90px;
69 | padding: 0 20px;
70 | border-radius: 12px;
71 | background-color: var(--md-sys-color-secondary-container);
72 | color: var(--md-sys-color-on-secondary-container);
73 | font-weight: 600;
74 | font-size: 14px;
75 | display: inline-flex;
76 | align-items: center;
77 | justify-content: center;
78 | gap: 8px;
79 | }
80 | .btn-icon {
81 | width: 40px;
82 | height: 40px;
83 | border-radius: 50%;
84 | background: transparent;
85 | color: var(--md-sys-color-on-surface-variant);
86 | display: flex;
87 | align-items: center;
88 | justify-content: center;
89 | }
90 | .btn-icon:hover {
91 | background-color: rgba(128,128,128, 0.1);
92 | }
93 | .md3-card {
94 | background-color: var(--md-sys-color-surface-container);
95 | border: 1px solid var(--md-sys-color-outline-variant);
96 | border-radius: var(--md-sys-shape-corner-large);
97 | padding: 20px;
98 | display: flex;
99 | flex-direction: column;
100 | gap: 16px;
101 | margin-bottom: 16px;
102 | box-shadow: none;
103 | }
104 | .text-field {
105 | position: relative;
106 | }
107 | .text-field input, .text-field select {
108 | width: 100%;
109 | height: 56px;
110 | background: var(--md-sys-color-surface-container-highest);
111 | border: 1px solid var(--md-sys-color-outline);
112 | border-bottom: 1px solid var(--md-sys-color-outline);
113 | border-radius: 4px 4px 0 0;
114 | padding: 0 16px;
115 | color: var(--md-sys-color-on-surface);
116 | font-size: 16px;
117 | outline: none;
118 | transition: all 0.2s;
119 | appearance: none;
120 | }
121 | .text-field input:focus, .text-field select:focus {
122 | border-bottom: 2px solid var(--md-sys-color-primary);
123 | border-color: var(--md-sys-color-primary);
124 | background: var(--md-sys-color-surface-container-highest);
125 | padding-left: 16px;
126 | padding-right: 16px;
127 | }
128 | .text-field label {
129 | position: absolute;
130 | top: 8px;
131 | left: 12px;
132 | background-color: transparent;
133 | padding: 0 4px;
134 | font-size: 12px;
135 | color: var(--md-sys-color-on-surface-variant);
136 | z-index: 1;
137 | pointer-events: none;
138 | }
139 | .text-field input:focus ~ label,
140 | .text-field select:focus ~ label {
141 | color: var(--md-sys-color-primary);
142 | }
143 | .bottom-actions {
144 | position: absolute;
145 | bottom: 0;
146 | left: 0;
147 | right: 0;
148 | padding: 16px 24px calc(16px + env(safe-area-inset-bottom)) 24px;
149 | background: linear-gradient(to top, var(--md-sys-color-surface) 85%, transparent);
150 | display: flex;
151 | justify-content: flex-end;
152 | align-items: center;
153 | gap: 12px;
154 | z-index: 50;
155 | pointer-events: none;
156 | }
157 | .bottom-actions button {
158 | pointer-events: auto;
159 | }
160 | code, pre {
161 | font-family: var(--md-ref-typeface-mono);
162 | }
--------------------------------------------------------------------------------
/webui/src/routes/ModulesTab.css:
--------------------------------------------------------------------------------
1 | .modules-container {
2 | display: flex;
3 | flex-direction: column;
4 | gap: 16px;
5 | padding: 16px;
6 | padding-bottom: 80px;
7 | }
8 |
9 | /* Description Card */
10 | .desc-card {
11 | background-color: var(--md-sys-color-secondary-container);
12 | color: var(--md-sys-color-on-secondary-container);
13 | border-radius: 16px;
14 | padding: 16px;
15 | display: flex;
16 | align-items: center;
17 | gap: 16px;
18 | }
19 |
20 | .desc-icon {
21 | display: flex;
22 | align-items: center;
23 | justify-content: center;
24 | }
25 |
26 | .desc-text {
27 | font-size: 14px;
28 | line-height: 1.4;
29 | margin: 0;
30 | }
31 |
32 | /* Search Section */
33 | .search-section {
34 | display: flex;
35 | flex-direction: column;
36 | }
37 |
38 | .search-field {
39 | width: 100%;
40 | /* Match the 16px border-radius of module-card and desc-card */
41 | --md-outlined-text-field-container-shape: 16px;
42 | }
43 |
44 | /* Empty State */
45 | .empty-state {
46 | display: flex;
47 | flex-direction: column;
48 | align-items: center;
49 | justify-content: center;
50 | padding: 48px 0;
51 | gap: 16px;
52 | color: var(--md-sys-color-on-surface-variant);
53 | }
54 |
55 | .empty-icon {
56 | width: 64px;
57 | height: 64px;
58 | border-radius: 50%;
59 | background-color: var(--md-sys-color-surface-container-high);
60 | display: flex;
61 | align-items: center;
62 | justify-content: center;
63 | }
64 |
65 | .empty-icon md-icon {
66 | font-size: 32px;
67 | width: 32px;
68 | height: 32px;
69 | }
70 |
71 | /* Modules List */
72 | .modules-list {
73 | display: flex;
74 | flex-direction: column;
75 | gap: 12px;
76 | }
77 |
78 | .module-card {
79 | background-color: var(--md-sys-color-surface-container);
80 | border-radius: 16px;
81 | overflow: hidden;
82 | transition: background-color 0.2s;
83 | border: 1px solid transparent;
84 | }
85 |
86 | .module-card.expanded {
87 | background-color: var(--md-sys-color-surface-container-high);
88 | border-color: var(--md-sys-color-outline-variant);
89 | }
90 |
91 | .module-card.unmounted {
92 | opacity: 0.8;
93 | }
94 |
95 | .card-main {
96 | position: relative;
97 | padding: 16px;
98 | display: flex;
99 | justify-content: space-between;
100 | align-items: center;
101 | cursor: pointer;
102 | }
103 |
104 | .module-info {
105 | display: flex;
106 | flex-direction: column;
107 | gap: 4px;
108 | }
109 |
110 | .module-name {
111 | font-size: 16px;
112 | font-weight: 500;
113 | color: var(--md-sys-color-on-surface);
114 | }
115 |
116 | .module-meta-row {
117 | display: flex;
118 | align-items: center;
119 | gap: 8px;
120 | }
121 |
122 | .module-id {
123 | font-size: 12px;
124 | font-family: monospace;
125 | color: var(--md-sys-color-on-surface-variant);
126 | }
127 |
128 | .version-tag {
129 | font-size: 10px;
130 | background-color: var(--md-sys-color-surface-variant);
131 | color: var(--md-sys-color-on-surface-variant);
132 | padding: 2px 6px;
133 | border-radius: 4px;
134 | font-family: monospace;
135 | }
136 |
137 | /* Status Badge */
138 | .status-badge {
139 | font-size: 12px;
140 | font-weight: 500;
141 | padding: 6px 12px;
142 | border-radius: 20px;
143 | text-transform: uppercase;
144 | letter-spacing: 0.5px;
145 | }
146 |
147 | .status-badge.magic {
148 | background-color: var(--md-sys-color-primary-container);
149 | color: var(--md-sys-color-on-primary-container);
150 | }
151 |
152 | .status-badge.skipped {
153 | background-color: var(--md-sys-color-surface-variant);
154 | color: var(--md-sys-color-on-surface-variant);
155 | }
156 |
157 | /* Details Section */
158 | .card-details {
159 | padding: 0 16px 16px;
160 | border-top: 1px solid var(--md-sys-color-outline-variant);
161 | margin-top: 0;
162 | padding-top: 16px;
163 | display: flex;
164 | flex-direction: column;
165 | gap: 12px;
166 | }
167 |
168 | .detail-row {
169 | display: flex;
170 | flex-direction: column;
171 | gap: 2px;
172 | }
173 |
174 | .detail-row.description {
175 | margin-top: 4px;
176 | }
177 |
178 | .detail-label {
179 | font-size: 11px;
180 | text-transform: uppercase;
181 | color: var(--md-sys-color-primary);
182 | font-weight: 500;
183 | }
184 |
185 | .detail-value {
186 | font-size: 14px;
187 | color: var(--md-sys-color-on-surface);
188 | margin: 0;
189 | line-height: 1.5;
190 | }
191 |
192 | /* Alert Box */
193 | .status-alert {
194 | display: flex;
195 | align-items: center;
196 | gap: 12px;
197 | background-color: var(--md-sys-color-error-container);
198 | color: var(--md-sys-color-on-error-container);
199 | padding: 12px;
200 | border-radius: 12px;
201 | font-size: 13px;
202 | margin-top: 8px;
203 | }
204 |
205 | .alert-icon {
206 | color: inherit;
207 | }
208 |
209 | /* Skeleton */
210 | .skeleton-card {
211 | padding: 16px;
212 | min-height: 72px;
213 | }
--------------------------------------------------------------------------------
/cliff.toml:
--------------------------------------------------------------------------------
1 | # git-cliff ~ configuration file
2 | # https://git-cliff.org/docs/configuration
3 |
4 | [changelog]
5 | # A Tera template to be rendered for each release in the changelog.
6 | # See https://keats.github.io/tera/docs/#introduction
7 | body = """
8 | {% if version %}\
9 | ## {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }}
10 | {% else %}\
11 | ## Unreleased
12 | {% endif %}\
13 | {% if previous %}\
14 | {% if previous.commit_id and commit_id %}
15 | **[View changes](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/compare/{{ previous.commit_id }}...{{ commit_id }})**\
16 | ({{ previous.commit_id | truncate(length=7, end="") }}...{{ commit_id | truncate(length=7, end="") }})
17 | {% endif %}\
18 | {% endif %}\
19 | {% for group, commits in commits | group_by(attribute="group") %}
20 | ### {{ group | upper_first }}
21 | {% for commit in commits %}
22 | - {{ commit.message | split(pat="\n") | first | upper_first | trim }}\
23 | ([{{ commit.id | truncate(length=7, end="") }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/commit/{{ commit.id }}))\
24 | {% endfor %}
25 | {% endfor %}\n
26 | """
27 |
28 | # Remove leading and trailing whitespaces from the changelog's body.
29 | trim = true
30 | # Render body even when there are no releases to process.
31 | render_always = true
32 |
33 | # An array of regex based postprocessors to modify the changelog.
34 | postprocessors = [
35 | # Replace the placeholder with a URL.
36 | #{ pattern = '', replace = "https://github.com/orhun/git-cliff" },
37 | ]
38 |
39 | # Footer template
40 | footer = """
41 | {% if previous %}\
42 | **Full Changelog**: [{{ previous.version }}...{{ version }}]({{ remote.github }}/compare/{{ previous.version }}...{{ version }})\
43 | {% endif %}
44 | """
45 |
46 | # Remote repository configuration
47 | [remote.github]
48 | owner = "Tools-cx-app"
49 | repo = "meta-magic_mount"
50 |
51 | [git]
52 | # Parse commits according to the conventional commits specification.
53 | # See https://www.conventionalcommits.org
54 | conventional_commits = true
55 | # Exclude commits that do not match the conventional commits specification.
56 | filter_unconventional = true
57 | # Require all commits to be conventional.
58 | # Takes precedence over filter_unconventional.
59 | require_conventional = false
60 | # Split commits on newlines, treating each line as an individual commit.
61 | split_commits = false
62 |
63 | # An array of regex based parsers to modify commit messages prior to further processing.
64 | commit_preprocessors = [
65 | # Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
66 | #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"},
67 | # Check spelling of the commit message using https://github.com/crate-ci/typos.
68 | # If the spelling is incorrect, it will be fixed automatically.
69 | #{ pattern = '.*', replace_command = 'typos --write-changes -' },
70 | ]
71 |
72 | # Prevent commits that are breaking from being excluded by commit parsers.
73 | protect_breaking_commits = false
74 |
75 | # An array of regex based parsers for extracting data from the commit message.
76 | # Assigns commits to groups.
77 | # Optionally sets the commit's scope and can decide to exclude commits from further processing.
78 | commit_parsers = [
79 | { message = "^feat", group = " Features" },
80 | { message = "^fix", group = " Bug Fixes" },
81 | { message = "^doc", group = " Documentation" },
82 | { message = "^perf", group = " Performance" },
83 | { message = "^refactor", group = " Refactor" },
84 | { message = "^style", group = " Styling" },
85 | { message = "^opt", group = " Optimization"},
86 | { message = "^test", group = " Testing" },
87 | { message = "^chore\\(release\\): prepare for", skip = true },
88 | { message = "^chore\\(deps.*\\)", skip = true },
89 | { message = "^chore\\(pr\\)", skip = true },
90 | { message = "^chore\\(pull\\)", skip = true },
91 | { message = "^chore|^ci", group = " Miscellaneous Tasks" },
92 | { body = ".*security", group = " Security" },
93 | { message = "^revert", group = " Revert" },
94 | { message = ".*", group = " Other" },
95 | ]
96 |
97 | # Exclude commits that are not matched by any commit parser.
98 | filter_commits = false
99 | # An array of link parsers for extracting external references, and turning them into URLs, using regex.
100 | link_parsers = []
101 | # Include only the tags that belong to the current branch.
102 | use_branch_tags = false
103 | # Order releases topologically instead of chronologically.
104 | topo_order = false
105 | # Order releases topologically instead of chronologically.
106 | topo_order_commits = true
107 | # Order of commits in each group/release within the changelog.
108 | # Allowed values: newest, oldest
109 | sort_commits = "oldest"
110 | # Process submodules commits
111 | recurse_submodules = false
112 |
--------------------------------------------------------------------------------
/xtask/src/main.rs:
--------------------------------------------------------------------------------
1 | mod zip_ext;
2 |
3 | use std::{
4 | fs,
5 | path::{Path, PathBuf},
6 | process::Command,
7 | };
8 |
9 | use anyhow::Result;
10 | use fs_extra::{dir, file};
11 | use serde::{Deserialize, Serialize};
12 | use zip::{CompressionMethod, write::FileOptions};
13 |
14 | use crate::zip_ext::zip_create_from_directory_with_options;
15 |
16 | #[derive(Deserialize)]
17 | struct Package {
18 | pub version: String,
19 | }
20 |
21 | #[derive(Deserialize)]
22 | struct CargoConfig {
23 | pub package: Package,
24 | }
25 |
26 | #[derive(Serialize)]
27 | struct UpdateJson {
28 | version: String,
29 | #[serde(rename = "versionCode")]
30 | versioncode: usize,
31 | #[serde(rename = "zipUrl")]
32 | zipurl: String,
33 | changelog: String,
34 | }
35 |
36 | fn main() -> Result<()> {
37 | let args: Vec<_> = std::env::args().collect();
38 |
39 | if args.len() == 1 {
40 | return Ok(());
41 | }
42 |
43 | match args[1].as_str() {
44 | "build" | "b" => build()?,
45 | "update" | "u" => update()?,
46 | _ => {}
47 | }
48 |
49 | Ok(())
50 | }
51 |
52 | fn cal_version_code(version: &str) -> Result {
53 | let manjor = version
54 | .split('.')
55 | .next()
56 | .ok_or_else(|| anyhow::anyhow!("Invalid version format"))?;
57 | let manjor: usize = manjor.parse()?;
58 | let minor = version
59 | .split('.')
60 | .nth(1)
61 | .ok_or_else(|| anyhow::anyhow!("Invalid version format"))?;
62 | let minor: usize = minor.parse()?;
63 | let patch = version
64 | .split('.')
65 | .nth(2)
66 | .ok_or_else(|| anyhow::anyhow!("Invalid version format"))?;
67 | let patch: usize = patch.parse()?;
68 |
69 | // Version code rule: Major * 100000 + Minor * 1000 + Patch
70 | Ok(manjor * 100000 + minor * 1000 + patch)
71 | }
72 |
73 | fn update() -> Result<()> {
74 | let toml = fs::read_to_string("Cargo.toml")?;
75 | let data: CargoConfig = toml::from_str(&toml)?;
76 |
77 | //build()?;
78 |
79 | let json = UpdateJson {
80 | versioncode: cal_version_code(&data.package.version)?,
81 | // Fixed typo here as well
82 | version: data.package.version.clone(),
83 | zipurl: format!(
84 | "https://github.com/Tools-cx-app/meta-magic_mount/releases/download/v{}/magic_mount_rs.zip",
85 | data.package.version.clone()
86 | ),
87 | changelog: String::from(
88 | "https://github.com/Tools-cx-app/meta-magic_mount/raw/master/update/changelog.md",
89 | ),
90 | };
91 |
92 | let raw_json = serde_json::to_string_pretty(&json)?;
93 |
94 | fs::write("update/update.json", raw_json)?;
95 |
96 | Ok(())
97 | }
98 |
99 | fn build() -> Result<()> {
100 | let temp_dir = temp_dir();
101 |
102 | let _ = fs::remove_dir_all(&temp_dir);
103 | fs::create_dir_all(&temp_dir)?;
104 |
105 | build_webui()?;
106 |
107 | let mut cargo = cargo_ndk();
108 | let args = vec![
109 | "build",
110 | "--target",
111 | "aarch64-linux-android",
112 | "-Z",
113 | "build-std",
114 | "-Z",
115 | "trim-paths",
116 | "-r",
117 | ];
118 |
119 | cargo.args(args);
120 |
121 | cargo.spawn()?.wait()?;
122 |
123 | let module_dir = module_dir();
124 | dir::copy(
125 | &module_dir,
126 | &temp_dir,
127 | &dir::CopyOptions::new().overwrite(true).content_only(true),
128 | )
129 | .unwrap();
130 |
131 | if temp_dir.join(".gitignore").exists() {
132 | fs::remove_file(temp_dir.join(".gitignore")).unwrap();
133 | }
134 |
135 | file::copy(
136 | bin_path(),
137 | temp_dir.join("magic_mount_rs"),
138 | &file::CopyOptions::new().overwrite(true),
139 | )?;
140 |
141 | let options: FileOptions<'_, ()> = FileOptions::default()
142 | .compression_method(CompressionMethod::Deflated)
143 | .compression_level(Some(9));
144 | zip_create_from_directory_with_options(
145 | &Path::new("output").join("magic_mount_rs.zip"),
146 | &temp_dir,
147 | |_| options,
148 | )
149 | .unwrap();
150 |
151 | Ok(())
152 | }
153 |
154 | fn module_dir() -> PathBuf {
155 | Path::new("module").to_path_buf()
156 | }
157 |
158 | fn temp_dir() -> PathBuf {
159 | Path::new("output").join(".temp")
160 | }
161 |
162 | fn bin_path() -> PathBuf {
163 | Path::new("target")
164 | .join("aarch64-linux-android")
165 | .join("release")
166 | .join("magic_mount_rs")
167 | }
168 | fn cargo_ndk() -> Command {
169 | let mut command = Command::new("cargo");
170 | command
171 | .args(["+nightly", "ndk", "--platform", "31", "-t", "arm64-v8a"])
172 | .env("RUSTFLAGS", "-C default-linker-libraries")
173 | .env("CARGO_CFG_BPF_TARGET_ARCH", "aarch64");
174 | command
175 | }
176 |
177 | fn build_webui() -> Result<()> {
178 | let npm = || {
179 | let mut command = Command::new("npm");
180 | command.current_dir("webui");
181 | command
182 | };
183 |
184 | npm().arg("install").spawn()?.wait()?;
185 | npm().args(["run", "build"]).spawn()?.wait()?;
186 |
187 | Ok(())
188 | }
189 |
--------------------------------------------------------------------------------
/webui/src/routes/ModulesTab.svelte:
--------------------------------------------------------------------------------
1 |
43 |
44 |
45 |
46 |
49 |
50 | {store.L.modules?.desc || "Modules are strictly managed by Magic Mount strategy."}
51 |
52 |
53 |
54 |
64 |
65 | {#if store.loading.modules}
66 |
67 | {#each Array(5) as _}
68 |
69 |
70 |
71 |
72 | {/each}
73 |
74 | {:else if filteredModules.length === 0}
75 |
76 |
79 |
{store.modules.length === 0 ? (store.L.modules?.empty || "No modules found") : "No matching modules"}
80 |
81 | {:else}
82 |
83 | {#each filteredModules as mod (mod.id)}
84 |
89 |
toggleExpand(mod.id)}
92 | onkeydown={(e) => handleKeydown(e, mod.id)}
93 | role="button"
94 | tabindex="0"
95 | >
96 |
97 |
98 |
{mod.name}
99 |
100 | {mod.id}
101 | {mod.version}
102 |
103 |
104 |
105 |
106 | {mod.is_mounted ? 'Magic' : 'Skipped'}
107 |
108 |
109 |
110 | {#if expandedId === mod.id}
111 |
112 |
113 | Author
114 | {mod.author || 'Unknown'}
115 |
116 |
117 |
Description
118 |
{mod.description || 'No description'}
119 |
120 |
121 | {#if !mod.is_mounted}
122 |
123 |
124 |
125 | {#if mod.disabledByFlag}
126 | Disabled via Manager or 'disable' file.
127 | {:else if mod.skipMount}
128 | Skipped via 'skip_mount' flag.
129 | {:else}
130 | Not mounted.
131 | {/if}
132 |
133 |
134 | {/if}
135 |
136 | {/if}
137 |
138 | {/each}
139 |
140 | {/if}
141 |
142 |
143 |
144 |
145 | store.loadModules()}
147 | disabled={store.loading.modules}
148 | title={store.L.modules?.reload || "Refresh"}
149 | role="button"
150 | tabindex="0"
151 | onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') store.loadModules(); }}
152 | >
153 |
154 |
155 |
--------------------------------------------------------------------------------
/webui/src/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_CONFIG = {
2 | moduledir: '/data/adb/modules',
3 | tempdir: '',
4 | mountsource: 'KSU',
5 | verbose: false,
6 | umount: true,
7 | disable_umount: false,
8 | partitions: []
9 | };
10 | export const PATHS = {
11 | CONFIG: '/data/adb/magic_mount/config.toml',
12 | LOG_FILE: '/data/adb/magic_mount/mm.log',
13 | MODULE_ROOT: '/data/adb/modules',
14 | DAEMON_STATE: '/data/adb/magic_mount/run/daemon_state.json'
15 | };
16 | export const DEFAULT_SEED = '#6750A4';
17 | export const ICONS = {
18 | home: "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z",
19 | settings: "M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z",
20 | modules: "M2 4v16h20V4H2zm18 14H4V6h16v12zM6 10h12v2H6v-2zm0 4h8v2H6v-2z",
21 | storage: "M2 20h20v-4H2v4zm2-3h2v2H4v-2zM2 4v4h20V4H2zm4 3H4V5h2v2zm-4 7h20v-4H2v4zm2-3h2v2H4v-2z",
22 | description: "M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z",
23 | save: "M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z",
24 | refresh: "M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z",
25 | translate: "M12.87 15.07l-2.54-2.51.03-.03A17.52 17.52 0 0 0 14.07 6H17V4h-7V2H8v2H1v2h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z",
26 | light_mode: "M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1z",
27 | dark_mode: "M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z",
28 | auto_mode: "M11 7l-3.2 9h1.9l.7-2h3.2l.7 2h1.9L13 7h-2zm-.15 5.65L12 9l1.15 3.65h-2.3zM20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM18 18H6V6h12v12z",
29 | search: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z",
30 | copy: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z",
31 | share: "M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92-2.92-2.92z",
32 | info: "M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8 8z",
33 | github: "M12 2C6.48 2 2 6.48 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.89 1.52 2.34 1.08 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02a9.56 9.56 0 0 1 2.5-.34c.85.04 1.7.19 2.5.34 1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2z",
34 | donate: "M12.75 3.94c4.89-1.57 9.42 1.13 8.32 6.55-.54 2.66-2.5 4.54-4.5 6.43-1.89 1.79-3.79 3.58-4.57 5.08-.78-1.5-2.68-3.29-4.57-5.08-2-1.89-3.96-3.77-4.5-6.43-1.1-5.42 3.43-8.12 8.32-6.55.43.14.93.14 1.36 0 .04 0 .09-.01.14 0zM7.5 9c0-.83-.67-1.5-1.5-1.5S4.5 8.17 4.5 9s.67 1.5 1.5 1.5S7.5 9.83 7.5 9zm9 4.5c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5-1.5.67-1.5 1.5.67 1.5 1.5 1.5z",
35 | ksu: "M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z",
36 | cat_paw: "M12 11c-1.33 0-4 .67-4 3 0 2.33 2.67 4 4 4 1.33 0 4-1.67 4-4 0-2.33-2.67-3-4-3zm-5.25-2c-.97 0-1.75.78-1.75 1.75 0 .97.78 1.75 1.75 1.75.97 0 1.75-.78 1.75-1.75 0-.97-.78-1.75-1.75-1.75zm10.5 0c-.97 0-1.75.78-1.75 1.75 0 .97.78 1.75 1.75 1.75.97 0 1.75-.78 1.75-1.75 0-.97-.78-1.75-1.75-1.75zm-8.5-4c-.97 0-1.75.78-1.75 1.75 0 .97.78 1.75 1.75 1.75.97 0 1.75-.78 1.75-1.75 0-.97-.78-1.75-1.75-1.75zm6.5 0c-.97 0-1.75.78-1.75 1.75 0 .97.78 1.75 1.75 1.75.97 0 1.75-.78 1.75-1.75 0-.97-.78-1.75-1.75-1.75z",
37 | ghost: "M12 2a9 9 0 0 0-9 9v11l3-3 3 3 3-3 3 3 3-3 3 3v-11a9 9 0 0 0-9-9zm0 14a2 2 0 1 1 2-2 2 2 0 0 1-2 2zm3-5a2 2 0 1 1 2-2 2 2 0 0 1-2 2zm-6 0a2 2 0 1 1 2-2 2 2 0 0 1-2 2z",
38 | anchor: "M17 13h-2v-4h-2V6h2V4h-2V2h-2v2H9v2h2v3H9v4H7v2c0 2.76 2.24 5 5 5s5-2.24 5-5v-2zm-5 5c-1.65 0-3-1.35-3-3v-2h6v2c0 1.65-1.35 3-3 3z",
39 | timer: "M15 1H9v2h6V1zm-4 13h2V8h-2v6zm8.03-6.61l1.42-1.42c-.43-.51-.9-.99-1.41-1.41l-1.42 1.42A8.962 8.962 0 0012 4c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.52.02-1.02.05-1.52zM12 20c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z",
40 | delete: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
41 | add: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"
42 | };
--------------------------------------------------------------------------------
/webui/src/routes/InfoTab.svelte:
--------------------------------------------------------------------------------
1 |
103 |
104 |
105 |
106 |
113 |
114 |
126 |
127 |
128 |
{store.L.info.contributors}
129 |
130 | {#if loading}
131 | {#each Array(3) as _}
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | {/each}
140 | {:else if error}
141 |
142 | {store.L.info.loadFail}
143 |
144 | {:else}
145 |
146 | {#each contributors as user}
147 | handleLink(e, user.html_url)}
152 | role="link"
153 | tabindex="0"
154 | onkeydown={(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') handleLink(e, user.html_url); }}
155 | >
156 |
157 | {user.name || user.login}
158 | {user.bio || store.L.info.noBio}
159 |
160 | {/each}
161 |
162 | {/if}
163 |
164 |
165 |
--------------------------------------------------------------------------------
/webui/src/routes/LogsTab.svelte:
--------------------------------------------------------------------------------
1 |
92 |
93 |
94 |
95 |
104 |
105 |
106 |
107 |
113 |
114 |
115 |
116 |
117 |
118 |
124 |
125 |
126 |
127 |
128 | {#if store.loading.logs && !autoRefresh && filteredLogs.length === 0}
129 |
130 | {#each Array(10) as _, i}
131 |
132 | {/each}
133 |
134 | {:else if filteredLogs.length === 0}
135 |
136 | {store.logs.length === 0 ? store.L.logs.empty : "No matching logs"}
137 |
138 | {:else}
139 | {#each filteredLogs as line}
140 |
141 | {#if typeof line === 'string'}
142 | {line}
143 | {:else}
144 | {line.text}
145 | {/if}
146 |
147 | {/each}
148 |
151 | {/if}
152 |
153 | {#if userHasScrolledUp}
154 |
162 | {/if}
163 |
164 |
165 |
166 |
167 | { if (e.key === 'Enter' || e.key === ' ') copyLogs(); }}
174 | >
175 |
176 |
177 |
178 |
179 |
180 | refreshLogs(false)}
182 | disabled={store.loading.logs}
183 | title={store.L.logs.refresh}
184 | role="button"
185 | tabindex="0"
186 | onkeydown={(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') refreshLogs(false); }}
187 | >
188 |
189 |
190 |
--------------------------------------------------------------------------------
/webui/src/routes/StatusTab.svelte:
--------------------------------------------------------------------------------
1 |
39 |
40 | showRebootConfirm = false}
43 | style="--md-dialog-scrim-color: transparent; --md-sys-color-scrim: transparent;"
44 | >
45 | {store.L.common?.rebootTitle || "Reboot System?"}
46 |
47 | {store.L.common?.rebootConfirm || "Are you sure you want to reboot the device?"}
48 |
49 |
50 | showRebootConfirm = false}
52 | role="button" tabindex="0" onkeydown={() => {}}
53 | >
54 | {store.L.common?.cancel || "Cancel"}
55 |
56 | {}}
59 | >
60 | {store.L.common?.reboot || "Reboot"}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
72 |
73 |
74 |
75 |
78 |
{store.L.status.deviceTitle || "Device"}
79 |
80 |
81 | {#if store.loading.status}
82 |
83 |
84 | {:else}
85 |
{store.device.model}
86 |
87 | Magic Mount v{store.version}
88 |
89 | {/if}
90 |
91 |
92 |
93 |
{ if (e.key === 'Enter' || e.key === ' ') copyDebugInfo(); }}
99 | >
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | {#if store.loading.status}
108 |
109 |
110 | {:else}
111 |
{mountedCount}
112 |
{store.L.status.moduleActive}
113 | {/if}
114 |
115 |
116 |
117 | {#if store.loading.status}
118 |
119 |
120 | {:else}
121 |
{store.config?.mountsource ?? '-'}
122 |
{store.L.config.mountSource}
123 | {/if}
124 |
125 |
126 |
127 |
128 |
{store.L.status.sysInfoTitle || "System Details"}
129 |
130 |
131 | {store.L.status.androidLabel || "Android"}
132 | {#if store.loading.status}
133 |
134 | {:else}
135 | {store.device.android}
136 | {/if}
137 |
138 |
139 |
140 | {store.L.status.selinuxLabel || "SELinux"}
141 | {#if store.loading.status}
142 |
143 | {:else}
144 |
145 | {store.device.selinux}
146 |
147 | {/if}
148 |
149 |
150 |
151 | {store.L.status.kernelLabel || "Kernel"}
152 | {#if store.loading.status}
153 |
154 | {:else}
155 | {store.device.kernel}
156 | {/if}
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | showRebootConfirm = true}
167 | title={store.L.common?.reboot || "Reboot Device"}
168 | role="button"
169 | tabindex="0"
170 | onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') showRebootConfirm = true; }}
171 | >
172 |
173 |
174 |
175 |
176 |
177 | store.loadStatus()}
179 | disabled={store.loading.status}
180 | title={store.L.logs.refresh}
181 | role="button"
182 | tabindex="0"
183 | onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') store.loadStatus(); }}
184 | >
185 |
186 |
187 |
--------------------------------------------------------------------------------
/webui/src/routes/ConfigTab.svelte:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
109 |
110 |
111 |
112 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
144 |
145 |
160 |
161 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | { if (e.key === 'Enter' || e.key === ' ') reload(); }}
189 | >
190 |
191 |
192 |
193 |
194 |
195 | { if (e.key === 'Enter' || e.key === ' ') save(); }}
201 | >
202 |
203 | {store.saving.config ? store.L.common.saving : store.L.config.save}
204 |
205 |
--------------------------------------------------------------------------------
/src/magic_mount/utils.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs::{self, DirEntry, Metadata, create_dir, create_dir_all, read_link},
3 | os::unix::fs::{MetadataExt, symlink},
4 | path::{Path, PathBuf},
5 | };
6 |
7 | use anyhow::{Result, bail};
8 | use rustix::{
9 | fs::{Gid, Mode, Uid, chmod, chown},
10 | mount::mount_bind,
11 | };
12 |
13 | use crate::{
14 | defs::{DISABLE_FILE_NAME, REMOVE_FILE_NAME, SKIP_MOUNT_FILE_NAME},
15 | magic_mount::node::{Node, NodeFileType},
16 | utils::{lgetfilecon, lsetfilecon, validate_module_id},
17 | };
18 |
19 | fn metadata_path(path: P, node: &Node) -> Result<(Metadata, PathBuf)>
20 | where
21 | P: AsRef,
22 | {
23 | let path = path.as_ref();
24 | if path.exists() {
25 | Ok((path.metadata()?, path.to_path_buf()))
26 | } else if let Some(module_path) = &node.module_path {
27 | Ok((module_path.metadata()?, module_path.clone()))
28 | } else {
29 | bail!("cannot mount root dir {}!", path.display());
30 | }
31 | }
32 |
33 | pub fn tmpfs_skeleton(path: P, work_dir_path: P, node: &Node) -> Result<()>
34 | where
35 | P: AsRef,
36 | {
37 | let (path, work_dir_path) = (path.as_ref(), work_dir_path.as_ref());
38 | log::debug!(
39 | "creating tmpfs skeleton for {} at {}",
40 | path.display(),
41 | work_dir_path.display()
42 | );
43 |
44 | create_dir_all(work_dir_path)?;
45 |
46 | let (metadata, path) = metadata_path(path, node)?;
47 |
48 | chmod(work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
49 | chown(
50 | work_dir_path,
51 | Some(Uid::from_raw(metadata.uid())),
52 | Some(Gid::from_raw(metadata.gid())),
53 | )?;
54 | lsetfilecon(work_dir_path, lgetfilecon(path)?.as_str())?;
55 |
56 | Ok(())
57 | }
58 |
59 | pub fn mount_mirror(path: P, work_dir_path: P, entry: &DirEntry) -> Result<()>
60 | where
61 | P: AsRef,
62 | {
63 | let path = path.as_ref().join(entry.file_name());
64 | let work_dir_path = work_dir_path.as_ref().join(entry.file_name());
65 | let file_type = entry.file_type()?;
66 |
67 | if file_type.is_file() {
68 | log::debug!(
69 | "mount mirror file {} -> {}",
70 | path.display(),
71 | work_dir_path.display()
72 | );
73 | fs::File::create(&work_dir_path)?;
74 | mount_bind(&path, &work_dir_path)?;
75 | } else if file_type.is_dir() {
76 | log::debug!(
77 | "mount mirror dir {} -> {}",
78 | path.display(),
79 | work_dir_path.display()
80 | );
81 | create_dir(&work_dir_path)?;
82 | let metadata = entry.metadata()?;
83 | chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?;
84 | chown(
85 | &work_dir_path,
86 | Some(Uid::from_raw(metadata.uid())),
87 | Some(Gid::from_raw(metadata.gid())),
88 | )?;
89 | lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?;
90 | for entry in path.read_dir()?.flatten() {
91 | mount_mirror(&path, &work_dir_path, &entry)?;
92 | }
93 | } else if file_type.is_symlink() {
94 | log::debug!(
95 | "create mirror symlink {} -> {}",
96 | path.display(),
97 | work_dir_path.display()
98 | );
99 | clone_symlink(&path, &work_dir_path)?;
100 | }
101 |
102 | Ok(())
103 | }
104 |
105 | pub fn check_tmpfs(node: &mut Node, path: P) -> (Node, bool)
106 | where
107 | P: AsRef,
108 | {
109 | let mut ret_tmpfs = false;
110 | for it in &mut node.children {
111 | let (name, node) = it;
112 | let real_path = path.as_ref().join(name);
113 | let need = match node.file_type {
114 | NodeFileType::Symlink => true,
115 | NodeFileType::Whiteout => real_path.exists(),
116 | _ => {
117 | if let Ok(metadata) = real_path.symlink_metadata() {
118 | let file_type = NodeFileType::from(metadata.file_type());
119 | file_type != node.file_type || file_type == NodeFileType::Symlink
120 | } else {
121 | // real path not exists
122 | true
123 | }
124 | }
125 | };
126 | if need {
127 | if node.module_path.is_none() {
128 | log::error!(
129 | "cannot create tmpfs on {}, ignore: {name}",
130 | path.as_ref().display()
131 | );
132 | node.skip = true;
133 | continue;
134 | }
135 | ret_tmpfs = true;
136 | break;
137 | }
138 | }
139 |
140 | (node.clone(), ret_tmpfs)
141 | }
142 |
143 | pub fn collect_module_files(
144 | module_dir: &Path,
145 | extra_partitions: &[String],
146 | ) -> Result