using System; using System.IO; namespace ytLive.Helpers; /// /// Which run of LlamaCasty this process is, and where its private state lives. /// Dev-only affordance (creator directive, 2026-09-26). Pre-1.0 the creator /// needs two instances side by side: one streaming, one screen-capturing the other. /// Two instances on one Windows profile collide on state, and two of those collisions /// are hard failures rather than annoyances: /// /// The SQLite layout DB. The app's model is read-whole-scene / write-whole-scene, /// so two instances saving different layouts clobber each other. /// The WebView2 user data folder. Chromium takes an exclusive lock on it; the /// second instance of the same exe simply fails to start. /// The auth token store. A test instance would overwrite the real YouTube /// sign-in. /// startup.log. Two appenders interleave into one file, so a crash in either /// instance is ambiguous. /// /// So in DEBUG a process can claim an identity and gets its own private root /// under instances\<id>: $env:YTLIVE_INSTANCE=2; dotnet run. /// Recording, screen capture and the ffmpeg tools\ cache are deliberately LEFT /// shared — the cache should be shared, and the creator chooses the record folder. /// Global hotkeys are also left alone: if both instances register the same one, Windows /// refuses the second, which is the correct answer. /// At 1.0 this disappears. The whole implementation is inside /// #if DEBUG; a Release build compiles to the two pass-throughs at the bottom — /// no environment variable, no alternate folder, not even the variable's NAME in the /// binary. Call sites are unconditional, so Release cannot accidentally diverge: it /// always resolves the one real user profile. /// public static class InstanceProfile { #if DEBUG /// Environment variable naming this instance. Absent or blank = the primary /// instance on the real user profile. public const string InstanceVariable = "YTLIVE_INSTANCE"; /// This instance's id, or null for the primary instance. public static string? Id => Sanitize(Environment.GetEnvironmentVariable(InstanceVariable)); /// Private state root: the real profile normally, a per-instance subfolder /// when an id is claimed. public static string DataRoot => Id is { } id ? Path.Combine(DefaultRoot, "instances", id) : DefaultRoot; /// Chromium's user data folder, or null to let WebView2 pick its default /// (which is what Release always does). Must be per-instance or the second /// instance dies on Chromium's exclusive lock. public static string? WebViewDataFolder => Id is null ? null : Path.Combine(DataRoot, "webview2"); /// Reject anything that is not a plain path segment, so the variable can /// never be used to walk out of the profile directory or name a UNC path. private static string? Sanitize(string? raw) { if (string.IsNullOrWhiteSpace(raw)) return null; var trimmed = raw.Trim(); foreach (var c in trimmed) if (!char.IsLetterOrDigit(c) && c != '-' && c != '_') return null; return trimmed; } #else // Release: one profile, no alternate paths, no environment variable. Multi-instance // is a development affordance, not a shipping feature. public static string DataRoot => DefaultRoot; public static string? WebViewDataFolder => null; #endif /// The one real user profile: %APPDATA%\ytLlive. Note the historical /// double-L — do not "fix" it, users already have their layouts there. public static string DefaultRoot => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ytLlive"); }