Skip to content

Commit 1f4980a

Browse files
committed
Add performance optimizations and security improvements
- Add command result caching (mount 1s, pgrep 500ms) to reduce syscalls - Add keys to Svelte {#each} blocks to prevent unnecessary re-renders - Add debounce to status.refresh() to merge rapid calls - Fix store read pattern using local variable instead of subscribe/unsubscribe - Add proper Rust error types with thiserror crate - Add shared paths module for socket/log file locations - Add command timeouts using tokio::time::timeout - Improve mount/unmount detection with faster polling intervals - Poll for VM shutdown instead of fixed delay after unmount
1 parent dfc1584 commit 1f4980a

12 files changed

Lines changed: 395 additions & 79 deletions

File tree

src-tauri/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ tauri = { version = "2", features = [] }
1515
tauri-plugin-shell = "2"
1616
tauri-plugin-log = "2"
1717
log = "0.4"
18+
thiserror = "2"
1819
serde = { version = "1", features = ["derive"] }
1920
serde_json = "1"
2021
tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] }

src-tauri/src/cache.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
use std::collections::HashMap;
2+
use std::process::{Command, Output};
3+
use std::sync::Mutex;
4+
use std::time::{Duration, Instant};
5+
6+
/// Cache entry with output and timestamp
7+
struct CacheEntry {
8+
output: Output,
9+
timestamp: Instant,
10+
}
11+
12+
/// Global cache for command outputs
13+
static COMMAND_CACHE: Mutex<Option<CommandCache>> = Mutex::new(None);
14+
15+
struct CommandCache {
16+
entries: HashMap<String, CacheEntry>,
17+
}
18+
19+
impl CommandCache {
20+
fn new() -> Self {
21+
Self {
22+
entries: HashMap::new(),
23+
}
24+
}
25+
26+
fn get(&self, key: &str, max_age: Duration) -> Option<&Output> {
27+
self.entries.get(key).and_then(|entry| {
28+
if entry.timestamp.elapsed() < max_age {
29+
Some(&entry.output)
30+
} else {
31+
None
32+
}
33+
})
34+
}
35+
36+
fn insert(&mut self, key: String, output: Output) {
37+
self.entries.insert(key, CacheEntry {
38+
output,
39+
timestamp: Instant::now(),
40+
});
41+
}
42+
43+
fn invalidate(&mut self, prefix: &str) {
44+
self.entries.retain(|k, _| !k.starts_with(prefix));
45+
}
46+
}
47+
48+
fn with_cache<F, R>(f: F) -> R
49+
where
50+
F: FnOnce(&mut CommandCache) -> R,
51+
{
52+
let mut guard = COMMAND_CACHE.lock().unwrap();
53+
if guard.is_none() {
54+
*guard = Some(CommandCache::new());
55+
}
56+
f(guard.as_mut().unwrap())
57+
}
58+
59+
/// Cache durations for different command types
60+
const MOUNT_CACHE_DURATION: Duration = Duration::from_millis(1000);
61+
const PGREP_CACHE_DURATION: Duration = Duration::from_millis(500);
62+
63+
/// Get cached mount command output (1 second cache)
64+
pub fn get_mount_output() -> Option<Output> {
65+
let cache_key = "mount".to_string();
66+
67+
// Check cache first
68+
let cached = with_cache(|cache| {
69+
cache.get(&cache_key, MOUNT_CACHE_DURATION).cloned()
70+
});
71+
72+
if let Some(output) = cached {
73+
return Some(output);
74+
}
75+
76+
// Execute and cache
77+
let output = Command::new("mount").output().ok()?;
78+
with_cache(|cache| {
79+
cache.insert(cache_key, output.clone());
80+
});
81+
Some(output)
82+
}
83+
84+
/// Get cached pgrep output for checking if krun is running (500ms cache)
85+
pub fn is_krun_running() -> bool {
86+
let cache_key = "pgrep_krun".to_string();
87+
88+
// Check cache first
89+
let cached = with_cache(|cache| {
90+
cache.get(&cache_key, PGREP_CACHE_DURATION).cloned()
91+
});
92+
93+
if let Some(output) = cached {
94+
return output.status.success() && !output.stdout.is_empty();
95+
}
96+
97+
// Execute and cache
98+
if let Ok(output) = Command::new("pgrep").args(["-x", "krun"]).output() {
99+
let result = output.status.success() && !output.stdout.is_empty();
100+
with_cache(|cache| {
101+
cache.insert(cache_key, output);
102+
});
103+
return result;
104+
}
105+
106+
false
107+
}
108+
109+
/// Get cached pgrep output for checking if libkrun is running (500ms cache)
110+
pub fn is_libkrun_running() -> bool {
111+
let cache_key = "pgrep_libkrun".to_string();
112+
113+
// Check cache first
114+
let cached = with_cache(|cache| {
115+
cache.get(&cache_key, PGREP_CACHE_DURATION).cloned()
116+
});
117+
118+
if let Some(output) = cached {
119+
return output.status.success() && !output.stdout.is_empty();
120+
}
121+
122+
// Execute and cache
123+
if let Ok(output) = Command::new("pgrep").args(["-f", "libkrun"]).output() {
124+
let result = output.status.success() && !output.stdout.is_empty();
125+
with_cache(|cache| {
126+
cache.insert(cache_key, output);
127+
});
128+
return result;
129+
}
130+
131+
false
132+
}
133+
134+
/// Check if VM is running (uses cached pgrep)
135+
pub fn is_vm_running_cached() -> bool {
136+
is_krun_running() || is_libkrun_running()
137+
}
138+
139+
/// Invalidate all caches (call after mount/unmount operations)
140+
pub fn invalidate_all() {
141+
with_cache(|cache| {
142+
cache.entries.clear();
143+
});
144+
}
145+
146+
/// Invalidate mount-related caches
147+
pub fn invalidate_mount_cache() {
148+
with_cache(|cache| {
149+
cache.invalidate("mount");
150+
});
151+
}
152+
153+
/// Invalidate process-related caches
154+
pub fn invalidate_process_cache() {
155+
with_cache(|cache| {
156+
cache.invalidate("pgrep");
157+
});
158+
}

src-tauri/src/commands/disk.rs

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use std::process::Command;
33
use std::thread;
44
use std::time::Duration;
55
use tauri::{AppHandle, Emitter};
6+
use tokio::time::timeout;
7+
use crate::cache;
68
use crate::cli::execute_command;
9+
use crate::paths::{get_socket_path, COMMAND_TIMEOUT_SECS, MOUNT_TIMEOUT_SECS};
710

811
#[derive(Debug, Clone, Serialize, Deserialize)]
912
pub struct Partition {
@@ -36,8 +39,8 @@ pub struct DiskListResult {
3639

3740
#[tauri::command]
3841
pub async fn list_disks(use_sudo: bool) -> Result<DiskListResult, String> {
39-
// Run in blocking task to avoid freezing UI
40-
tokio::task::spawn_blocking(move || {
42+
// Run in blocking task with timeout to avoid freezing UI
43+
let list_future = tokio::task::spawn_blocking(move || {
4144
// Run both list commands and merge results:
4245
// - `list` (native): correctly detects Linux filesystems on Linux-only cards
4346
// - `list -m` (Microsoft fallback): works with broken GUID tables
@@ -68,9 +71,12 @@ pub async fn list_disks(use_sudo: bool) -> Result<DiskListResult, String> {
6871
result.used_admin_mode = use_sudo;
6972

7073
Ok(result)
71-
})
72-
.await
73-
.map_err(|e| format!("Task error: {}", e))?
74+
});
75+
76+
timeout(Duration::from_secs(COMMAND_TIMEOUT_SECS), list_future)
77+
.await
78+
.map_err(|_| format!("List disks timed out after {} seconds", COMMAND_TIMEOUT_SECS))?
79+
.map_err(|e| format!("Task error: {}", e))?
7480
}
7581

7682
fn merge_disk_results(
@@ -170,7 +176,8 @@ fn update_mount_status(result: &mut DiskListResult) {
170176
fn get_system_mounts() -> Vec<(String, String)> {
171177
let mut mounts = Vec::new();
172178

173-
if let Ok(output) = Command::new("mount").output() {
179+
// Use cached mount output to avoid redundant process spawning
180+
if let Some(output) = cache::get_mount_output() {
174181
let mount_output = String::from_utf8_lossy(&output.stdout);
175182

176183
for line in mount_output.lines() {
@@ -505,18 +512,23 @@ fn parse_type_and_name(parts: &[&str]) -> (String, Option<String>) {
505512

506513
#[tauri::command]
507514
pub async fn mount_disk(app: AppHandle, device: String, passphrase: Option<String>) -> Result<String, String> {
508-
// Run in blocking task to avoid freezing UI during sudo prompt
509-
let result = tokio::task::spawn_blocking(move || {
515+
// Run in blocking task with timeout to avoid freezing UI
516+
let mount_future = tokio::task::spawn_blocking(move || {
510517
let pass_ref = passphrase.as_deref();
511518
let result = execute_command(&["mount", &device], true, pass_ref);
512519

513-
// Give a moment for mount to complete, then verify with retries
514-
// 20 retries × 500ms = 10 seconds total timeout
515-
for _ in 0..20 {
516-
thread::sleep(Duration::from_millis(500));
520+
// Check immediately first, then retry with short intervals
521+
// 40 retries × 250ms = 10 seconds total timeout
522+
for i in 0..40 {
523+
// Invalidate cache to get fresh mount data
524+
cache::invalidate_mount_cache();
517525
if check_nfs_mount_exists() {
518526
return Ok(result.unwrap_or_else(|_| "Mounted successfully".to_string()));
519527
}
528+
// Don't sleep on first iteration - check immediately
529+
if i > 0 {
530+
thread::sleep(Duration::from_millis(250));
531+
}
520532
}
521533

522534
// Mount verification failed - return error with details
@@ -531,9 +543,13 @@ pub async fn mount_disk(app: AppHandle, device: String, passphrase: Option<Strin
531543
}
532544
Err(e) => Err(e),
533545
}
534-
})
535-
.await
536-
.map_err(|e| format!("Task error: {}", e))?;
546+
});
547+
548+
// Apply overall timeout
549+
let result = timeout(Duration::from_secs(MOUNT_TIMEOUT_SECS), mount_future)
550+
.await
551+
.map_err(|_| format!("Mount operation timed out after {} seconds", MOUNT_TIMEOUT_SECS))?
552+
.map_err(|e| format!("Task error: {}", e))?;
537553

538554
// Emit status changed event regardless of success/failure
539555
let _ = app.emit("status-changed", ());
@@ -542,7 +558,8 @@ pub async fn mount_disk(app: AppHandle, device: String, passphrase: Option<Strin
542558
}
543559

544560
fn check_nfs_mount_exists() -> bool {
545-
if let Ok(output) = Command::new("mount").output() {
561+
// Use cached mount output to avoid redundant process spawning
562+
if let Some(output) = cache::get_mount_output() {
546563
let mount_output = String::from_utf8_lossy(&output.stdout);
547564
// Look for anylinuxfs NFS mount pattern
548565
mount_output.contains("localhost:/mnt/") && mount_output.contains("/Volumes/")
@@ -553,24 +570,46 @@ fn check_nfs_mount_exists() -> bool {
553570

554571
#[tauri::command]
555572
pub async fn unmount_disk(app: AppHandle) -> Result<String, String> {
556-
// Run in blocking task - unmount doesn't need sudo
557-
let result = tokio::task::spawn_blocking(|| {
573+
// Run in blocking task with timeout
574+
let unmount_future = tokio::task::spawn_blocking(|| {
558575
execute_command(&["unmount"], false, None)
559-
})
560-
.await
561-
.map_err(|e| format!("Task error: {}", e))?;
576+
});
577+
578+
let result = timeout(Duration::from_secs(COMMAND_TIMEOUT_SECS), unmount_future)
579+
.await
580+
.map_err(|_| format!("Unmount timed out after {} seconds", COMMAND_TIMEOUT_SECS))?
581+
.map_err(|e| format!("Task error: {}", e))?;
582+
583+
// Wait for VM to fully shut down by polling until krun process is gone
584+
// 40 retries × 250ms = 10 seconds max wait
585+
for _ in 0..40 {
586+
if !is_vm_running() {
587+
break;
588+
}
589+
tokio::time::sleep(Duration::from_millis(250)).await;
590+
}
591+
592+
// Invalidate all caches after unmount
593+
cache::invalidate_all();
562594

563595
// Emit status changed event
564596
let _ = app.emit("status-changed", ());
565597

566598
result
567599
}
568600

601+
/// Check if the VM (krun) process is running
602+
fn is_vm_running() -> bool {
603+
// Use cached pgrep result, but invalidate first since we're polling for shutdown
604+
cache::invalidate_process_cache();
605+
cache::is_krun_running()
606+
}
607+
569608
#[tauri::command]
570609
pub async fn eject_disk(device: String) -> Result<String, String> {
571610
// Eject (power down) a disk using diskutil
572611
// First unmount anylinuxfs if it has anything mounted, then eject
573-
tokio::task::spawn_blocking(move || {
612+
let eject_future = tokio::task::spawn_blocking(move || {
574613
// Check if anylinuxfs has anything mounted and unmount first
575614
if check_nfs_mount_exists() {
576615
// Unmount anylinuxfs first - this shuts down the VM properly
@@ -579,10 +618,14 @@ pub async fn eject_disk(device: String) -> Result<String, String> {
579618
// Wait for anylinuxfs to fully stop (up to 5 seconds)
580619
for _ in 0..10 {
581620
thread::sleep(Duration::from_millis(500));
621+
// Invalidate cache to get fresh mount data
622+
cache::invalidate_mount_cache();
582623
if !check_nfs_mount_exists() {
583624
break;
584625
}
585626
}
627+
// Invalidate all caches after unmount
628+
cache::invalidate_all();
586629
}
587630

588631
// Now safe to eject the disk
@@ -597,9 +640,12 @@ pub async fn eject_disk(device: String) -> Result<String, String> {
597640
let stderr = String::from_utf8_lossy(&output.stderr);
598641
Err(format!("Failed to eject: {}", stderr))
599642
}
600-
})
601-
.await
602-
.map_err(|e| format!("Task error: {}", e))?
643+
});
644+
645+
timeout(Duration::from_secs(COMMAND_TIMEOUT_SECS), eject_future)
646+
.await
647+
.map_err(|_| format!("Eject timed out after {} seconds", COMMAND_TIMEOUT_SECS))?
648+
.map_err(|e| format!("Task error: {}", e))?
603649
}
604650

605651
#[tauri::command]
@@ -623,9 +669,9 @@ pub async fn force_cleanup() -> Result<String, String> {
623669
}
624670

625671
// Remove socket file if it exists
626-
let socket_path = "/tmp/anylinuxfs.sock";
627-
if std::path::Path::new(socket_path).exists() {
628-
if std::fs::remove_file(socket_path).is_ok() {
672+
let socket_path = get_socket_path();
673+
if socket_path.exists() {
674+
if std::fs::remove_file(&socket_path).is_ok() {
629675
killed.push("socket");
630676
}
631677
}

src-tauri/src/commands/log.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::sync::mpsc::channel;
77
use std::sync::Arc;
88
use std::time::{Duration, Instant};
99
use tauri::{AppHandle, Emitter, Manager};
10+
use crate::paths::get_log_path;
1011

1112
/// State to track and control watcher threads
1213
pub struct WatcherState {
@@ -27,14 +28,6 @@ impl Default for WatcherState {
2728
}
2829
}
2930

30-
fn get_log_path() -> PathBuf {
31-
if let Some(home) = dirs::home_dir() {
32-
home.join("Library/Logs/anylinuxfs.log")
33-
} else {
34-
PathBuf::from("/tmp/anylinuxfs.log")
35-
}
36-
}
37-
3831
#[tauri::command]
3932
pub fn get_log_content(lines: Option<usize>) -> Result<Vec<String>, String> {
4033
let log_path = get_log_path();

0 commit comments

Comments
 (0)