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