diff --git a/StarterNG.Tests/StartSimulationTests.cs b/StarterNG.Tests/StartSimulationTests.cs
index e21f256..744c5f7 100644
--- a/StarterNG.Tests/StartSimulationTests.cs
+++ b/StarterNG.Tests/StartSimulationTests.cs
@@ -68,6 +68,23 @@ public class StartSimulationTests
Assert.False(launcher.CapturedErrors);
}
+ [Fact]
+ public void Minidumps_of_this_run_are_found_in_the_installation_and_under_crashpad()
+ {
+ var (start, _, installation) = Rig(files => files
+ .WithExecutable(Build)
+ .WithFile(TestInstallation.At("crash_2026-10-04_13-03-12.dmp"), "MDMP")
+ .WithFile(TestInstallation.At("crashdumps", "reports", "1a2b.dmp"), "MDMP"));
+ installation.Files.LastWriteTimeUtc = new DateTime(2026, 10, 4, 11, 3, 12, DateTimeKind.Utc);
+
+ var thisRun = start.CrashDumpsSince(new DateTime(2026, 10, 4, 11, 3, 0, DateTimeKind.Utc), Build);
+ var later = start.CrashDumpsSince(new DateTime(2026, 10, 4, 12, 0, 0, DateTimeKind.Utc), Build);
+
+ Assert.Equal(2, thisRun.Count);
+ Assert.Contains(thisRun, dump => dump.EndsWith("crash_2026-10-04_13-03-12.dmp"));
+ Assert.Empty(later);
+ }
+
[Fact]
public void The_end_of_the_simulator_log_is_read_for_a_silent_crash()
{
diff --git a/StarterNG/Application/StartSimulation.cs b/StarterNG/Application/StartSimulation.cs
index 9678958..b1a25c0 100644
--- a/StarterNG/Application/StartSimulation.cs
+++ b/StarterNG/Application/StartSimulation.cs
@@ -63,6 +63,41 @@ public sealed class StartSimulation
_log = log;
}
+ ///
+ /// Minidumps written since the simulator was started, newest first. The
+ /// simulator names them crash_*.dmp relative to its working directory - the
+ /// installation - and crashpad, where it is set up, keeps its own under
+ /// crashdumps; the folder of the binary is looked at too, for a simulator that
+ /// was once run from there.
+ ///
+ public IReadOnlyList CrashDumpsSince(DateTime startedUtc, string executable)
+ {
+ var dumps = new List();
+ try
+ {
+ foreach (string directory in new[] { _paths.Root, Path.GetDirectoryName(executable) ?? _paths.Root }
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .Where(_files.DirectoryExists))
+ dumps.AddRange(_files.GetFiles(directory, "*.dmp"));
+
+ string crashpad = _paths.FromRoot("crashdumps");
+ if (_files.DirectoryExists(crashpad))
+ dumps.AddRange(_files.GetFilesRecursive(crashpad, "*.dmp"));
+
+ // A little slack: the start is taken just after the launch, not before it.
+ var since = startedUtc - TimeSpan.FromSeconds(5);
+ return dumps.Distinct(StringComparer.OrdinalIgnoreCase)
+ .Where(dump => _files.GetLastWriteTimeUtc(dump) >= since)
+ .OrderByDescending(_files.GetLastWriteTimeUtc)
+ .ToList();
+ }
+ catch (Exception ex)
+ {
+ _log.Log("looking for crash dumps", ex);
+ return Array.Empty();
+ }
+ }
+
/// The end of the simulator's own log, for when it died without a word on its error stream.
public IReadOnlyList SimulatorLogTail(int count)
{
diff --git a/StarterNG/MainWindow.axaml.cs b/StarterNG/MainWindow.axaml.cs
index f9f2972..b1ddeec 100644
--- a/StarterNG/MainWindow.axaml.cs
+++ b/StarterNG/MainWindow.axaml.cs
@@ -522,14 +522,14 @@ public partial class MainWindow : Window
GC.Collect();
GC.WaitForPendingFinalizers();
- WatchSimulator(result.Process);
+ WatchSimulator(result.Process, result.ExecutablePath);
}
///
/// Brings the starter back when the simulator exits. Adopts an already
/// running simulator when we did not start it ourselves.
///
- private void WatchSimulator(IProcessHandle? simulator)
+ private void WatchSimulator(IProcessHandle? simulator, string executable)
{
var started = DateTime.UtcNow;
simulator ??= AppServices.Current.Processes.FindRunning(
@@ -544,7 +544,7 @@ public partial class MainWindow : Window
_ = simulator.WaitForExitAsync().ContinueWith(_ => Dispatcher.UIThread.Post(() =>
{
RestoreFromSimulator();
- ReportIfCrashed(simulator, DateTime.UtcNow - started);
+ ReportIfCrashed(simulator, executable, started);
}), TaskScheduler.Default);
}
@@ -552,13 +552,15 @@ public partial class MainWindow : Window
/// Says why the simulator is gone when it did not end the way a session does:
/// an error code, or a close so soon after the start that nobody played - a
/// window that only flashed. Its own words come from the error stream, or from
- /// its log when that stayed silent.
+ /// its log when that stayed silent, and a minidump it left is pointed at.
///
- private async void ReportIfCrashed(IProcessHandle simulator, TimeSpan runTime)
+ private async void ReportIfCrashed(IProcessHandle simulator, string executable, DateTime startedUtc)
{
if (simulator.ExitCode is not { } code)
return;
+ var runTime = DateTime.UtcNow - startedUtc;
+
var exit = new SimulatorExit(code, runTime, simulator.ErrorTail);
if (!exit.Failed)
return;
@@ -579,8 +581,16 @@ public partial class MainWindow : Window
}
message.AppendLine().Append(App.Loc["SimExitSeeLog"]);
+ string? dump = AppServices.Current.StartSimulation.CrashDumpsSince(startedUtc, executable).FirstOrDefault();
+ if (dump is not null)
+ message.AppendLine().AppendLine().AppendLine(string.Format(App.Loc["SimExitDump"], dump))
+ .Append(App.Loc["SimExitOpenDump"]);
+
Diagnostics.Log(message.ToString().Replace(Environment.NewLine, " | "));
- await MessageBox.Show(this, message.ToString(), App.Loc["SimExitTitle"], MessageBoxButtons.Ok);
+ bool open = await MessageBox.Show(this, message.ToString(), App.Loc["SimExitTitle"],
+ dump is null ? MessageBoxButtons.Ok : MessageBoxButtons.YesNo);
+ if (open && dump is not null && Path.GetDirectoryName(Path.GetFullPath(dump)) is { } folder)
+ AppServices.Current.Processes.OpenInShell(folder);
}
private async void CheckExternalSettings()
diff --git a/StarterNG/startercfg/lang/cz.xml b/StarterNG/startercfg/lang/cz.xml
index e6a40bf..f40ec0c 100644
--- a/StarterNG/startercfg/lang/cz.xml
+++ b/StarterNG/startercfg/lang/cz.xml
@@ -258,6 +258,8 @@
Ukončil se {0:0.0} s po spuštění.
Poslední zprávy:
Celý průběh je v souboru log.txt ve složce simulátoru.
+ Simulátor uložil výpis paměti: {0}
+ Otevřít složku s výpisem? Přiložte jej k hlášení chyby.
Informace
Nastavení
Soubor eu07.ini byl změněn mimo starter. Načíst jej znovu?
diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml
index 3451d1f..305b53b 100644
--- a/StarterNG/startercfg/lang/en.xml
+++ b/StarterNG/startercfg/lang/en.xml
@@ -258,6 +258,8 @@
It closed {0:0.0} s after starting.
Last messages:
The full run is in log.txt in the simulator folder.
+ The simulator saved a memory dump: {0}
+ Open the folder with the dump? Attach it to the bug report.
Info
Settings
eu07.ini was changed outside the starter. Reload it?
diff --git a/StarterNG/startercfg/lang/hu.xml b/StarterNG/startercfg/lang/hu.xml
index f9ba872..eccaad0 100644
--- a/StarterNG/startercfg/lang/hu.xml
+++ b/StarterNG/startercfg/lang/hu.xml
@@ -258,6 +258,8 @@
{0:0.0} másodperccel az indítás után bezárult.
Utolsó üzenetek:
A teljes futás a szimulátor mappájában lévő log.txt fájlban található.
+ A szimulátor memóriaképet mentett: {0}
+ Megnyitod a memóriaképet tartalmazó mappát? Csatold a hibajelentéshez.
Információ
Beállítások
Az eu07.ini fájl a starteren kívül módosult. Újratölti?
diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml
index 5afc1d1..7168818 100644
--- a/StarterNG/startercfg/lang/pl.xml
+++ b/StarterNG/startercfg/lang/pl.xml
@@ -258,6 +258,8 @@
Zamknął się {0:0.0} s po uruchomieniu.
Ostatnie komunikaty:
Pełny przebieg jest w pliku log.txt w katalogu symulatora.
+ Symulator zapisał zrzut pamięci: {0}
+ Otworzyć folder ze zrzutem? Dołącz go do zgłoszenia błędu.
Informacje
Ustawienia
Plik eu07.ini został zmieniony poza starterem. Wczytać ponownie?
diff --git a/StarterNG/startercfg/lang/ru.xml b/StarterNG/startercfg/lang/ru.xml
index f9de835..333c9e7 100644
--- a/StarterNG/startercfg/lang/ru.xml
+++ b/StarterNG/startercfg/lang/ru.xml
@@ -258,6 +258,8 @@
Он закрылся через {0:0.0} с после запуска.
Последние сообщения:
Полный журнал находится в файле log.txt в папке симулятора.
+ Симулятор сохранил дамп памяти: {0}
+ Открыть папку с дампом? Приложите его к сообщению об ошибке.
Информация
Настройки
Файл eu07.ini изменён вне стартера. Загрузить заново?