| 1 | #nullable enable |
| 2 | using System.Diagnostics.CodeAnalysis; |
| 3 | using System.Text.Json; |
| 4 | using CascadeIDE.Contracts; |
| 5 | |
| 6 | namespace CascadeIDE.Features.Launch.DataAcquisition; |
| 7 | |
| 8 | /// <summary> |
| 9 | /// Чтение SDK <c>Properties/launchSettings.json</c> и сопоставление с <see cref="LaunchProfileModel"/> (только <c>commandName: Project</c>, Kestrel). |
| 10 | /// </summary> |
| 11 | [IoBoundary] |
| 12 | internal static class LaunchSettingsJsonImport |
| 13 | { |
| 14 | internal static bool TryReadProjectProfiles( |
| 15 | string json, |
| 16 | string projectPathRelativeToSolution, |
| 17 | [NotNullWhen(true)] out List<(string Name, LaunchProfileModel Model)>? profiles, |
| 18 | [NotNullWhen(false)] out string? error) |
| 19 | { |
| 20 | profiles = null; |
| 21 | error = null; |
| 22 | try |
| 23 | { |
| 24 | using var doc = JsonDocument.Parse(json); |
| 25 | if (!doc.RootElement.TryGetProperty("profiles", out var p) || p.ValueKind != JsonValueKind.Object) |
| 26 | { |
| 27 | error = "launch_settings_no_profiles"; |
| 28 | return false; |
| 29 | } |
| 30 | |
| 31 | var list = new List<(string, LaunchProfileModel)>(); |
| 32 | var proj = projectPathRelativeToSolution.Replace('/', Path.DirectorySeparatorChar); |
| 33 | foreach (var prop in p.EnumerateObject()) |
| 34 | { |
| 35 | if (TryMapProjectProfile(prop.Value, proj, out var m)) |
| 36 | list.Add((prop.Name, m)); |
| 37 | } |
| 38 | |
| 39 | if (list.Count == 0) |
| 40 | { |
| 41 | error = "launch_settings_no_project_profiles"; |
| 42 | return false; |
| 43 | } |
| 44 | |
| 45 | profiles = list; |
| 46 | return true; |
| 47 | } |
| 48 | catch (Exception ex) |
| 49 | { |
| 50 | error = "launch_settings_parse: " + ex.Message; |
| 51 | return false; |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | private static bool TryMapProjectProfile(JsonElement el, string projectRelative, [NotNullWhen(true)] out LaunchProfileModel? model) |
| 56 | { |
| 57 | model = null; |
| 58 | if (!IsKestrelProjectProfile(el)) |
| 59 | return false; |
| 60 | |
| 61 | var m = CreateBaseProfileModel(projectRelative); |
| 62 | ApplyCommandLineArgs(el, m); |
| 63 | ApplyApplicationUrl(el, m); |
| 64 | ApplyLaunchUrl(el, m); |
| 65 | ApplyLaunchBrowserFlag(el, m); |
| 66 | m.Env = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); |
| 67 | ApplyEnvironmentVariables(el, m); |
| 68 | ApplyKestrelBrowserHeuristic(m); |
| 69 | |
| 70 | model = m; |
| 71 | return true; |
| 72 | } |
| 73 | |
| 74 | private static bool IsKestrelProjectProfile(JsonElement el) |
| 75 | { |
| 76 | if (el.ValueKind != JsonValueKind.Object) |
| 77 | return false; |
| 78 | if (!el.TryGetProperty("commandName", out var cn) || cn.ValueKind != JsonValueKind.String) |
| 79 | return false; |
| 80 | return string.Equals(cn.GetString(), "Project", StringComparison.Ordinal); |
| 81 | } |
| 82 | |
| 83 | private static LaunchProfileModel CreateBaseProfileModel(string projectRelative) => |
| 84 | new() |
| 85 | { |
| 86 | Project = projectRelative, |
| 87 | Configuration = LaunchProfilesStore.DefaultConfiguration |
| 88 | }; |
| 89 | |
| 90 | // Семантика как у SDK: токенизация по пробелу без кавычек; аргументы с пробелами в кавычках не разбираем. |
| 91 | private static void ApplyCommandLineArgs(JsonElement el, LaunchProfileModel m) |
| 92 | { |
| 93 | if (!el.TryGetProperty("commandLineArgs", out var cla) || cla.ValueKind != JsonValueKind.String) |
| 94 | return; |
| 95 | var s = cla.GetString(); |
| 96 | if (string.IsNullOrWhiteSpace(s)) |
| 97 | return; |
| 98 | var t = s.Trim(); |
| 99 | if (t.Length == 0) |
| 100 | return; |
| 101 | m.ProgramArgs = t.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); |
| 102 | } |
| 103 | |
| 104 | private static void ApplyApplicationUrl(JsonElement el, LaunchProfileModel m) |
| 105 | { |
| 106 | if (!el.TryGetProperty("applicationUrl", out var au) || au.ValueKind != JsonValueKind.String) |
| 107 | return; |
| 108 | var u = au.GetString(); |
| 109 | if (!string.IsNullOrWhiteSpace(u)) |
| 110 | m.ApplicationUrls = u.Trim(); |
| 111 | } |
| 112 | |
| 113 | private static void ApplyLaunchUrl(JsonElement el, LaunchProfileModel m) |
| 114 | { |
| 115 | if (!el.TryGetProperty("launchUrl", out var lurl) || lurl.ValueKind != JsonValueKind.String) |
| 116 | return; |
| 117 | var s = lurl.GetString(); |
| 118 | if (!string.IsNullOrWhiteSpace(s)) |
| 119 | m.LaunchUrl = s.Trim(); |
| 120 | } |
| 121 | |
| 122 | private static void ApplyLaunchBrowserFlag(JsonElement el, LaunchProfileModel m) |
| 123 | { |
| 124 | if (!el.TryGetProperty("launchBrowser", out var lb) || |
| 125 | (lb.ValueKind != JsonValueKind.True && lb.ValueKind != JsonValueKind.False)) |
| 126 | return; |
| 127 | m.LaunchBrowser = lb.GetBoolean(); |
| 128 | } |
| 129 | |
| 130 | private static void ApplyEnvironmentVariables(JsonElement el, LaunchProfileModel m) |
| 131 | { |
| 132 | if (!el.TryGetProperty("environmentVariables", out var ev) || ev.ValueKind != JsonValueKind.Object) |
| 133 | return; |
| 134 | foreach (var v in ev.EnumerateObject()) |
| 135 | { |
| 136 | if (string.IsNullOrEmpty(v.Name)) |
| 137 | continue; |
| 138 | var val = v.Value.ValueKind == JsonValueKind.String |
| 139 | ? (v.Value.GetString() ?? "") |
| 140 | : v.Value.ToString(); |
| 141 | m.Env![v.Name] = val; |
| 142 | } |
| 143 | } |
| 144 | |
| 145 | private static void ApplyKestrelBrowserHeuristic(LaunchProfileModel m) |
| 146 | { |
| 147 | if (m.LaunchBrowser is not true && ApplicationUrlsSuggestKestrelListener(m.ApplicationUrls)) |
| 148 | m.LaunchBrowser = true; |
| 149 | } |
| 150 | |
| 151 | /// <summary>Любой сегмент в <c>applicationUrl</c> (через <c>;</c> как у ASPNETCORE_URLS) с префиксом http(s).</summary> |
| 152 | internal static bool ApplicationUrlsSuggestKestrelListener(string? applicationUrls) |
| 153 | { |
| 154 | if (string.IsNullOrWhiteSpace(applicationUrls)) |
| 155 | return false; |
| 156 | foreach (var part in applicationUrls.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) |
| 157 | { |
| 158 | if (part.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || |
| 159 | part.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) |
| 160 | return true; |
| 161 | } |
| 162 | |
| 163 | return false; |
| 164 | } |
| 165 | } |
| 166 | |