using System.Diagnostics; using System.IO; using System.Net.Http; namespace MijnBesluit.Uitroller; /// /// 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 /// dotnet build, starten met dotnet run 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. /// public sealed class Lokaaltest : IDisposable { private readonly Instellingen _instellingen; private readonly Action _meld; private Process? _site; /// 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. private Process? _browser; public Lokaaltest(Instellingen instellingen, Action meld) { _instellingen = instellingen; _meld = meld; } public bool Draait => _site is { HasExited: false }; /// Gaat af als de site stopt, ook als hij vanzelf stopt; het venster zet /// dan de knop terug op "Lokaal testen". public event Action? Gestopt; private string Webproject => Path.Combine(_instellingen.Projectmap, "src", "MijnBesluit.Web"); /// 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. private bool _mobiel; /// /// Start de testversie. bepaalt alleen het Chrome-venster: bij /// true opent Chrome smal (telefoonformaat, zoals RenH's WM-testen), bij false /// normaal (desktopformaat, zoals voorheen). De site zelf bouwt en start hetzelfde in beide /// gevallen - dit is geen apart profiel, alleen een ander Chrome-venster. /// public async Task 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; } /// /// 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. /// 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); } /// Bij het sluiten van het venster: niet wachten, alleen afschieten. 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 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; } } /// Chrome, zoals gevraagd. Staat Chrome er niet, dan de standaardbrowser. private void OpenBrowser() { var adres = _instellingen.Testadres.TrimEnd('/') + "/"; // Het e-mailadres alvast meegeven, zodat het op de inlogpagina ingevuld staat (de verse // profielmap heeft geen onthoud-cookie). Alleen invullen, niet inloggen. if (!string.IsNullOrWhiteSpace(_instellingen.Emailadres)) adres += "?email=" + Uri.EscapeDataString(_instellingen.Emailadres.Trim()); 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); } } /// 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. 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 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; } }