rvlib/tools/wand/
server.rs1use 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 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 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#[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 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 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}