Skip to main content

rvlib/tools/wand/
server.rs

1use rvimage_domain::{RvResult, to_rv};
2use std::fmt::Debug;
3use std::fs;
4use std::path::Path;
5use std::process::{Child, Command};
6
7use crate::cfg::CmdServerSrc;
8use crate::file_util;
9use crate::result::trace_ok_err;
10
11fn install_uv() -> RvResult<()> {
12    if cfg!(target_os = "windows") {
13        // Windows: try `powershell` and `pwsh`.
14        let args = [
15            "-NoProfile",
16            "-NonInteractive",
17            "-ExecutionPolicy",
18            "Bypass",
19            "-Command",
20            "iwr -useb https://astral.sh/uv/install.ps1 | iex",
21        ];
22        let mut errors = Vec::new();
23        for exe in &["powershell", "pwsh"] {
24            match Command::new(exe).args(args).output() {
25                Ok(out) if out.status.success() => {
26                    tracing::info!("Installed uv using {}.", exe);
27                    return Ok(());
28                }
29                Ok(out) => {
30                    let err_msg = String::from_utf8_lossy(&out.stderr);
31                    errors.push(format!("{} failed: {}", exe, err_msg.trim()));
32                    tracing::warn!(
33                        "{} install exited with status {:?}: {}",
34                        exe,
35                        out.status.code(),
36                        err_msg
37                    );
38                }
39                Err(e) => {
40                    errors.push(format!("failed to spawn {}: {}", exe, e));
41                    tracing::warn!("failed to spawn {}: {}", exe, e);
42                }
43            }
44        }
45        return Err(to_rv(format!(
46            "Failed to install uv on Windows: {}",
47            errors.join("; ")
48        )));
49    } else {
50        // macOS and Linux
51        let status = Command::new("sh")
52            .args(["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"])
53            .status()
54            .map_err(to_rv)?;
55        status
56            .success()
57            .then_some(())
58            .ok_or_else(|| to_rv("Failed to install uv on macOS/Linux".to_string()))?;
59    }
60    Ok(())
61}
62
63pub trait WandServer: Debug + Send + Sync {
64    fn cleanup_server(&mut self) -> RvResult<()>;
65    fn start_server(&mut self, prj_path: &Path) -> RvResult<()>;
66    fn stop_server(&mut self) -> RvResult<()>;
67}
68
69/// A Wand server implementation that runs an external command to start the server.
70///
71/// # Fields
72/// - `src`: Where to get the executable/source code for the server
73/// - `setup_cmd`: Command to set up and run the server
74/// - `setup_args`: Arguments for the setup command
75/// - `local_folder`: Local folder to extract the source code, default is $HOME/wand_server
76/// - `child`: The child process running the server
77///
78#[derive(Debug)]
79pub struct CmdServer {
80    src: CmdServerSrc,
81    additional_files: Vec<String>,
82    setup_cmd: String,
83    setup_args: Vec<String>,
84    local_base_folder: String,
85    install_uv: bool,
86    child: Option<Child>,
87}
88
89impl CmdServer {
90    pub fn new(
91        src: CmdServerSrc,
92        additional_files: Vec<String>,
93        setup_cmd: String,
94        setup_args: Vec<String>,
95        install_uv: bool,
96        local_folder: String,
97    ) -> Self {
98        CmdServer {
99            src,
100            additional_files,
101            setup_cmd,
102            setup_args,
103            local_base_folder: local_folder,
104            install_uv,
105            child: None,
106        }
107    }
108}
109
110impl WandServer for CmdServer {
111    fn start_server(&mut self, prj_path: &Path) -> RvResult<()> {
112        if self.install_uv {
113            tracing::info!("Installing uv...");
114            install_uv()?;
115        }
116
117        let local_repo_path =
118            Path::new(&self.local_base_folder).join(self.src.relative_working_dir());
119
120        // Check if the folder already exists
121        if local_repo_path.exists() && local_repo_path.read_dir().map_err(to_rv)?.next().is_some() {
122            tracing::info!(
123                "Local folder {} already exists and is not empty. Skipping download.",
124                self.local_base_folder
125            );
126        } else {
127            tracing::info!(
128                "Copying or downloading wand server and unzipping {:?} to {}...",
129                self.src,
130                self.local_base_folder
131            );
132            self.src
133                .put_to_dst(prj_path, Path::new(&self.local_base_folder))?;
134        }
135        for af in &self.additional_files {
136            let src_path = Path::new(af);
137            let src_path = file_util::relative_to_prj_path(prj_path, src_path)?;
138            let file_name = src_path
139                .file_name()
140                .ok_or_else(|| to_rv(format!("Invalid additional file path: {}", af)))?;
141            let dest_path = local_repo_path.join(file_name);
142            if !dest_path.exists() {
143                tracing::info!(
144                    "Copying additional file {} to {}...",
145                    af,
146                    dest_path.display()
147                );
148                fs::copy(src_path, dest_path).map_err(to_rv)?;
149            }
150        }
151        let churdir = format!(
152            "{}/{}",
153            self.local_base_folder,
154            self.src.relative_working_dir()
155        );
156        tracing::info!("Starting wand server from folder {churdir}...");
157
158        let child = Command::new(&self.setup_cmd)
159            .args(&self.setup_args)
160            .env("PYTHONPATH", ".")
161            .current_dir(churdir)
162            .spawn()
163            .map_err(to_rv)?;
164        self.child = Some(child);
165        tracing::info!("Wand server up and running.");
166        Ok(())
167    }
168
169    fn cleanup_server(&mut self) -> RvResult<()> {
170        // Remove the local repo folder
171        if Path::new(&self.local_base_folder).exists() {
172            tracing::info!("Removing local folder {}...", self.local_base_folder);
173            fs::remove_dir_all(&self.local_base_folder).map_err(to_rv)?;
174        }
175        trace_ok_err(self.stop_server());
176        Ok(())
177    }
178
179    fn stop_server(&mut self) -> RvResult<()> {
180        if let Some(mut child) = self.child.take() {
181            tracing::warn!(
182                "child process will be killed, but processes spawned by this might still exist"
183            );
184            child.kill().map_err(to_rv)?;
185        }
186        Ok(())
187    }
188}
189impl Drop for CmdServer {
190    fn drop(&mut self) {
191        let _ = self.stop_server();
192    }
193}