Skip to content

Commit 6ebae28

Browse files
authored
feat: Start/Stop server buttons, server stays running until manual stop (#53)
- Replace checkbox+auto-run with Start Server / Stop Server / Run Client buttons - Server stays running after start, user controls when to stop - SimulationService split into StartServerAsync / StopServerAsync / RunClientAsync - SimulateConfigModel: remove AutoRun, add ServerRunning flag
1 parent 61b75f8 commit 6ebae28

4 files changed

Lines changed: 111 additions & 165 deletions

File tree

src/Models/SimulateConfigModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ public partial class SimulateConfigModel : ObservableObject
1616
[ObservableProperty] private string _appSecretKey = "dfeb5833-975e-4afb-88f1-6278ee9aeff6";
1717
[ObservableProperty] private string _productId = "2d974e2a-31e6-4887-9bb1-b4689e98c77a";
1818
[ObservableProperty] private string _outputDirectory = string.Empty;
19-
[ObservableProperty] private bool _autoRun = true;
2019
public int ServerPort { get; set; } = 5000;
20+
public bool ServerRunning { get; set; }
2121
}

src/Services/SimulationService.cs

Lines changed: 63 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -10,215 +10,144 @@
1010

1111
namespace GeneralUpdate.Tools.Services;
1212

13-
/// <summary>
14-
/// Orchestrates the full update simulation: server, client, upgrade, log collection.
15-
/// </summary>
1613
public class SimulationService
1714
{
1815
private readonly ClientGeneratorService _generator = new();
1916
private readonly LocalUpdateServer _server = new();
20-
private readonly StringBuilder _fullLog = new();
2117
private int _timeoutSeconds = 120;
2218

23-
public IReadOnlyList<string> LogLines => _fullLog.ToString().Split('\n').ToList();
19+
public string ServerBaseUrl => _server.BaseUrl;
2420

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)
2964
{
3065
var result = new SimulationResult();
3166
var sw = Stopwatch.StartNew();
3267

3368
try
3469
{
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);
7871
var clientResult = await RunDotNetScript(config.OutputDirectory, "client.csx", ct);
7972
Log(clientResult.Output, progress);
8073

8174
if (!clientResult.Success)
8275
{
83-
Log(" Client failed - see output above", progress);
8476
result.Success = false;
8577
result.ErrorMessage = "Client exited with error";
8678
return result;
8779
}
8880

89-
Log(" Client completed successfully", progress);
81+
Log("Client completed", progress);
82+
await Task.Delay(2000, ct);
9083

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}");
9586

9687
result.Success = true;
9788
result.Elapsed = sw.Elapsed;
98-
Log($"Simulation complete ({sw.Elapsed.TotalSeconds:F1}s)", progress);
89+
Log($"Simulation complete ({sw.Elapsed.TotalSeconds:F1}s)", progress);
9990
}
10091
catch (Exception ex)
10192
{
10293
result.Success = false;
10394
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);
11696
}
11797

98+
result.FullLog = "";
11899
return result;
119100
}
120101

121102
private void Validate(SimulateConfigModel config)
122103
{
123104
if (!Directory.Exists(config.AppDirectory))
124105
throw new DirectoryNotFoundException($"App directory not found: {config.AppDirectory}");
125-
126106
if (!File.Exists(config.PatchFilePath))
127107
throw new FileNotFoundException($"Patch file not found: {config.PatchFilePath}");
128-
129108
if (string.IsNullOrWhiteSpace(config.OutputDirectory))
130109
throw new ArgumentException("Output directory is required");
131-
132-
// Check dotnet
133110
try
134111
{
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);
143114
var ver = p?.StandardOutput.ReadToEnd().Trim();
144115
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");
146117
}
147118
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"); }
149120
}
150121

151122
private async Task<(bool Success, string Output)> RunDotNetScript(string workDir, string script, CancellationToken ct)
152123
{
153124
var psi = new ProcessStartInfo("dotnet", $"script {script}")
154125
{
155126
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
162130
};
163-
164131
using var p = Process.Start(psi)!;
165132
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);
180135
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]"); }
188137
await Task.WhenAll(readTask, errorTask);
189138
var outputStr = output.ToString();
190139
var hasError = outputStr.Contains("ERROR:") || outputStr.Contains("FATAL:") || outputStr.Contains("JsonException");
191-
192140
return (!hasError && p.ExitCode == 0, outputStr);
193141
}
194142

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-
209143
private static string ComputeQuickHash(string filePath)
210144
{
211145
using var sha = System.Security.Cryptography.SHA256.Create();
212146
using var fs = File.OpenRead(filePath);
213147
return BitConverter.ToString(sha.ComputeHash(fs)).Replace("-", "").ToLowerInvariant();
214148
}
215149

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}");
222151
}
223152

224153
public class SimulationResult

src/ViewModels/SimulateViewModel.cs

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -84,45 +84,50 @@ public int AppTypeIndex
8484
[RelayCommand] async Task SelectOutputDir() { var p = await PickFolder(_loc["Sim.SelectOutput"]); if (p != null) Config.OutputDirectory = p; }
8585

8686
[RelayCommand]
87-
async Task StartSimulation()
87+
async Task StartServer()
8888
{
8989
if (string.IsNullOrWhiteSpace(Config.AppDirectory)) { Status = _loc["Sim.ValidateDirs"]; return; }
9090
if (string.IsNullOrWhiteSpace(Config.PatchFilePath)) { Status = _loc["Sim.ValidateDirs"]; return; }
9191
if (string.IsNullOrWhiteSpace(Config.OutputDirectory)) { Status = _loc["Sim.ValidateDirs"]; return; }
9292

93-
IsRunning = true;
94-
Log.Clear();
95-
Status = _loc["Sim.Starting"];
96-
93+
IsRunning = true; Log.Clear(); Status = _loc["Sim.Starting"];
9794
try
9895
{
99-
var progress = new Progress<string>(L);
100-
var result = await _sim.RunAsync(Config, progress);
96+
await _sim.StartServerAsync(Config, new Progress<string>(L));
97+
Status = $"Server: {_sim.ServerBaseUrl}";
98+
L($"Server running on {_sim.ServerBaseUrl}");
99+
L($"Manual: dotnet script client.csx");
100+
}
101+
catch (Exception ex) { Status = $"Error: {ex.Message}"; L($"FATAL: {ex}"); }
102+
finally { IsRunning = false; }
103+
}
101104

105+
[RelayCommand]
106+
async Task StopServer()
107+
{
108+
await _sim.StopServerAsync();
109+
Status = _loc["Patch.Ready"];
110+
L("Server stopped");
111+
}
112+
113+
[RelayCommand]
114+
async Task RunClient()
115+
{
116+
if (!Config.ServerRunning) { Status = "Server not running"; return; }
117+
IsRunning = true; Status = "Running client...";
118+
try
119+
{
120+
var result = await _sim.RunClientAsync(Config, new Progress<string>(L));
102121
if (result.Success)
103122
{
104123
Status = _loc.T("Sim.Completed", result.Elapsed.TotalSeconds);
105-
L($"Result: {(result.Success ? "PASS" : "FAIL")}");
106-
foreach (var note in result.Notes)
107-
L($" Note: {note}");
108-
109124
var reportPath = await _report.GenerateAsync(Config, result, Config.OutputDirectory);
110125
L(_loc.T("Sim.Report", reportPath));
111126
}
112-
else
113-
{
114-
Status = _loc.T("Sim.Failed", result.ErrorMessage);
115-
}
116-
}
117-
catch (Exception ex)
118-
{
119-
Status = $"Error: {ex.Message}";
120-
L($"FATAL: {ex}");
121-
}
122-
finally
123-
{
124-
IsRunning = false;
127+
else { Status = _loc.T("Sim.Failed", result.ErrorMessage ?? "unknown"); }
125128
}
129+
catch (Exception ex) { Status = $"Error: {ex.Message}"; L($"FATAL: {ex}"); }
130+
finally { IsRunning = false; }
126131
}
127132

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

src/Views/SimulateView.axaml

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,24 @@
8181
</StackPanel>
8282
</Border>
8383

84-
<!-- Run -->
85-
<CheckBox Content="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Sim.AutoRun]}"
86-
IsChecked="{Binding Config.AutoRun}"/>
87-
<Button Content="{Binding Source={x:Static svc:LocalizationService.Instance}, Path=[Sim.Start]}"
88-
Command="{Binding StartSimulationCommand}"
89-
IsEnabled="{Binding !IsRunning}" Height="40" FontSize="14" HorizontalAlignment="Stretch"/>
84+
<!-- Server Controls -->
85+
<Grid ColumnDefinitions="*,*,Auto" Margin="0,4,0,0">
86+
<Button Grid.Column="0" Content="🚀 Start Server"
87+
Command="{Binding StartServerCommand}"
88+
IsEnabled="{Binding !Config.ServerRunning}"
89+
Height="40" FontSize="14"
90+
Margin="0,0,4,0"/>
91+
<Button Grid.Column="1" Content="⏹ Stop Server"
92+
Command="{Binding StopServerCommand}"
93+
IsEnabled="{Binding Config.ServerRunning}"
94+
Height="40" FontSize="14"
95+
Margin="4,0"/>
96+
<Button Grid.Column="2" Content="▶ Run Client"
97+
Command="{Binding RunClientCommand}"
98+
IsEnabled="{Binding Config.ServerRunning}"
99+
Height="40" FontSize="14" MinWidth="110"
100+
Margin="4,0,0,0"/>
101+
</Grid>
90102
<TextBlock Text="{Binding Status}" FontSize="13"/>
91103

92104
<!-- Log -->

0 commit comments

Comments
 (0)