Eerste commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user