Rust · 14127 bytes Raw Blame History
1 //! Prefix lifecycle management
2
3 use crate::config::WandaConfig;
4 use crate::error::{Result, WandaError};
5 use crate::prefix::PrefixBuilder;
6 use crate::steam::ProtonVersion;
7 use serde::{Deserialize, Serialize};
8 use std::path::{Path, PathBuf};
9 use tracing::{debug, info, warn};
10
11 /// Health status of a Wine prefix
12 #[derive(Debug, Clone, PartialEq, Eq)]
13 pub enum PrefixHealth {
14 /// Prefix is healthy and ready for use
15 Healthy,
16 /// Prefix has issues that may be repairable
17 NeedsRepair(Vec<PrefixIssue>),
18 /// Prefix is corrupted beyond repair
19 Corrupted(String),
20 /// Prefix doesn't exist yet
21 NotCreated,
22 }
23
24 /// Specific issues that can affect a prefix
25 #[derive(Debug, Clone, PartialEq, Eq)]
26 pub enum PrefixIssue {
27 /// .NET Framework is missing
28 MissingDotNet,
29 /// Required dependency is missing
30 MissingDependency(String),
31 /// Registry appears corrupted
32 CorruptedRegistry,
33 /// WeMod is not installed
34 WemodNotInstalled,
35 /// WeMod version is outdated
36 WemodOutdated,
37 /// Wine prefix structure is incomplete
38 IncompletePrefix,
39 }
40
41 impl std::fmt::Display for PrefixIssue {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 Self::MissingDotNet => write!(f, ".NET Framework 4.8 not installed"),
45 Self::MissingDependency(dep) => write!(f, "Missing dependency: {}", dep),
46 Self::CorruptedRegistry => write!(f, "Windows registry is corrupted"),
47 Self::WemodNotInstalled => write!(f, "WeMod is not installed"),
48 Self::WemodOutdated => write!(f, "WeMod is outdated"),
49 Self::IncompletePrefix => write!(f, "Wine prefix structure is incomplete"),
50 }
51 }
52 }
53
54 /// Metadata about a WANDA prefix
55 #[derive(Debug, Clone, Serialize, Deserialize)]
56 pub struct WandaPrefix {
57 /// Name of this prefix
58 pub name: String,
59 /// Path to the prefix
60 pub path: PathBuf,
61 /// Whether WeMod is installed in this prefix
62 pub wemod_installed: bool,
63 /// Installed WeMod version
64 pub wemod_version: Option<String>,
65 /// Proton version used to create this prefix
66 pub proton_version: Option<String>,
67 /// When the prefix was created
68 pub created_at: Option<String>,
69 /// When the prefix was last used
70 pub last_used: Option<String>,
71 }
72
73 impl WandaPrefix {
74 /// Get the path to the actual Wine prefix (pfx subdirectory)
75 /// Proton stores the Wine prefix in a `pfx` subdirectory
76 pub fn pfx_path(&self) -> PathBuf {
77 self.path.join("pfx")
78 }
79
80 /// Get the path to drive_c (inside the pfx directory)
81 pub fn drive_c(&self) -> PathBuf {
82 self.pfx_path().join("drive_c")
83 }
84
85 /// Get the path to the Windows user folder
86 pub fn user_folder(&self) -> PathBuf {
87 self.drive_c().join("users/steamuser")
88 }
89
90 /// Get the expected WeMod installation path
91 /// Version 12.x uses "Wand" branding, version 11.x uses "WeMod"
92 pub fn wemod_path(&self) -> PathBuf {
93 // Check for new "Wand" branding first (version 12.x)
94 let wand_path = self.user_folder().join("AppData/Local/Wand");
95 if wand_path.exists() {
96 return wand_path;
97 }
98
99 // Fall back to old "WeMod" branding (version 11.x)
100 let wemod_path = self.user_folder().join("AppData/Local/WeMod");
101 if wemod_path.exists() {
102 return wemod_path;
103 }
104
105 // Default to WeMod path for new installations (version 11.5.0 is pinned)
106 wemod_path
107 }
108
109 /// Get the WeMod executable path (Squirrel stub)
110 pub fn wemod_exe(&self) -> PathBuf {
111 self.wemod_path().join("WeMod.exe")
112 }
113
114 /// Get the real WeMod executable inside the versioned app directory
115 ///
116 /// WeMod uses Squirrel for updates. The root WeMod.exe is a small stub
117 /// that spawns the real Electron app from `app-{version}/WeMod.exe` and
118 /// exits. For standalone mode we need the real exe so Proton keeps the
119 /// Wine session alive.
120 pub fn wemod_app_exe(&self) -> Option<PathBuf> {
121 let wemod_dir = self.wemod_path();
122 let mut best: Option<(String, PathBuf)> = None;
123
124 if let Ok(entries) = std::fs::read_dir(&wemod_dir) {
125 for entry in entries.flatten() {
126 let name = entry.file_name().to_string_lossy().to_string();
127 if name.starts_with("app-") && entry.path().is_dir() {
128 let exe = entry.path().join("WeMod.exe");
129 if exe.exists() {
130 // Pick the highest version directory
131 if best.as_ref().map_or(true, |(v, _)| name > *v) {
132 best = Some((name, exe));
133 }
134 }
135 }
136 }
137 }
138
139 best.map(|(_, path)| path)
140 }
141
142 /// Get alternative WeMod paths for checking installation
143 pub fn wemod_paths_all(&self) -> Vec<PathBuf> {
144 vec![
145 self.user_folder().join("AppData/Local/Wand"),
146 self.user_folder().join("AppData/Local/WeMod"),
147 ]
148 }
149
150 /// Get the system32 directory
151 pub fn system32(&self) -> PathBuf {
152 self.drive_c().join("windows/system32")
153 }
154 }
155
156 /// Manages WANDA Wine prefixes
157 pub struct PrefixManager {
158 /// Base directory for prefixes
159 pub base_path: PathBuf,
160 /// Currently loaded prefixes
161 prefixes: Vec<WandaPrefix>,
162 }
163
164 impl PrefixManager {
165 /// Create a new prefix manager
166 pub fn new(config: &WandaConfig) -> Self {
167 Self {
168 base_path: config.prefix_base_path(),
169 prefixes: Vec::new(),
170 }
171 }
172
173 /// Load all existing prefixes
174 pub fn load(&mut self) -> Result<()> {
175 self.prefixes.clear();
176
177 if !self.base_path.exists() {
178 debug!("Prefix base path doesn't exist yet: {}", self.base_path.display());
179 return Ok(());
180 }
181
182 // Look for prefix metadata files
183 let entries = std::fs::read_dir(&self.base_path)?;
184
185 for entry in entries.flatten() {
186 let path = entry.path();
187 if path.is_dir() {
188 let meta_path = path.join("wanda.json");
189 if meta_path.exists() {
190 match self.load_prefix_metadata(&meta_path) {
191 Ok(prefix) => {
192 debug!("Loaded prefix: {}", prefix.name);
193 self.prefixes.push(prefix);
194 }
195 Err(e) => {
196 warn!("Failed to load prefix at {}: {}", path.display(), e);
197 }
198 }
199 }
200 }
201 }
202
203 info!("Loaded {} prefixes", self.prefixes.len());
204 Ok(())
205 }
206
207 /// Load prefix metadata from file
208 fn load_prefix_metadata(&self, path: &Path) -> Result<WandaPrefix> {
209 let content = std::fs::read_to_string(path)?;
210 let prefix: WandaPrefix = serde_json::from_str(&content)?;
211 Ok(prefix)
212 }
213
214 /// Save prefix metadata to file
215 fn save_prefix_metadata(prefix: &WandaPrefix) -> Result<()> {
216 let meta_path = prefix.path.join("wanda.json");
217 let content = serde_json::to_string_pretty(prefix)?;
218 std::fs::write(meta_path, content)?;
219 Ok(())
220 }
221
222 /// Get all prefixes
223 pub fn list(&self) -> &[WandaPrefix] {
224 &self.prefixes
225 }
226
227 /// Get a prefix by name
228 pub fn get(&self, name: &str) -> Option<&WandaPrefix> {
229 self.prefixes.iter().find(|p| p.name == name)
230 }
231
232 /// Get the default prefix (creates one if it doesn't exist)
233 pub async fn get_or_create_default(
234 &mut self,
235 proton: &ProtonVersion,
236 ) -> Result<&WandaPrefix> {
237 const DEFAULT_NAME: &str = "default";
238
239 // Check if default exists
240 if self.get(DEFAULT_NAME).is_some() {
241 // Validate health
242 let health = self.validate(DEFAULT_NAME)?;
243 if health != PrefixHealth::Healthy {
244 warn!("Default prefix needs attention: {:?}", health);
245 }
246 return Ok(self.get(DEFAULT_NAME).unwrap());
247 }
248
249 // Create the default prefix
250 self.create(DEFAULT_NAME, proton).await?;
251 Ok(self.get(DEFAULT_NAME).unwrap())
252 }
253
254 /// Create a new prefix
255 pub async fn create(&mut self, name: &str, proton: &ProtonVersion) -> Result<WandaPrefix> {
256 let prefix_path = self.base_path.join(name);
257
258 if prefix_path.exists() {
259 return Err(WandaError::PrefixCreationFailed {
260 path: prefix_path,
261 reason: "Prefix already exists".to_string(),
262 });
263 }
264
265 info!("Creating prefix '{}' at {}", name, prefix_path.display());
266
267 // Use PrefixBuilder to set up the prefix
268 let builder = PrefixBuilder::new(&prefix_path, proton);
269 builder.build().await?;
270
271 // Create metadata
272 let prefix = WandaPrefix {
273 name: name.to_string(),
274 path: prefix_path,
275 wemod_installed: false,
276 wemod_version: None,
277 proton_version: Some(proton.name.clone()),
278 created_at: Some(chrono_lite_now()),
279 last_used: None,
280 };
281
282 Self::save_prefix_metadata(&prefix)?;
283 self.prefixes.push(prefix.clone());
284
285 info!("Prefix '{}' created successfully", name);
286 Ok(prefix)
287 }
288
289 /// Validate a prefix's health
290 pub fn validate(&self, name: &str) -> Result<PrefixHealth> {
291 let prefix = self.get(name).ok_or_else(|| WandaError::PrefixNotFound {
292 path: self.base_path.join(name),
293 })?;
294
295 let mut issues = Vec::new();
296
297 // Check basic structure
298 if !prefix.drive_c().exists() {
299 return Ok(PrefixHealth::NotCreated);
300 }
301
302 if !prefix.system32().exists() {
303 issues.push(PrefixIssue::IncompletePrefix);
304 }
305
306 // Check for registry files (in the pfx directory)
307 let system_reg = prefix.pfx_path().join("system.reg");
308 let user_reg = prefix.pfx_path().join("user.reg");
309 if !system_reg.exists() || !user_reg.exists() {
310 issues.push(PrefixIssue::CorruptedRegistry);
311 }
312
313 // Check .NET (look for mscorlib.dll as indicator)
314 let dotnet_indicator = prefix.drive_c().join("windows/Microsoft.NET");
315 if !dotnet_indicator.exists() {
316 issues.push(PrefixIssue::MissingDotNet);
317 }
318
319 // Check WeMod
320 if !prefix.wemod_exe().exists() {
321 issues.push(PrefixIssue::WemodNotInstalled);
322 }
323
324 if issues.is_empty() {
325 Ok(PrefixHealth::Healthy)
326 } else if issues.contains(&PrefixIssue::CorruptedRegistry) {
327 Ok(PrefixHealth::Corrupted("Registry files are missing or corrupted".to_string()))
328 } else {
329 Ok(PrefixHealth::NeedsRepair(issues))
330 }
331 }
332
333 /// Attempt to repair a prefix
334 pub async fn repair(&mut self, name: &str, proton: &ProtonVersion) -> Result<()> {
335 let health = self.validate(name)?;
336
337 match health {
338 PrefixHealth::Healthy => {
339 info!("Prefix '{}' is already healthy", name);
340 return Ok(());
341 }
342 PrefixHealth::Corrupted(reason) => {
343 return Err(WandaError::PrefixCorrupted {
344 path: self.base_path.join(name),
345 reason,
346 });
347 }
348 PrefixHealth::NotCreated => {
349 info!("Prefix doesn't exist, creating it");
350 self.create(name, proton).await?;
351 return Ok(());
352 }
353 PrefixHealth::NeedsRepair(issues) => {
354 info!("Repairing prefix '{}': {:?}", name, issues);
355 // Get prefix path before mutable borrow
356 let prefix_path = self.base_path.join(name);
357 let builder = PrefixBuilder::new(&prefix_path, proton);
358
359 for issue in issues {
360 match issue {
361 PrefixIssue::MissingDotNet => {
362 info!("Installing .NET Framework...");
363 builder.install_dotnet().await?;
364 }
365 PrefixIssue::MissingDependency(dep) => {
366 info!("Installing dependency: {}", dep);
367 builder.install_winetricks(&[&dep]).await?;
368 }
369 PrefixIssue::WemodNotInstalled | PrefixIssue::WemodOutdated => {
370 // WeMod installation is handled separately
371 info!("WeMod needs to be installed/updated");
372 }
373 _ => {
374 warn!("Cannot automatically repair: {:?}", issue);
375 }
376 }
377 }
378 }
379 }
380
381 Ok(())
382 }
383
384 /// Delete a prefix
385 pub fn delete(&mut self, name: &str) -> Result<()> {
386 let prefix_path = self.base_path.join(name);
387
388 if !prefix_path.exists() {
389 return Err(WandaError::PrefixNotFound { path: prefix_path });
390 }
391
392 info!("Deleting prefix '{}' at {}", name, prefix_path.display());
393 std::fs::remove_dir_all(&prefix_path)?;
394
395 self.prefixes.retain(|p| p.name != name);
396
397 Ok(())
398 }
399
400 /// Update prefix metadata (e.g., after WeMod installation)
401 pub fn update_metadata(&mut self, name: &str, updater: impl FnOnce(&mut WandaPrefix)) -> Result<()> {
402 if let Some(prefix) = self.prefixes.iter_mut().find(|p| p.name == name) {
403 updater(prefix);
404 Self::save_prefix_metadata(prefix)?;
405 }
406 Ok(())
407 }
408 }
409
410 /// Get current timestamp as ISO string (simple implementation)
411 fn chrono_lite_now() -> String {
412 use std::time::{SystemTime, UNIX_EPOCH};
413 let duration = SystemTime::now()
414 .duration_since(UNIX_EPOCH)
415 .unwrap_or_default();
416 format!("{}", duration.as_secs())
417 }
418