DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/IPC/DevLog/AppDataPrefix.rs
1//! Resolve the Tauri app-data prefix for THIS profile so logs
2//! and aliasing pick the right `~/Library/Application Support/
3//! land.editor.*.mountain` directory. The detection walks the
4//! Application Support tree, prefers a strict suffix match
5//! against the binary signature, falls back to the first
6//! `*.mountain` candidate so a mismatch still produces a
7//! usable path.
8
9use std::sync::{Mutex, OnceLock};
10
11// Two-phase resolution:
12// • `RESOLVED` is set permanently once we find a real prefix.
13// • `FAILED_ONCE` records whether the first attempt returned None so
14// subsequent writes can retry after Tauri has created the directory,
15// rather than caching None forever and routing all logs to /tmp.
16static RESOLVED:OnceLock<String> = OnceLock::new();
17
18static RETRY:Mutex<bool> = Mutex::new(true);
19
20pub fn Fn() -> Option<&'static str> {
21 if let Some(S) = RESOLVED.get() {
22 return Some(S.as_str());
23 }
24
25 // Fast-path: guard without taking the mutex if we already have a result.
26 let Ok(mut Guard) = RETRY.try_lock() else {
27 return None;
28 };
29
30 if !*Guard {
31 return None;
32 }
33
34 if let Some(Prefix) = DetectAppDataPrefix() {
35 // RESOLVED may already be set by a concurrent caller - that's fine.
36 let _ = RESOLVED.set(Prefix);
37
38 *Guard = false;
39 return RESOLVED.get().map(String::as_str);
40 }
41
42 // Not found yet - leave RETRY=true so the next write retries.
43 None
44}
45
46fn BinarySignature() -> String {
47 let PackageName = env!("CARGO_PKG_NAME");
48
49 let Segments:Vec<&str> = PackageName.split('_').collect();
50
51 let Take = Segments.len().min(4);
52
53 let Start = Segments.len().saturating_sub(Take);
54
55 Segments[Start..]
56 .iter()
57 .flat_map(|Segment| SplitPascalCaseIntoWords(Segment))
58 .collect::<Vec<String>>()
59 .join(".")
60 .to_ascii_lowercase()
61}
62
63fn SplitPascalCaseIntoWords(Segment:&str) -> Vec<String> {
64 let mut Words:Vec<String> = Vec::new();
65
66 let mut Current = String::new();
67
68 let mut PrevWasUpper = false;
69
70 let mut PrevWasDigit = false;
71
72 for Ch in Segment.chars() {
73 let IsUpper = Ch.is_ascii_uppercase();
74
75 let IsDigit = Ch.is_ascii_digit();
76
77 let NeedBreak =
78 !Current.is_empty() && ((IsUpper && !PrevWasUpper) || (IsDigit != PrevWasDigit && !Current.is_empty()));
79
80 if NeedBreak {
81 Words.push(std::mem::take(&mut Current));
82 }
83
84 Current.push(Ch);
85
86 PrevWasUpper = IsUpper;
87
88 PrevWasDigit = IsDigit;
89 }
90
91 if !Current.is_empty() {
92 Words.push(Current);
93 }
94
95 Words.into_iter().filter(|Word| !Word.is_empty()).collect()
96}
97
98fn DetectAppDataPrefix() -> Option<String> {
99 let Home = std::env::var("HOME").ok()?;
100
101 let Base = format!("{}/Library/Application Support", Home);
102
103 let Signature = BinarySignature();
104
105 let mut FirstMatchingMountain:Option<String> = None;
106
107 if let Ok(Entries) = std::fs::read_dir(&Base) {
108 for Entry in Entries.flatten() {
109 let Name = Entry.file_name();
110
111 let Name = Name.to_string_lossy().into_owned();
112
113 if !Name.starts_with("land.editor.") || !Name.contains("mountain") {
114 continue;
115 }
116
117 if Name.ends_with(&Signature) {
118 return Some(format!("{}/{}", Base, Name));
119 }
120
121 if FirstMatchingMountain.is_none() {
122 FirstMatchingMountain = Some(format!("{}/{}", Base, Name));
123 }
124 }
125 }
126
127 FirstMatchingMountain
128}