Eerste commit

This commit is contained in:
jf
2026-09-27 14:21:31 +00:00
commit de4ff128cb
295 changed files with 47374 additions and 0 deletions
+309
View File
@@ -0,0 +1,309 @@
using System.Diagnostics;
using System.IO;
using System.Net.Http;
namespace MijnBesluit.Uitroller;
/// <summary>
/// De testversie op deze pc: MijnBesluit.Web bouwen, starten, en Chrome openen op de site.
///
/// Hetzelfde als F5 in Visual Studio, maar zonder Visual Studio: bouwen met
/// <c>dotnet build</c>, starten met <c>dotnet run</c> en het profiel uit
/// launchSettings.json (Development, dus de user secrets doen mee), en pas als de site
/// antwoordt de browser openen. Zo krijg je geen "deze site is niet bereikbaar" omdat
/// Chrome sneller was dan de site.
///
/// De site blijft draaien tot je op "Test stoppen" drukt of het venster sluit.
/// </summary>
public sealed class Lokaaltest : IDisposable
{
private readonly Instellingen _instellingen;
private readonly Action<string, bool> _meld;
private Process? _site;
/// <summary>De Chrome van de test: een eigen exemplaar met een eigen profiel, zodat
/// Test stoppen precies dat venster kan sluiten en niet de Chrome waarin je werkt.</summary>
private Process? _browser;
public Lokaaltest(Instellingen instellingen, Action<string, bool> meld)
{
_instellingen = instellingen;
_meld = meld;
}
public bool Draait => _site is { HasExited: false };
/// <summary>Gaat af als de site stopt, ook als hij vanzelf stopt; het venster zet
/// dan de knop terug op "Lokaal testen".</summary>
public event Action? Gestopt;
private string Webproject => Path.Combine(_instellingen.Projectmap, "src", "MijnBesluit.Web");
/// <summary>Voor welke weergave de laatst gestarte test Chrome heeft geopend (desktop of
/// mobiel/smal venster) - onthouden zodat StopAsync/Dispose niets extra's hoeft te weten.</summary>
private bool _mobiel;
/// <summary>
/// Start de testversie. <paramref name="mobiel"/> bepaalt alleen het Chrome-venster: bij
/// <c>true</c> opent Chrome smal (telefoonformaat, zoals RenH's WM-testen), bij <c>false</c>
/// normaal (desktopformaat, zoals voorheen). De site zelf bouwt en start hetzelfde in beide
/// gevallen - dit is geen apart profiel, alleen een ander Chrome-venster.
/// </summary>
public async Task<bool> StartAsync(CancellationToken ct, bool mobiel = false)
{
_mobiel = mobiel;
await StopAsync();
if (!Directory.Exists(Webproject))
{
_meld($"Het webproject staat er niet: {Webproject}", true);
return false;
}
// Draait er al iets op dat adres — meestal Visual Studio — dan niet bouwen:
// de dll's zitten dan vast. Alleen de browser openen.
if (await AntwoordtAsync(ct))
{
_meld($"Er draait al een MijnBesluit op {_instellingen.Testadres} (Visual Studio?). " +
"Ik open alleen de browser.", false);
OpenBrowser();
return true;
}
_meld("MijnBesluit.Web bouwen (Debug) …", false);
if (!await DraaiAsync("dotnet", $"build \"{Webproject}\" -c Debug --nologo -v minimal", ct))
{
_meld("Bouwen is mislukt; zie de regels hierboven.", true);
return false;
}
_meld("MijnBesluit.Web starten …", false);
var start = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"run --project \"{Webproject}\" --no-build --launch-profile https",
WorkingDirectory = Webproject,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
_site = new Process { StartInfo = start, EnableRaisingEvents = true };
_site.OutputDataReceived += (_, e) => { if (!string.IsNullOrWhiteSpace(e.Data)) _meld("site: " + e.Data, false); };
_site.ErrorDataReceived += (_, e) => { if (!string.IsNullOrWhiteSpace(e.Data)) _meld("site: " + e.Data, true); };
_site.Exited += (_, _) => { _meld("De testversie is gestopt.", false); Gestopt?.Invoke(); };
try
{
_site.Start();
_site.BeginOutputReadLine();
_site.BeginErrorReadLine();
}
catch (Exception ex)
{
_meld($"Kon dotnet niet starten: {ex.Message}", true);
_site = null;
return false;
}
// Wachten tot hij antwoordt, maximaal een minuut.
for (var i = 0; i < 120; i++)
{
if (ct.IsCancellationRequested) { await StopAsync(); return false; }
if (_site.HasExited)
{
_meld("De site stopte meteen weer. Kijk naar de regels met \"site:\" hierboven.", true);
return false;
}
if (await AntwoordtAsync(ct)) break;
await Task.Delay(500, ct);
}
if (!await AntwoordtAsync(ct))
{
_meld($"De site antwoordt na een minuut nog niet op {_instellingen.Testadres}.", true);
return false;
}
_meld($"De site draait op {_instellingen.Testadres}.", false);
OpenBrowser();
return true;
}
/// <summary>
/// Stopt de site. Buiten de UI-thread: het afschieten van de procesboom en het
/// afronden van de uitvoer kan even duren, en zolang dat op de UI-thread gebeurt
/// staat het venster stil — en als de uitvoer dan nog een regel in het logboek wil
/// zetten, wacht die op de UI-thread die op hem wacht. Dat was het vastlopen.
/// </summary>
public async Task StopAsync()
{
SluitBrowser();
var site = _site;
_site = null;
if (site is null) return;
await Task.Run(() =>
{
try
{
site.CancelOutputRead();
site.CancelErrorRead();
}
catch (Exception) { /* lezen was al gestopt */ }
try
{
if (!site.HasExited)
{
// Met de hele boom: dotnet run start zelf nog een proces (MijnBesluit.Web.exe).
site.Kill(entireProcessTree: true);
site.WaitForExit(5000);
}
}
catch (Exception ex)
{
_meld($"Stoppen lukte niet helemaal: {ex.Message}", true);
}
finally
{
site.Dispose();
}
});
_meld("Testversie gestopt.", false);
}
/// <summary>Bij het sluiten van het venster: niet wachten, alleen afschieten.</summary>
public void Dispose()
{
SluitBrowser();
var site = _site;
_site = null;
if (site is null) return;
try { if (!site.HasExited) site.Kill(entireProcessTree: true); } catch (Exception) { }
site.Dispose();
}
private async Task<bool> AntwoordtAsync(CancellationToken ct)
{
try
{
// Het ontwikkelcertificaat van localhost hoeft hier niet gecontroleerd te
// worden: we vragen alleen of er iets luistert. Alleen voor localhost.
using var handler = new HttpClientHandler
{
AllowAutoRedirect = false,
ServerCertificateCustomValidationCallback = (bericht, _, _, _) =>
bericht.RequestUri?.IsLoopback == true
};
using var http = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(2) };
using var antwoord = await http.GetAsync(_instellingen.Testadres.TrimEnd('/') + "/inloggen", ct);
return true; // elk antwoord telt: 200, een doorverwijzing, zelfs een fout — er luistert iets
}
catch (Exception) when (!ct.IsCancellationRequested)
{
return false;
}
}
/// <summary>Chrome, zoals gevraagd. Staat Chrome er niet, dan de standaardbrowser.</summary>
private void OpenBrowser()
{
var adres = _instellingen.Testadres.TrimEnd('/') + "/";
var chrome = new[]
{
_instellingen.Chrome,
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Google\Chrome\Application\chrome.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Google\Chrome\Application\chrome.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Google\Chrome\Application\chrome.exe")
}.FirstOrDefault(p => !string.IsNullOrWhiteSpace(p) && File.Exists(p));
try
{
if (chrome is not null)
{
// Eigen profielmap: dan is het een apart Chrome-exemplaar dat we bij
// Test stoppen kunnen sluiten. De map blijft bestaan, dus je blijft
// ingelogd en instellingen (zoals de splitters) worden onthouden.
// Mobiel krijgt een eigen profielmap, anders vecht die om dezelfde
// (onthouden) venstergrootte als de desktop-test.
var profiel = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MijnBesluitTools", _mobiel ? "ChromeTestMobiel" : "ChromeTest");
var nieuw = !Directory.Exists(profiel);
Directory.CreateDirectory(profiel);
var start = new ProcessStartInfo(chrome) { UseShellExecute = false };
start.ArgumentList.Add($"--user-data-dir={profiel}");
start.ArgumentList.Add("--no-first-run");
start.ArgumentList.Add("--no-default-browser-check");
start.ArgumentList.Add("--new-window");
if (_mobiel)
{
// Smal venster (telefoonformaat), zodat de site zijn mobiele/responsieve
// weergave laat zien. Dit is venstergrootte, geen echte devicetools-emulatie
// (geen andere user-agent of device-pixel-ratio) - net als bij RenH's WM-testen.
start.ArgumentList.Add("--window-size=412,915");
start.ArgumentList.Add("--window-position=120,40");
}
else if (nieuw)
{
// Alleen de allereerste keer een vaste maat/positie meegeven; daarna onthoudt
// Chrome zelf de laatst gebruikte venstergrootte in deze profielmap.
start.ArgumentList.Add("--window-size=1366,900");
start.ArgumentList.Add("--window-position=60,40");
}
start.ArgumentList.Add(adres);
_browser = Process.Start(start);
}
else
{
_meld("Chrome niet gevonden; de standaardbrowser wordt gebruikt.", false);
Process.Start(new ProcessStartInfo(adres) { UseShellExecute = true });
}
}
catch (Exception ex)
{
_meld($"De browser kon niet worden gestart: {ex.Message}. Open zelf {adres}", true);
}
}
/// <summary>Sluit de test-Chrome (alleen die, met al zijn tabbladen en processen).
/// Bij de standaardbrowser weten we niet welk tabblad het is; die blijft open.</summary>
private void SluitBrowser()
{
var browser = _browser;
_browser = null;
if (browser is null) return;
try { if (!browser.HasExited) browser.Kill(entireProcessTree: true); }
catch (Exception) { /* al dicht */ }
finally { browser.Dispose(); }
}
private async Task<bool> DraaiAsync(string programma, string argumenten, CancellationToken ct)
{
var start = new ProcessStartInfo
{
FileName = programma,
Arguments = argumenten,
WorkingDirectory = _instellingen.Projectmap,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var proces = new Process { StartInfo = start };
proces.OutputDataReceived += (_, e) => { if (!string.IsNullOrWhiteSpace(e.Data)) _meld(e.Data, false); };
proces.ErrorDataReceived += (_, e) => { if (!string.IsNullOrWhiteSpace(e.Data)) _meld(e.Data, true); };
proces.Start();
proces.BeginOutputReadLine();
proces.BeginErrorReadLine();
await proces.WaitForExitAsync(ct);
return proces.ExitCode == 0;
}
}