@@ -140,63 +140,35 @@ fn ResolveLogDirectory() -> PathBuf {
140140 Base . join ( Stamp )
141141}
142142
143- fn FormatTimestamp ( ) -> String {
143+ /// Session timestamp in local time, cached once per process. MUST match
144+ /// whatever `WindServiceHandlers.rs::"nativeHost:getEnvironmentPaths"`
145+ /// builds, because VS Code's file service writes `window1/output/*.log`
146+ /// into the directory that handler returns — if DevLog and VS Code use
147+ /// different timezones, `Mountain.dev.log` and the `window1/` subtree
148+ /// land in two sibling directories 2–3 hours apart, which makes every
149+ /// post-mortem investigation start with "which folder has the real
150+ /// log?". Picking `chrono::Local::now()` matches the VS Code convention
151+ /// (Tauri's tauri-plugin-log also writes local-time `YYYYMMDDTHHMMSS`).
152+ ///
153+ /// The format string is deliberately identical to the handler's
154+ /// `"%Y%m%dT%H%M%S"`, and both sides pull from the same OnceLock via
155+ /// `SessionTimestamp()` so re-entrant calls from anywhere in the
156+ /// codebase produce the same string.
157+ pub fn SessionTimestamp ( ) -> String {
144158 static STAMP : OnceLock < String > = OnceLock :: new ( ) ;
145159 STAMP
146- . get_or_init ( || {
147- let Duration = SystemTime :: now ( ) . duration_since ( std:: time:: UNIX_EPOCH ) . unwrap_or_default ( ) ;
148- let Secs = Duration . as_secs ( ) as i64 ;
149- // Minimal UTC breakdown without pulling chrono into DevLog: the
150- // Tauri plugin format is `YYYYMMDDTHHMMSS`, which is easy to build
151- // manually from the epoch.
152- let Days = Secs / 86_400 ;
153- let SecondsOfDay = ( Secs % 86_400 ) as u32 ;
154- let Hour = SecondsOfDay / 3_600 ;
155- let Minute = ( SecondsOfDay % 3_600 ) / 60 ;
156- let Second = SecondsOfDay % 60 ;
157- let ( Year , Month , Day ) = DaysToYMD ( Days ) ;
158- format ! ( "{:04}{:02}{:02}T{:02}{:02}{:02}" , Year , Month , Day , Hour , Minute , Second )
159- } )
160+ . get_or_init ( || chrono:: Local :: now ( ) . format ( "%Y%m%dT%H%M%S" ) . to_string ( ) )
160161 . clone ( )
161162}
162163
163- /// Convert days-since-epoch to (year, month, day). Vendored here to avoid
164- /// dragging chrono into the dev-log hot path.
165- fn DaysToYMD ( Days : i64 ) -> ( i64 , u32 , u32 ) {
166- let mut Year = 1970_i64 ;
167- let mut Remaining = Days ;
168- loop {
169- let YearLen = if IsLeap ( Year ) { 366 } else { 365 } ;
170- if Remaining < YearLen as i64 {
171- break ;
172- }
173- Remaining -= YearLen as i64 ;
174- Year += 1 ;
175- }
176- let MonthLengths = [
177- 31u32 ,
178- if IsLeap ( Year ) { 29 } else { 28 } ,
179- 31 ,
180- 30 ,
181- 31 ,
182- 30 ,
183- 31 ,
184- 31 ,
185- 30 ,
186- 31 ,
187- 30 ,
188- 31 ,
189- ] ;
190- let mut Month = 0_usize ;
191- let mut Day = Remaining as u32 ;
192- while Month < 12 && Day >= MonthLengths [ Month ] {
193- Day -= MonthLengths [ Month ] ;
194- Month += 1 ;
195- }
196- ( Year , ( Month as u32 ) + 1 , Day + 1 )
197- }
164+ fn FormatTimestamp ( ) -> String { SessionTimestamp ( ) }
198165
199- fn IsLeap ( Year : i64 ) -> bool { ( Year % 4 == 0 && Year % 100 != 0 ) || Year % 400 == 0 }
166+ // `DaysToYMD` + `IsLeap` were previously used to build a UTC timestamp
167+ // string without pulling chrono into DevLog. Replaced by
168+ // `chrono::Local::now()` in `SessionTimestamp()` so this file agrees
169+ // with `WindServiceHandlers.rs::"nativeHost:getEnvironmentPaths"` on
170+ // the session-log directory. chrono is already a Mountain dependency,
171+ // so the vendored date math is dead weight now.
200172
201173/// Initialise the file sink on first call. Silently falls through to a
202174/// disabled sink if the directory or file can't be created — the caller
@@ -258,21 +230,65 @@ pub fn WriteToFile(Line:&str) {
258230// The app-data directory name is absurdly long. In short mode, alias it.
259231static APP_DATA_PREFIX : OnceLock < Option < String > > = OnceLock :: new ( ) ;
260232
233+ /// Produce an identity signature for THIS running binary derived from
234+ /// `CARGO_PKG_NAME` (which Maintain sets to the long PascalCase product
235+ /// name before `cargo build`). Each profile produces a distinct signature
236+ /// — `_Debug_Mountain` → `.debug.mountain`, `_Compile_Mountain` →
237+ /// `.compile.mountain`, `_Bundle_Clean_Debug_ElectronProfile_Mountain`
238+ /// → `.debug.electron.profile.mountain` — so a candidate directory in
239+ /// `~/Library/Application Support/` can be disambiguated against every
240+ /// other `land.editor.*.mountain` leftover from prior runs.
241+ ///
242+ /// Only the last three underscore-delimited segments are used: the
243+ /// leading `DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_
244+ /// 22NodeVersion_Bundle_Clean` prefix is identical across profiles and
245+ /// doesn't help disambiguate, while the tail (`Debug_Mountain` vs
246+ /// `Compile_Mountain` vs `Debug_ElectronProfile_Mountain`) is where the
247+ /// per-profile identity lives.
248+ fn BinarySignature ( ) -> String {
249+ let PackageName = env ! ( "CARGO_PKG_NAME" ) ;
250+ let Segments : Vec < & str > = PackageName . split ( '_' ) . collect ( ) ;
251+ let Take = Segments . len ( ) . min ( 4 ) ;
252+ let Start = Segments . len ( ) . saturating_sub ( Take ) ;
253+ let Tail = Segments [ Start ..] . join ( "_" ) ;
254+ // Lowercase + `_` → `.` gives the same segment order the Tauri
255+ // identifier uses (identifiers are dot-delimited lowercase). We
256+ // don't split PascalCase into words here — the substring match
257+ // below doesn't need exact equality, just a unique tail.
258+ Tail . to_ascii_lowercase ( ) . replace ( '_' , "." )
259+ }
260+
261261fn DetectAppDataPrefix ( ) -> Option < String > {
262- // Match the bundle identifier pattern used by Mountain
263- if let Ok ( Home ) = std:: env:: var ( "HOME" ) {
264- let Base = format ! ( "{}/Library/Application Support" , Home ) ;
265- if let Ok ( Entries ) = std:: fs:: read_dir ( & Base ) {
266- for Entry in Entries . flatten ( ) {
267- let Name = Entry . file_name ( ) ;
268- let Name = Name . to_string_lossy ( ) ;
269- if Name . starts_with ( "land.editor." ) && Name . contains ( "mountain" ) {
270- return Some ( format ! ( "{}/{}" , Base , Name ) ) ;
271- }
262+ let Home = std:: env:: var ( "HOME" ) . ok ( ) ?;
263+ let Base = format ! ( "{}/Library/Application Support" , Home ) ;
264+ let Signature = BinarySignature ( ) ;
265+
266+ // Prefer a directory whose name ends with this binary's unique tail
267+ // signature: that's the app-data directory Tauri created for THIS
268+ // profile. Without this check, a user who has ever launched another
269+ // profile (debug-electron, release-electron, …) will see DevLog
270+ // writing into that stale directory while userdata still goes into
271+ // the current one, producing an "empty logs folder" mystery.
272+ let mut FirstMatchingMountain : Option < String > = None ;
273+ if let Ok ( Entries ) = std:: fs:: read_dir ( & Base ) {
274+ for Entry in Entries . flatten ( ) {
275+ let Name = Entry . file_name ( ) ;
276+ let Name = Name . to_string_lossy ( ) . into_owned ( ) ;
277+ if !Name . starts_with ( "land.editor." ) || !Name . contains ( "mountain" ) {
278+ continue ;
279+ }
280+ // Strict match: binary signature tail is a suffix of the dir name.
281+ if Name . ends_with ( & Signature ) {
282+ return Some ( format ! ( "{}/{}" , Base , Name ) ) ;
283+ }
284+ // Lossy match: some segment of the binary signature is contained
285+ // in the dir name. Used only if no strict match exists.
286+ if FirstMatchingMountain . is_none ( ) {
287+ FirstMatchingMountain = Some ( format ! ( "{}/{}" , Base , Name ) ) ;
272288 }
273289 }
274290 }
275- None
291+ FirstMatchingMountain
276292}
277293
278294/// Get the app-data path prefix for aliasing (cached).
0 commit comments