Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions src/Services/SimulationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using GeneralUpdate.Tools.Models;

namespace GeneralUpdate.Tools.Services;

/// <summary>
/// Orchestrates the full update simulation: server, client, upgrade, log collection.
/// </summary>
public class SimulationService
{
private readonly ClientGeneratorService _generator = new();
private readonly LocalUpdateServer _server = new();
private readonly StringBuilder _fullLog = new();
private int _timeoutSeconds = 60;

public IReadOnlyList<string> LogLines => _fullLog.ToString().Split('\n').ToList();

public async Task<SimulationResult> RunAsync(
SimulateConfigModel config,
IProgress<string>? progress = null,
CancellationToken ct = default)
{
var result = new SimulationResult();
var sw = Stopwatch.StartNew();

try
{
// 1. Validate
Log("STEP 1: Validating inputs", progress);
Validate(config);

// 2. Prepare output directory
Log($"STEP 2: Preparing {config.OutputDirectory}", progress);
Directory.CreateDirectory(config.OutputDirectory);

// 3. Copy patch to server working dir
Log("STEP 3: Setting up local server", progress);
var serverPatchDir = Path.Combine(config.OutputDirectory, ".server");
Directory.CreateDirectory(serverPatchDir);
var patchName = Path.GetFileName(config.PatchFilePath);
var patchDest = Path.Combine(serverPatchDir, patchName);
File.Copy(config.PatchFilePath, patchDest, true);

var hash = ComputeQuickHash(patchDest);
LocalUpdateServerFiles.Register(patchName, patchDest);
_server.Updates.Add((config.CurrentVersion, config.TargetVersion, hash, patchDest, config.AppType));

await _server.StartAsync(config.ServerPort);
Log($" Server running on {_server.BaseUrl}", progress);
config.ServerPort = _server.Port;

// 4. Generate client/upgrade scripts
Log("STEP 4: Generating client.cs and upgrade.cs", progress);
await _generator.GenerateAsync(config, config.OutputDirectory);
Log($" client.cs → {config.OutputDirectory}", progress);
Log($" upgrade.cs → {config.OutputDirectory}", progress);

// 5. Run client
Log("STEP 5: Running client (dotnet run client.cs)", progress);
var clientResult = await RunDotNetScript(config.OutputDirectory, "client.cs", ct);
Log(clientResult.Output, progress);

if (!clientResult.Success)
{
Log(" Client failed - see output above", progress);
result.Success = false;
result.ErrorMessage = "Client exited with error";
return result;
}

Log(" Client completed successfully", progress);

// 6. Verify the patch was applied
Log("STEP 6: Verifying update result", progress);
await Task.Delay(2000, ct); // Give upgrade process time to complete
VerifyUpdateResult(config, result);

result.Success = true;
result.Elapsed = sw.Elapsed;
Log($"✅ Simulation complete ({sw.Elapsed.TotalSeconds:F1}s)", progress);
}
catch (Exception ex)
{
result.Success = false;
result.ErrorMessage = ex.Message;
Log($"❌ Simulation failed: {ex.Message}", progress);
}
finally
{
try
{
await _server.DisposeAsync();
LocalUpdateServerFiles.Clear();
}
catch { }

result.FullLog = _fullLog.ToString();
}

return result;
}

private void Validate(SimulateConfigModel config)
{
if (!Directory.Exists(config.AppDirectory))
throw new DirectoryNotFoundException($"App directory not found: {config.AppDirectory}");

if (!File.Exists(config.PatchFilePath))
throw new FileNotFoundException($"Patch file not found: {config.PatchFilePath}");

if (string.IsNullOrWhiteSpace(config.OutputDirectory))
throw new ArgumentException("Output directory is required");

// Check dotnet
try
{
var psi = new ProcessStartInfo("dotnet", "--version")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var p = Process.Start(psi);
p?.WaitForExit(5000);
var ver = p?.StandardOutput.ReadToEnd().Trim();
if (string.IsNullOrEmpty(ver) || !ver.StartsWith("10.") && !ver.StartsWith("11."))
throw new InvalidOperationException(".NET 10.0 SDK is required. Install from https://dotnet.microsoft.com/");
}
catch (InvalidOperationException) { throw; }
catch { throw new InvalidOperationException("dotnet CLI not found. Install .NET 10.0 SDK."); }
}

private async Task<(bool Success, string Output)> RunDotNetScript(string workDir, string script, CancellationToken ct)
{
var psi = new ProcessStartInfo("dotnet", $"run {script}")
{
WorkingDirectory = workDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};

using var p = Process.Start(psi)!;
var output = new StringBuilder();

var readTask = Task.Run(async () =>
{
while (!p.StandardOutput.EndOfStream)
output.AppendLine(await p.StandardOutput.ReadLineAsync(ct));
}, ct);

var errorTask = Task.Run(async () =>
{
while (!p.StandardError.EndOfStream)
output.AppendLine(await p.StandardError.ReadLineAsync(ct));
}, ct);

// Wait with timeout
var completed = p.WaitForExit(_timeoutSeconds * 1000);
if (!completed)
{
p.Kill(true);
return (false, output + "\n[TIMEOUT] Simulation exceeded time limit");
}

await Task.WhenAll(readTask, errorTask);
return (p.ExitCode == 0, output.ToString());
}

private void VerifyUpdateResult(SimulateConfigModel config, SimulationResult result)
{
// Check if delete_files.json was consumed (it should be gone or applied)
var deleteFile = Path.Combine(config.AppDirectory, "delete_files.json");
if (File.Exists(deleteFile))
{
result.Notes.Add("delete_files.json still present - HandleDeleteList may not have run");
}

// Count files changed in app directory
var fileCount = Directory.GetFiles(config.AppDirectory, "*", SearchOption.AllDirectories).Length;
result.Notes.Add($"Files in app directory after update: {fileCount}");
}

private static string ComputeQuickHash(string filePath)
{
using var sha = System.Security.Cryptography.SHA256.Create();
using var fs = File.OpenRead(filePath);
return BitConverter.ToString(sha.ComputeHash(fs)).Replace("-", "").ToLowerInvariant();
}

private void Log(string msg, IProgress<string>? progress)
{
var line = $"[{DateTime.Now:HH:mm:ss}] {msg}";
_fullLog.AppendLine(line);
progress?.Report(line);
}
}

public class SimulationResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public TimeSpan Elapsed { get; set; }
public string FullLog { get; set; } = "";
public List<string> Notes { get; } = new();
}
45 changes: 40 additions & 5 deletions src/ViewModels/SimulateViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
Expand All @@ -11,6 +12,7 @@ namespace GeneralUpdate.Tools.ViewModels;
public partial class SimulateViewModel : ViewModelBase
{
private readonly LocalizationService _loc = LocalizationService.Instance;
private readonly SimulationService _sim = new();

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

Expand Down Expand Up @@ -62,11 +64,44 @@ public SimulateViewModel()
[RelayCommand]
async Task StartSimulation()
{
// Validation will be implemented in issue #4
if (string.IsNullOrWhiteSpace(Config.AppDirectory)) { Status = "请选择旧版本应用目录"; return; }
if (string.IsNullOrWhiteSpace(Config.PatchFilePath)) { Status = "请选择补丁包文件"; return; }
if (string.IsNullOrWhiteSpace(Config.OutputDirectory)) { Status = "请选择模拟输出目录"; return; }
Status = "模拟功能将在后续 issue 中实现";
if (string.IsNullOrWhiteSpace(Config.AppDirectory)) { Status = _loc["Sim.ValidateDirs"]; return; }
if (string.IsNullOrWhiteSpace(Config.PatchFilePath)) { Status = _loc["Sim.ValidateDirs"]; return; }
if (string.IsNullOrWhiteSpace(Config.OutputDirectory)) { Status = _loc["Sim.ValidateDirs"]; return; }

IsRunning = true;
Log.Clear();
Status = "Starting simulation...";

try
{
var progress = new Progress<string>(L);
var result = await _sim.RunAsync(Config, progress);

if (result.Success)
{
Status = $"Simulation completed ({result.Elapsed.TotalSeconds:F1}s)";
L($"Result: {(result.Success ? "PASS" : "FAIL")}");
foreach (var note in result.Notes)
L($" Note: {note}");

// Generate report
var reportPath = Path.Combine(Config.OutputDirectory, "simulation_report.md");
// report generation will be in next PR
}
else
{
Status = $"Simulation failed: {result.ErrorMessage}";
}
}
catch (Exception ex)
{
Status = $"Error: {ex.Message}";
L($"FATAL: {ex}");
}
finally
{
IsRunning = false;
}
}

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