Skip to main content

hexz_cli/cmd/data/
shell.rs

1//! Mount an archive and spawn a subshell.
2
3use anyhow::{Context, Result};
4use colored::Colorize;
5use hexz_fuse::fuse::Hexz;
6use std::path::PathBuf;
7use std::process::Command;
8
9use super::mount::open_archive;
10
11/// Execute the `hexz shell` command to mount an archive and spawn a subshell.
12#[allow(unsafe_code)]
13pub fn run(
14    hexz_path: &str,
15    overlay: Option<PathBuf>,
16    editable: bool,
17    cache_size: Option<&str>,
18) -> Result<()> {
19    // SAFETY: getuid() is always safe to call
20    let uid = unsafe { libc::getuid() };
21    // SAFETY: getgid() is always safe to call
22    let gid = unsafe { libc::getgid() };
23
24    let snap = open_archive(hexz_path, cache_size, None)?;
25
26    // Create temp mountpoint
27    let tmp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
28    let mountpoint = tmp_dir.path().to_path_buf();
29
30    // Create temp metadata dir for workspace config
31    let tmp_meta = tempfile::tempdir().context("Failed to create temporary metadata directory")?;
32    let metadata_dir = tmp_meta.path().to_path_buf();
33
34    // Handle --editable / --overlay
35    let (overlay, is_temp_overlay) = if let Some(o) = overlay {
36        std::fs::create_dir_all(&o)?;
37        (Some(o), false)
38    } else if editable {
39        let temp_overlay = tempfile::tempdir().context("Failed to create temporary overlay")?;
40        let path = temp_overlay.path().to_path_buf();
41        let _ = temp_overlay.keep();
42        (Some(path), true)
43    } else {
44        (None, false)
45    };
46
47    // Initialize workspace config so `hexz status` works
48    {
49        let host_cwd = std::env::current_dir().ok();
50        let config = crate::cmd::data::workspace::WorkspaceConfig {
51            base_archive: Some(std::fs::canonicalize(hexz_path)?),
52            overlay_path: overlay.clone(),
53            host_cwd,
54            remotes: std::collections::HashMap::new(),
55        };
56        let config_path = metadata_dir.join("config.json");
57        let f = std::fs::File::create(config_path)?;
58        serde_json::to_writer_pretty(f, &config)?;
59    }
60
61    let fs = Hexz::new(snap, uid, gid, overlay.clone(), Some(&metadata_dir))?;
62
63    let mut options = vec![
64        fuser::MountOption::FSName("hexz".to_string()),
65        fuser::MountOption::DefaultPermissions,
66    ];
67
68    if overlay.is_none() {
69        options.push(fuser::MountOption::RO);
70    }
71
72    println!(
73        "  {} Mounting archive at {}",
74        "→".yellow(),
75        mountpoint.display().to_string().cyan()
76    );
77    if let Some(ref o) = overlay {
78        println!(
79            "  {} Using overlay: {}",
80            "→".yellow(),
81            o.display().to_string().bright_black()
82        );
83    }
84
85    // Mount in background thread
86    let mountpoint_clone = mountpoint.clone();
87    let options_clone = options.clone();
88    drop(std::thread::spawn(move || {
89        let _ = fuser::mount2(fs, mountpoint_clone, &options_clone);
90    }));
91
92    // Give FUSE a moment to actually mount
93    std::thread::sleep(std::time::Duration::from_millis(200));
94
95    // Spawn shell
96    let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
97    println!(
98        "  {} Dropping into shell: {}",
99        "→".yellow(),
100        shell.bright_black()
101    );
102    println!(
103        "  {} Type {} to unmount and exit.",
104        "→".yellow(),
105        "exit".bold()
106    );
107
108    let status = Command::new(&shell)
109        .current_dir(&mountpoint)
110        .status()
111        .context("Failed to spawn shell")?;
112
113    if !status.success() {
114        eprintln!("  {} Shell exited with status: {}", "✗".red(), status);
115    }
116
117    // Cleanup
118    println!("  {} Unmounting...", "→".yellow());
119
120    // Attempt to unmount
121    #[cfg(target_os = "linux")]
122    {
123        // Try fusermount3 first, then fusermount
124        if Command::new("fusermount3")
125            .arg("-u")
126            .arg(&mountpoint)
127            .status()
128            .is_err()
129        {
130            let _ = Command::new("fusermount")
131                .arg("-u")
132                .arg(&mountpoint)
133                .status();
134        }
135    }
136    #[cfg(target_os = "macos")]
137    let _ = Command::new("umount").arg(&mountpoint).status();
138
139    if is_temp_overlay {
140        if let Some(o) = overlay {
141            let _ = std::fs::remove_dir_all(o);
142        }
143    }
144
145    Ok(())
146}