Skip to content

Commit 9233719

Browse files
authored
feat: add SimulationService and wire up SimulateViewModel (#35)
- SimulationService orchestrates full simulation flow: validate → copy patch → start server → generate scripts → run client → verify - Checks dotnet SDK version before starting - Runs client.cs via dotnet run, captures stdout/stderr with timeout - Verifies update result (file count, delete_files.json consumption) - SimulateViewModel now calls SimulationService on Start
1 parent 6d7a1d8 commit 9233719

2 files changed

Lines changed: 254 additions & 5 deletions

File tree

src/Services/SimulationService.cs

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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+
}

src/ViewModels/SimulateViewModel.cs

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.ObjectModel;
3+
using System.IO;
34
using System.Threading.Tasks;
45
using CommunityToolkit.Mvvm.ComponentModel;
56
using CommunityToolkit.Mvvm.Input;
@@ -11,6 +12,7 @@ namespace GeneralUpdate.Tools.ViewModels;
1112
public partial class SimulateViewModel : ViewModelBase
1213
{
1314
private readonly LocalizationService _loc = LocalizationService.Instance;
15+
private readonly SimulationService _sim = new();
1416

1517
public SimulateConfigModel Config { get; } = new();
1618

@@ -62,11 +64,44 @@ public SimulateViewModel()
6264
[RelayCommand]
6365
async Task StartSimulation()
6466
{
65-
// Validation will be implemented in issue #4
66-
if (string.IsNullOrWhiteSpace(Config.AppDirectory)) { Status = "请选择旧版本应用目录"; return; }
67-
if (string.IsNullOrWhiteSpace(Config.PatchFilePath)) { Status = "请选择补丁包文件"; return; }
68-
if (string.IsNullOrWhiteSpace(Config.OutputDirectory)) { Status = "请选择模拟输出目录"; return; }
69-
Status = "模拟功能将在后续 issue 中实现";
67+
if (string.IsNullOrWhiteSpace(Config.AppDirectory)) { Status = _loc["Sim.ValidateDirs"]; return; }
68+
if (string.IsNullOrWhiteSpace(Config.PatchFilePath)) { Status = _loc["Sim.ValidateDirs"]; return; }
69+
if (string.IsNullOrWhiteSpace(Config.OutputDirectory)) { Status = _loc["Sim.ValidateDirs"]; return; }
70+
71+
IsRunning = true;
72+
Log.Clear();
73+
Status = "Starting simulation...";
74+
75+
try
76+
{
77+
var progress = new Progress<string>(L);
78+
var result = await _sim.RunAsync(Config, progress);
79+
80+
if (result.Success)
81+
{
82+
Status = $"Simulation completed ({result.Elapsed.TotalSeconds:F1}s)";
83+
L($"Result: {(result.Success ? "PASS" : "FAIL")}");
84+
foreach (var note in result.Notes)
85+
L($" Note: {note}");
86+
87+
// Generate report
88+
var reportPath = Path.Combine(Config.OutputDirectory, "simulation_report.md");
89+
// report generation will be in next PR
90+
}
91+
else
92+
{
93+
Status = $"Simulation failed: {result.ErrorMessage}";
94+
}
95+
}
96+
catch (Exception ex)
97+
{
98+
Status = $"Error: {ex.Message}";
99+
L($"FATAL: {ex}");
100+
}
101+
finally
102+
{
103+
IsRunning = false;
104+
}
70105
}
71106

72107
void L(string msg) => Log.Add($"[{DateTime.Now:HH:mm:ss}] {msg}");

0 commit comments

Comments
 (0)