using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; using SbLauncher; // Real first-run/import/save/restart/update paths using synthetic credentials. public static class ProfileUpdates { [STAThread] public static int Main() { string root = Path.Combine(Path.GetTempPath(), "SbLauncherUpdates-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); var app = new Application { ShutdownMode = ShutdownMode.OnExplicitShutdown }; MainVm vm = null; try { var a = Shared("a", "AA", "127.0.0.1", "00000000-0000-4000-8000-000000000001"); var b = Shared("b", "BB", "127.0.0.1", "00000000-0000-4000-8000-000000000002"); WriteBase(root, "198.51.100.10", a, b); Paths paths = new Paths(root); vm = new MainVm(); vm.Load(paths); Require(vm.Prof.Servers.Count == 2 && !File.Exists(paths.Profiles), "Fresh install loads shared servers without personal profiles"); vm.PrivateKey = Convert.ToBase64String(Enumerable.Range(1, 32).Select(delegate(int n) { return (byte)n; }).ToArray()); vm.LocalAddress = "10.10.0.2/24"; vm.Adapter = "Personal adapter"; Require(Wait(vm.AddFromLinkAsync("trojan://personal-secret@127.0.0.2:443?sni=example.com#Personal")), "UI import adds a personal server"); var own = vm.Prof.Servers.Single(delegate(ServerCfg server) { return server.BaseId.Length == 0; }); string ownCode = own.Code; vm.Selected = vm.Servers.Single(delegate(ServerVm card) { return card.Cfg != null && card.Cfg.BaseId == "b"; }); vm.FlushLocal(); string localBefore = File.ReadAllText(paths.Local); string personalBefore = File.ReadAllText(paths.Profiles); Require(!personalBefore.Contains("00000000-0000-4000-8000-000000000001") && !personalBefore.Contains("00000000-0000-4000-8000-000000000002"), "Saving personal servers excludes all shared credentials"); vm.DisposeTimers(); vm = null; Profiles removed = Profiles.Load(paths.Profiles); removed.Remove(removed.Servers.Single(delegate(ServerCfg server) { return server.BaseId == "a"; })); removed.Save(paths.Profiles); string personalHidden = File.ReadAllText(paths.Profiles); // Simulate replacement of files shipped by the next release. var changedA = Shared("a", "AA", "127.0.0.3", "00000000-0000-4000-8000-000000000003"); var changedB = Shared("b", "BB", "127.0.0.4", "00000000-0000-4000-8000-000000000004"); var c = Shared("c", ownCode, "127.0.0.5", "00000000-0000-4000-8000-000000000005"); WriteBase(root, "198.51.100.20", changedA, changedB, c); Require(File.ReadAllText(paths.Local) == localBefore && File.ReadAllText(paths.Profiles) == personalHidden, "Replacing the release catalog does not write either personal file"); vm = new MainVm(); vm.Load(paths); Require(vm.Prof.Servers.Count == 3 && vm.Prof.Wg.Server == "198.51.100.20", "Updated shared catalog and WireGuard endpoint load"); Require(vm.Prof.Servers.All(delegate(ServerCfg server) { return server.BaseId != "a"; }), "Hidden shared server stays hidden after its credentials change"); Require(vm.Prof.Servers.Single(delegate(ServerCfg server) { return server.BaseId == "b"; }).Server == "127.0.0.4", "Existing shared server receives updated configuration"); Require(vm.Prof.Servers.Single(delegate(ServerCfg server) { return server.BaseId.Length == 0; }).Code == ownCode, "Personal server survives a shared code collision"); Require(vm.Prof.Servers.Select(delegate(ServerCfg server) { return server.Code; }).Distinct(StringComparer.OrdinalIgnoreCase).Count() == 3, "Merged server codes are unique"); Require(vm.Selected.Cfg.BaseId == "b", "Shared selection is restored by stable id"); vm.FlushLocal(); Require(File.ReadAllText(paths.Local) == localBefore, "WireGuard key, address, adapter and selection survive restart after update"); vm.DisposeTimers(); vm = null; // Upgrade a v0.1.1 full list with one personal server and settings. string legacyRoot = Path.Combine(root, "legacy"); Directory.CreateDirectory(legacyRoot); WriteBase(legacyRoot, "198.51.100.10", a, b); Profiles legacy = new Profiles(); legacy.Wg.Server = "198.51.100.10"; legacy.Wg.PeerPublicKey = "shared-public-key"; legacy.Servers.Add(new ServerCfg { Code = "AA", Server = "127.0.0.1", Uuid = "00000000-0000-4000-8000-000000000001" }); legacy.Servers.Add(new ServerCfg { Code = "BB", Server = "127.0.0.1", Uuid = "00000000-0000-4000-8000-000000000002" }); legacy.Servers.Add(own); string legacyPath = Path.Combine(legacyRoot, "profiles.json"); legacy.Save(legacyPath); File.WriteAllText(Path.Combine(legacyRoot, "local.json"), localBefore); string legacyBefore = File.ReadAllText(legacyPath); vm = new MainVm(); vm.Load(new Paths(legacyRoot)); Require(vm.Prof.Servers.Count == 3 && !vm.Prof.NeedsMigration, "Legacy list migrates without losing personal servers or duplicating shared servers"); Require(File.ReadAllText(Path.Combine(legacyRoot, "profiles.legacy-backup.json")) == legacyBefore, "Migration preserves a complete legacy backup"); Require(!File.ReadAllText(legacyPath).Contains("00000000-0000-4000-8000-000000000001"), "Migration stores only personal servers"); vm.DisposeTimers(); vm = null; WriteBase(legacyRoot, "198.51.100.20", changedB); Profiles upgraded = Profiles.Load(legacyPath); Require(upgraded.Servers.Count == 2 && upgraded.Wg.Server == "198.51.100.20", "Migrated installations receive later shared updates and removals"); Require(File.ReadAllText(Path.Combine(legacyRoot, "local.json")) == localBefore, "Legacy upgrade preserves the personal WireGuard settings"); string customRoot = Path.Combine(root, "custom"); Directory.CreateDirectory(customRoot); WriteBase(customRoot, "198.51.100.10", a); Profiles custom = new Profiles(); custom.Servers.Add(new ServerCfg { Code = "AA", Server = "127.0.0.1", Uuid = "00000000-0000-4000-8000-000000000001", Sni = "custom.example.com" }); custom.Save(Path.Combine(customRoot, "profiles.json")); Require(Profiles.Load(Path.Combine(customRoot, "profiles.json")).Servers.Count == 2, "Legacy custom connection settings are preserved even with a matching endpoint and UUID"); upgraded.Remove(upgraded.Servers.Single(delegate(ServerCfg server) { return server.BaseId.Length == 0; })); upgraded.Save(legacyPath); Require(Profiles.Load(legacyPath).Servers.Count == 1, "Deleted personal server does not return on restart"); Console.WriteLine("Profile update checks passed."); return 0; } catch (Exception ex) { Console.Error.WriteLine(ex); return 1; } finally { if (vm != null) vm.DisposeTimers(); app.Shutdown(); string full = Path.GetFullPath(root); string temp = Path.GetFullPath(Path.GetTempPath()).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; if (full.StartsWith(temp, StringComparison.OrdinalIgnoreCase)) Directory.Delete(full, true); } } static Dictionary Shared(string id, string code, string host, string uuid) { return new Dictionary { { "id", id }, { "code", code }, { "name", code }, { "server", host }, { "port", 443 }, { "uuid", uuid }, { "protocol", "vless" }, { "flow", "" } }; } static void WriteBase(string root, string wgHost, params Dictionary[] servers) { J.Write(Path.Combine(root, "profiles.base.json"), J.ToJson(new Dictionary { { "wireguard", new Dictionary { { "server", wgHost }, { "peer_public_key", "shared-public-key" } } }, { "servers", servers } })); } static bool Wait(Task task) { var frame = new DispatcherFrame(); var started = DateTime.UtcNow; var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(20) }; timer.Tick += delegate { if (task.IsCompleted || (DateTime.UtcNow - started).TotalSeconds > 10) { timer.Stop(); frame.Continue = false; } }; timer.Start(); Dispatcher.PushFrame(frame); return task.IsCompleted && task.Result; } static void Require(bool condition, string message) { if (!condition) throw new InvalidOperationException(message); Console.WriteLine("PASS " + message); } }