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
1 change: 0 additions & 1 deletion src/Models/SimulateConfigModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,4 @@ public partial class SimulateConfigModel : ObservableObject
[ObservableProperty] private string _productId = "2d974e2a-31e6-4887-9bb1-b4689e98c77a";
[ObservableProperty] private string _outputDirectory = string.Empty;
public int ServerPort { get; set; } = 5000;
public bool ServerRunning { get; set; }
}
120 changes: 66 additions & 54 deletions src/Services/SimulationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,60 +14,51 @@ public class SimulationService
{
private readonly ClientGeneratorService _generator = new();
private readonly LocalUpdateServer _server = new();
private readonly StringBuilder _fullLog = new();
private int _timeoutSeconds = 120;

public string ServerBaseUrl => _server.BaseUrl;

/// <summary>
/// Generate scripts and start the local update server. Server stays running until StopServerAsync is called.
/// </summary>
public async Task StartServerAsync(SimulateConfigModel config, IProgress<string>? progress = null)
{
Log("STEP 1: Validating inputs", progress);
Validate(config);

Log("STEP 2: Generating client.csx and upgrade.csx", progress);
await _generator.GenerateAsync(config, config.OutputDirectory);
Log($" client.csx → {config.OutputDirectory}", progress);
Log($" upgrade.csx → {config.OutputDirectory}", progress);

Log("STEP 3: Starting 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);
config.ServerPort = _server.Port;
config.ServerRunning = true;
Log($" Server running on {_server.BaseUrl}", progress);
}

/// <summary>
/// Stop the local update server.
/// </summary>
public async Task StopServerAsync()
{
await _server.DisposeAsync();
LocalUpdateServerFiles.Clear();
}

/// <summary>
/// Run the client script and return results. Server must already be running.
/// </summary>
public async Task<SimulationResult> RunClientAsync(SimulateConfigModel config, IProgress<string>? progress = null, CancellationToken ct = default)
public async Task<SimulationResult> RunAsync(
SimulateConfigModel config,
IProgress<string>? progress = null,
CancellationToken ct = default)
{
var result = new SimulationResult();
var sw = Stopwatch.StartNew();

try
{
Log("Running client (dotnet script client.csx)", progress);
// 1. Validate
Log("STEP 1: Validating inputs", progress);
Validate(config);

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

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

// 4. Start server
Log("STEP 4: Starting 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;

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

Expand All @@ -78,24 +69,30 @@ public async Task<SimulationResult> RunClientAsync(SimulateConfigModel config, I
return result;
}

Log("Client completed", progress);
await Task.Delay(2000, ct);
Log(" Client completed successfully", progress);

var fileCount = Directory.GetFiles(config.AppDirectory, "*", SearchOption.AllDirectories).Length;
result.Notes.Add($"Files in app directory after update: {fileCount}");
// 6. Verify
Log("STEP 6: Verifying update result", progress);
await Task.Delay(2000, ct);
VerifyUpdateResult(config, result);

result.Success = true;
result.Elapsed = sw.Elapsed;
Log($"Simulation complete ({sw.Elapsed.TotalSeconds:F1}s)", progress);
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);
Log($"❌ Simulation failed: {ex.Message}", progress);
}
finally
{
try { await _server.DisposeAsync(); } catch { }
LocalUpdateServerFiles.Clear();
result.FullLog = _fullLog.ToString();
}

result.FullLog = "";
return result;
}

Expand Down Expand Up @@ -140,14 +137,29 @@ private void Validate(SimulateConfigModel config)
return (!hasError && p.ExitCode == 0, outputStr);
}

private void VerifyUpdateResult(SimulateConfigModel config, SimulationResult result)
{
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");

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 static void Log(string msg, IProgress<string>? progress) => progress?.Report($"[{DateTime.Now:HH:mm:ss}] {msg}");
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
Expand Down
31 changes: 5 additions & 26 deletions src/ViewModels/SimulateViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public int AppTypeIndex
[RelayCommand] async Task SelectOutputDir() { var p = await PickFolder(_loc["Sim.SelectOutput"]); if (p != null) Config.OutputDirectory = p; }

[RelayCommand]
async Task StartServer()
async Task StartSimulation()
{
if (string.IsNullOrWhiteSpace(Config.AppDirectory)) { Status = _loc["Sim.ValidateDirs"]; return; }
if (string.IsNullOrWhiteSpace(Config.PatchFilePath)) { Status = _loc["Sim.ValidateDirs"]; return; }
Expand All @@ -93,34 +93,13 @@ async Task StartServer()
IsRunning = true; Log.Clear(); Status = _loc["Sim.Starting"];
try
{
await _sim.StartServerAsync(Config, new Progress<string>(L));
Status = $"Server: {_sim.ServerBaseUrl}";
L($"Server running on {_sim.ServerBaseUrl}");
L($"Manual: dotnet script client.csx");
}
catch (Exception ex) { Status = $"Error: {ex.Message}"; L($"FATAL: {ex}"); }
finally { IsRunning = false; }
}

[RelayCommand]
async Task StopServer()
{
await _sim.StopServerAsync();
Status = _loc["Patch.Ready"];
L("Server stopped");
}

[RelayCommand]
async Task RunClient()
{
if (!Config.ServerRunning) { Status = "Server not running"; return; }
IsRunning = true; Status = "Running client...";
try
{
var result = await _sim.RunClientAsync(Config, new Progress<string>(L));
var progress = new Progress<string>(L);
var result = await _sim.RunAsync(Config, progress);
if (result.Success)
{
Status = _loc.T("Sim.Completed", result.Elapsed.TotalSeconds);
L($"Result: {(result.Success ? "PASS" : "FAIL")}");
foreach (var note in result.Notes) L($" Note: {note}");
var reportPath = await _report.GenerateAsync(Config, result, Config.OutputDirectory);
L(_loc.T("Sim.Report", reportPath));
}
Expand Down
22 changes: 4 additions & 18 deletions src/Views/SimulateView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,24 +81,10 @@
</StackPanel>
</Border>

<!-- Server Controls -->
<Grid ColumnDefinitions="*,*,Auto" Margin="0,4,0,0">
<Button Grid.Column="0" Content="🚀 Start Server"
Command="{Binding StartServerCommand}"
IsEnabled="{Binding !Config.ServerRunning}"
Height="40" FontSize="14"
Margin="0,0,4,0"/>
<Button Grid.Column="1" Content="⏹ Stop Server"
Command="{Binding StopServerCommand}"
IsEnabled="{Binding Config.ServerRunning}"
Height="40" FontSize="14"
Margin="4,0"/>
<Button Grid.Column="2" Content="▶ Run Client"
Command="{Binding RunClientCommand}"
IsEnabled="{Binding Config.ServerRunning}"
Height="40" FontSize="14" MinWidth="110"
Margin="4,0,0,0"/>
</Grid>
<!-- Run -->
<Button Content="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Sim.Start]}"
Command="{Binding StartSimulationCommand}"
IsEnabled="{Binding !IsRunning}" Height="40" FontSize="14" HorizontalAlignment="Stretch"/>
<TextBlock Text="{Binding Status}" FontSize="13"/>

<!-- Log -->
Expand Down
Loading