|
10 | 10 |
|
11 | 11 | namespace GeneralUpdate.Tools.Services; |
12 | 12 |
|
13 | | -/// <summary> |
14 | | -/// Orchestrates the full update simulation: server, client, upgrade, log collection. |
15 | | -/// </summary> |
16 | 13 | public class SimulationService |
17 | 14 | { |
18 | 15 | private readonly ClientGeneratorService _generator = new(); |
19 | 16 | private readonly LocalUpdateServer _server = new(); |
20 | | - private readonly StringBuilder _fullLog = new(); |
21 | 17 | private int _timeoutSeconds = 120; |
22 | 18 |
|
23 | | - public IReadOnlyList<string> LogLines => _fullLog.ToString().Split('\n').ToList(); |
| 19 | + public string ServerBaseUrl => _server.BaseUrl; |
24 | 20 |
|
25 | | - public async Task<SimulationResult> RunAsync( |
26 | | - SimulateConfigModel config, |
27 | | - IProgress<string>? progress = null, |
28 | | - CancellationToken ct = default) |
| 21 | + /// <summary> |
| 22 | + /// Generate scripts and start the local update server. Server stays running until StopServerAsync is called. |
| 23 | + /// </summary> |
| 24 | + public async Task StartServerAsync(SimulateConfigModel config, IProgress<string>? progress = null) |
| 25 | + { |
| 26 | + Log("STEP 1: Validating inputs", progress); |
| 27 | + Validate(config); |
| 28 | + |
| 29 | + Log("STEP 2: Generating client.csx and upgrade.csx", progress); |
| 30 | + await _generator.GenerateAsync(config, config.OutputDirectory); |
| 31 | + Log($" client.csx → {config.OutputDirectory}", progress); |
| 32 | + Log($" upgrade.csx → {config.OutputDirectory}", progress); |
| 33 | + |
| 34 | + Log("STEP 3: Starting local server", progress); |
| 35 | + var serverPatchDir = Path.Combine(config.OutputDirectory, ".server"); |
| 36 | + Directory.CreateDirectory(serverPatchDir); |
| 37 | + var patchName = Path.GetFileName(config.PatchFilePath); |
| 38 | + var patchDest = Path.Combine(serverPatchDir, patchName); |
| 39 | + File.Copy(config.PatchFilePath, patchDest, true); |
| 40 | + |
| 41 | + var hash = ComputeQuickHash(patchDest); |
| 42 | + LocalUpdateServerFiles.Register(patchName, patchDest); |
| 43 | + _server.Updates.Add((config.CurrentVersion, config.TargetVersion, hash, patchDest, config.AppType)); |
| 44 | + |
| 45 | + await _server.StartAsync(config.ServerPort); |
| 46 | + config.ServerPort = _server.Port; |
| 47 | + config.ServerRunning = true; |
| 48 | + Log($" Server running on {_server.BaseUrl}", progress); |
| 49 | + } |
| 50 | + |
| 51 | + /// <summary> |
| 52 | + /// Stop the local update server. |
| 53 | + /// </summary> |
| 54 | + public async Task StopServerAsync() |
| 55 | + { |
| 56 | + await _server.DisposeAsync(); |
| 57 | + LocalUpdateServerFiles.Clear(); |
| 58 | + } |
| 59 | + |
| 60 | + /// <summary> |
| 61 | + /// Run the client script and return results. Server must already be running. |
| 62 | + /// </summary> |
| 63 | + public async Task<SimulationResult> RunClientAsync(SimulateConfigModel config, IProgress<string>? progress = null, CancellationToken ct = default) |
29 | 64 | { |
30 | 65 | var result = new SimulationResult(); |
31 | 66 | var sw = Stopwatch.StartNew(); |
32 | 67 |
|
33 | 68 | try |
34 | 69 | { |
35 | | - // 1. Validate |
36 | | - Log("STEP 1: Validating inputs", progress); |
37 | | - Validate(config); |
38 | | - |
39 | | - // 2. Prepare output directory |
40 | | - Log($"STEP 2: Preparing {config.OutputDirectory}", progress); |
41 | | - Directory.CreateDirectory(config.OutputDirectory); |
42 | | - |
43 | | - // 3. Generate scripts first (always, even in manual mode) |
44 | | - Log("STEP 3: Generating client.csx and upgrade.csx", progress); |
45 | | - await _generator.GenerateAsync(config, config.OutputDirectory); |
46 | | - Log($" client.csx → {config.OutputDirectory}", progress); |
47 | | - Log($" upgrade.csx → {config.OutputDirectory}", progress); |
48 | | - |
49 | | - // Manual mode: skip server + client run |
50 | | - if (!config.AutoRun) |
51 | | - { |
52 | | - Log(" Auto-run disabled — scripts ready for manual execution", progress); |
53 | | - Log($" cd {config.OutputDirectory}", progress); |
54 | | - Log(" dotnet script client.csx", progress); |
55 | | - result.Success = true; |
56 | | - result.Notes.Add("Manual mode: scripts generated, not executed"); |
57 | | - return result; |
58 | | - } |
59 | | - |
60 | | - // 4. Set up server + copy patch |
61 | | - Log("STEP 4: Setting up local server", progress); |
62 | | - var serverPatchDir = Path.Combine(config.OutputDirectory, ".server"); |
63 | | - Directory.CreateDirectory(serverPatchDir); |
64 | | - var patchName = Path.GetFileName(config.PatchFilePath); |
65 | | - var patchDest = Path.Combine(serverPatchDir, patchName); |
66 | | - File.Copy(config.PatchFilePath, patchDest, true); |
67 | | - |
68 | | - var hash = ComputeQuickHash(patchDest); |
69 | | - LocalUpdateServerFiles.Register(patchName, patchDest); |
70 | | - _server.Updates.Add((config.CurrentVersion, config.TargetVersion, hash, patchDest, config.AppType)); |
71 | | - |
72 | | - await _server.StartAsync(config.ServerPort); |
73 | | - Log($" Server running on {_server.BaseUrl}", progress); |
74 | | - config.ServerPort = _server.Port; |
75 | | - |
76 | | - // 5. Run client |
77 | | - Log("STEP 5: Running client (dotnet script client.csx)", progress); |
| 70 | + Log("Running client (dotnet script client.csx)", progress); |
78 | 71 | var clientResult = await RunDotNetScript(config.OutputDirectory, "client.csx", ct); |
79 | 72 | Log(clientResult.Output, progress); |
80 | 73 |
|
81 | 74 | if (!clientResult.Success) |
82 | 75 | { |
83 | | - Log(" Client failed - see output above", progress); |
84 | 76 | result.Success = false; |
85 | 77 | result.ErrorMessage = "Client exited with error"; |
86 | 78 | return result; |
87 | 79 | } |
88 | 80 |
|
89 | | - Log(" Client completed successfully", progress); |
| 81 | + Log("Client completed", progress); |
| 82 | + await Task.Delay(2000, ct); |
90 | 83 |
|
91 | | - // 6. Verify the patch was applied |
92 | | - Log("STEP 6: Verifying update result", progress); |
93 | | - await Task.Delay(2000, ct); // Give upgrade process time to complete |
94 | | - VerifyUpdateResult(config, result); |
| 84 | + var fileCount = Directory.GetFiles(config.AppDirectory, "*", SearchOption.AllDirectories).Length; |
| 85 | + result.Notes.Add($"Files in app directory after update: {fileCount}"); |
95 | 86 |
|
96 | 87 | result.Success = true; |
97 | 88 | result.Elapsed = sw.Elapsed; |
98 | | - Log($"✅ Simulation complete ({sw.Elapsed.TotalSeconds:F1}s)", progress); |
| 89 | + Log($"Simulation complete ({sw.Elapsed.TotalSeconds:F1}s)", progress); |
99 | 90 | } |
100 | 91 | catch (Exception ex) |
101 | 92 | { |
102 | 93 | result.Success = false; |
103 | 94 | result.ErrorMessage = ex.Message; |
104 | | - Log($"❌ Simulation failed: {ex.Message}", progress); |
105 | | - } |
106 | | - finally |
107 | | - { |
108 | | - try |
109 | | - { |
110 | | - await _server.DisposeAsync(); |
111 | | - LocalUpdateServerFiles.Clear(); |
112 | | - } |
113 | | - catch { } |
114 | | - |
115 | | - result.FullLog = _fullLog.ToString(); |
| 95 | + Log($"Simulation failed: {ex.Message}", progress); |
116 | 96 | } |
117 | 97 |
|
| 98 | + result.FullLog = ""; |
118 | 99 | return result; |
119 | 100 | } |
120 | 101 |
|
121 | 102 | private void Validate(SimulateConfigModel config) |
122 | 103 | { |
123 | 104 | if (!Directory.Exists(config.AppDirectory)) |
124 | 105 | throw new DirectoryNotFoundException($"App directory not found: {config.AppDirectory}"); |
125 | | - |
126 | 106 | if (!File.Exists(config.PatchFilePath)) |
127 | 107 | throw new FileNotFoundException($"Patch file not found: {config.PatchFilePath}"); |
128 | | - |
129 | 108 | if (string.IsNullOrWhiteSpace(config.OutputDirectory)) |
130 | 109 | throw new ArgumentException("Output directory is required"); |
131 | | - |
132 | | - // Check dotnet |
133 | 110 | try |
134 | 111 | { |
135 | | - var psi = new ProcessStartInfo("dotnet", "--version") |
136 | | - { |
137 | | - RedirectStandardOutput = true, |
138 | | - UseShellExecute = false, |
139 | | - CreateNoWindow = true |
140 | | - }; |
141 | | - using var p = Process.Start(psi); |
142 | | - p?.WaitForExit(5000); |
| 112 | + var psi = new ProcessStartInfo("dotnet", "--version") { RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; |
| 113 | + using var p = Process.Start(psi); p?.WaitForExit(5000); |
143 | 114 | var ver = p?.StandardOutput.ReadToEnd().Trim(); |
144 | 115 | if (string.IsNullOrEmpty(ver) || !ver.StartsWith("10.") && !ver.StartsWith("11.")) |
145 | | - throw new InvalidOperationException(".NET 10.0 SDK is required. Install from https://dotnet.microsoft.com/"); |
| 116 | + throw new InvalidOperationException(".NET 10.0 SDK required"); |
146 | 117 | } |
147 | 118 | catch (InvalidOperationException) { throw; } |
148 | | - catch { throw new InvalidOperationException("dotnet CLI not found. Install .NET 10.0 SDK."); } |
| 119 | + catch { throw new InvalidOperationException("dotnet CLI not found"); } |
149 | 120 | } |
150 | 121 |
|
151 | 122 | private async Task<(bool Success, string Output)> RunDotNetScript(string workDir, string script, CancellationToken ct) |
152 | 123 | { |
153 | 124 | var psi = new ProcessStartInfo("dotnet", $"script {script}") |
154 | 125 | { |
155 | 126 | WorkingDirectory = workDir, |
156 | | - RedirectStandardOutput = true, |
157 | | - RedirectStandardError = true, |
158 | | - StandardOutputEncoding = System.Text.Encoding.UTF8, |
159 | | - StandardErrorEncoding = System.Text.Encoding.UTF8, |
160 | | - UseShellExecute = false, |
161 | | - CreateNoWindow = true |
| 127 | + RedirectStandardOutput = true, RedirectStandardError = true, |
| 128 | + StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8, |
| 129 | + UseShellExecute = false, CreateNoWindow = true |
162 | 130 | }; |
163 | | - |
164 | 131 | using var p = Process.Start(psi)!; |
165 | 132 | var output = new StringBuilder(); |
166 | | - |
167 | | - var readTask = Task.Run(async () => |
168 | | - { |
169 | | - while (!p.StandardOutput.EndOfStream) |
170 | | - output.AppendLine(await p.StandardOutput.ReadLineAsync(ct)); |
171 | | - }, ct); |
172 | | - |
173 | | - var errorTask = Task.Run(async () => |
174 | | - { |
175 | | - while (!p.StandardError.EndOfStream) |
176 | | - output.AppendLine(await p.StandardError.ReadLineAsync(ct)); |
177 | | - }, ct); |
178 | | - |
179 | | - // Wait with timeout |
| 133 | + var readTask = Task.Run(async () => { while (!p.StandardOutput.EndOfStream) output.AppendLine(await p.StandardOutput.ReadLineAsync(ct)); }, ct); |
| 134 | + var errorTask = Task.Run(async () => { while (!p.StandardError.EndOfStream) output.AppendLine(await p.StandardError.ReadLineAsync(ct)); }, ct); |
180 | 135 | var completed = p.WaitForExit(_timeoutSeconds * 1000); |
181 | | - if (!completed) |
182 | | - { |
183 | | - p.Kill(true); |
184 | | - return (false, output + "\n[TIMEOUT] Simulation exceeded time limit"); |
185 | | - } |
186 | | - |
187 | | - // Also check output for error indicators (GeneralUpdate catches exceptions internally) |
| 136 | + if (!completed) { p.Kill(true); return (false, output + "\n[TIMEOUT]"); } |
188 | 137 | await Task.WhenAll(readTask, errorTask); |
189 | 138 | var outputStr = output.ToString(); |
190 | 139 | var hasError = outputStr.Contains("ERROR:") || outputStr.Contains("FATAL:") || outputStr.Contains("JsonException"); |
191 | | - |
192 | 140 | return (!hasError && p.ExitCode == 0, outputStr); |
193 | 141 | } |
194 | 142 |
|
195 | | - private void VerifyUpdateResult(SimulateConfigModel config, SimulationResult result) |
196 | | - { |
197 | | - // Check if delete_files.json was consumed (it should be gone or applied) |
198 | | - var deleteFile = Path.Combine(config.AppDirectory, "delete_files.json"); |
199 | | - if (File.Exists(deleteFile)) |
200 | | - { |
201 | | - result.Notes.Add("delete_files.json still present - HandleDeleteList may not have run"); |
202 | | - } |
203 | | - |
204 | | - // Count files changed in app directory |
205 | | - var fileCount = Directory.GetFiles(config.AppDirectory, "*", SearchOption.AllDirectories).Length; |
206 | | - result.Notes.Add($"Files in app directory after update: {fileCount}"); |
207 | | - } |
208 | | - |
209 | 143 | private static string ComputeQuickHash(string filePath) |
210 | 144 | { |
211 | 145 | using var sha = System.Security.Cryptography.SHA256.Create(); |
212 | 146 | using var fs = File.OpenRead(filePath); |
213 | 147 | return BitConverter.ToString(sha.ComputeHash(fs)).Replace("-", "").ToLowerInvariant(); |
214 | 148 | } |
215 | 149 |
|
216 | | - private void Log(string msg, IProgress<string>? progress) |
217 | | - { |
218 | | - var line = $"[{DateTime.Now:HH:mm:ss}] {msg}"; |
219 | | - _fullLog.AppendLine(line); |
220 | | - progress?.Report(line); |
221 | | - } |
| 150 | + private static void Log(string msg, IProgress<string>? progress) => progress?.Report($"[{DateTime.Now:HH:mm:ss}] {msg}"); |
222 | 151 | } |
223 | 152 |
|
224 | 153 | public class SimulationResult |
|
0 commit comments