From 90a63e39a1f88ee584af29150e8497326660729d Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 10:12:45 +0100 Subject: [PATCH 01/20] Set up process runner abstraction --- src/gui/dlnashmon.cc | 83 ++++++++++++++++++++++++++------------------ src/gui/dlnashmon.h | 6 ++-- 2 files changed, 53 insertions(+), 36 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index a07ab5a164..a29fdd8ded 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -44,10 +44,48 @@ END_EVENT_TABLE() #include "bitmaps/stop.xpm" +class ExternalProcessRunner { + wxEvtHandler *m_parent; + +public: + wxProcess *m_process{nullptr}; + long m_pid{0}; + + ExternalProcessRunner(wxEvtHandler *p_parent) : m_parent(p_parent) {} + + void Start(const wxString &p_command, const wxString &p_stdin) + { + m_process = new wxProcess(m_parent, GBT_ID_PROCESS); + m_process->Redirect(); + m_pid = wxExecute(p_command, wxEXEC_ASYNC, m_process); + + wxString str(p_stdin); + // It is possible that the whole string won't write on one go, so + // we should take this possibility into account. If this doesn't + // complete the whole way, we take a 100-millisecond siesta and try + // again. (This seems to primarily be an issue with -- you guessed it -- + // Windows!) + while (!str.empty()) { + wxTextOutputStream os(*m_process->GetOutputStream()); + + // It appears that (at least with mingw) the string itself contains + // only '\n' for newlines. If we don't SetMode here, these get + // converted to '\r\n' sequences, and so the number of characters + // LastWrite() returns does not match the number of characters in + // our string. Setting this explicitly solves this problem. + os.SetMode(wxEOL_UNIX); + os.WriteString(str); + str.Remove(0, m_process->GetOutputStream()->LastWrite()); + wxMilliSleep(100); + } + m_process->CloseOutput(); + } +}; + NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, const std::shared_ptr &p_command) : wxDialog(p_parent, wxID_ANY, wxT("Computing Nash equilibria"), wxDefaultPosition), - m_doc(p_doc), m_process(nullptr), m_timer(this, GBT_ID_TIMER), m_output(p_command) + m_doc(p_doc), m_timer(this, GBT_ID_TIMER), m_output(p_command) { auto *sizer = new wxBoxSizer(wxVERTICAL); @@ -102,11 +140,6 @@ void NashMonitorDialog::Start(std::shared_ptr p_command) m_doc->AddProfileList(p_command); - m_process = new wxProcess(this, GBT_ID_PROCESS); - m_process->Redirect(); - - m_pid = wxExecute(p_command->GetCommand(), wxEXEC_ASYNC, m_process); - std::ostringstream s; if (p_command->IsBehavior()) { m_doc->GetGame()->Write(s, "efg"); @@ -114,27 +147,9 @@ void NashMonitorDialog::Start(std::shared_ptr p_command) else { m_doc->GetGame()->Write(s, "nfg"); } - wxString str(wxString(s.str().c_str(), *wxConvCurrent)); - - // It is possible that the whole string won't write on one go, so - // we should take this possibility into account. If this doesn't - // complete the whole way, we take a 100-millisecond siesta and try - // again. (This seems to primarily be an issue with -- you guessed it -- - // Windows!) - while (str.length() > 0) { - wxTextOutputStream os(*m_process->GetOutputStream()); - - // It appears that (at least with mingw) the string itself contains - // only '\n' for newlines. If we don't SetMode here, these get - // converted to '\r\n' sequences, and so the number of characters - // LastWrite() returns does not match the number of characters in - // our string. Setting this explicitly solves this problem. - os.SetMode(wxEOL_UNIX); - os.WriteString(str); - str.Remove(0, m_process->GetOutputStream()->LastWrite()); - wxMilliSleep(100); - } - m_process->CloseOutput(); + + m_runner = std::make_shared(this); + m_runner->Start(p_command->GetCommand(), wxString(s.str().c_str(), *wxConvCurrent)); m_stopButton->Enable(true); @@ -143,12 +158,12 @@ void NashMonitorDialog::Start(std::shared_ptr p_command) void NashMonitorDialog::OnIdle(wxIdleEvent &p_event) { - if (!m_process) { + if (!m_runner->m_process) { return; } - if (m_process->IsInputAvailable()) { - wxTextInputStream tis(*m_process->GetInputStream()); + if (m_runner->m_process->IsInputAvailable()) { + wxTextInputStream tis(*m_runner->m_process->GetInputStream()); wxString msg; msg << tis.ReadLine(); @@ -171,8 +186,8 @@ void NashMonitorDialog::OnEndProcess(wxProcessEvent &p_event) m_stopButton->Enable(false); m_timer.Stop(); - while (m_process->IsInputAvailable()) { - wxTextInputStream tis(*m_process->GetInputStream()); + while (m_runner->m_process->IsInputAvailable()) { + wxTextInputStream tis(*m_runner->m_process->GetInputStream()); wxString msg; msg << tis.ReadLine(); @@ -205,9 +220,9 @@ void NashMonitorDialog::OnStop(wxCommandEvent &p_event) m_stopButton->Enable(false); #ifdef __WXMSW__ - wxProcess::Kill(m_pid, wxSIGKILL); + wxProcess::Kill(m_runner->m_pid, wxSIGKILL); #else - wxProcess::Kill(m_pid, wxSIGTERM); + wxProcess::Kill(m_runner->m_pid, wxSIGTERM); #endif // __WXMSW__ } } // namespace Gambit::GUI diff --git a/src/gui/dlnashmon.h b/src/gui/dlnashmon.h index 336926cdd7..556b1b534a 100644 --- a/src/gui/dlnashmon.h +++ b/src/gui/dlnashmon.h @@ -28,10 +28,12 @@ #include "gamedoc.h" namespace Gambit::GUI { + +class ExternalProcessRunner; + class NashMonitorDialog final : public wxDialog { GameDocument *m_doc; - int m_pid{0}; - wxProcess *m_process; + std::shared_ptr m_runner; wxWindow *m_profileList; wxStaticText *m_statusText, *m_countText; wxButton *m_stopButton, *m_okButton; From d923f82f644dbfa1e1fb5335579df9722b001a77 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 10:31:29 +0100 Subject: [PATCH 02/20] Create event abstraction for reading a line from the output robustly --- src/gui/dlnashmon.cc | 82 ++++++++++++++++++++++++++++++++------------ src/gui/dlnashmon.h | 1 + 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index a29fdd8ded..6a4cbbc1e5 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -33,6 +33,10 @@ #include "nfgprofile.h" namespace Gambit::GUI { + +wxDECLARE_EVENT(wxEVT_EXTERNAL_RUNNER_LINE, wxThreadEvent); +wxDEFINE_EVENT(wxEVT_EXTERNAL_RUNNER_LINE, wxThreadEvent); + constexpr int GBT_ID_TIMER = 1000; constexpr int GBT_ID_PROCESS = 1001; @@ -46,6 +50,7 @@ END_EVENT_TABLE() class ExternalProcessRunner { wxEvtHandler *m_parent; + wxString m_pending; public: wxProcess *m_process{nullptr}; @@ -80,6 +85,46 @@ class ExternalProcessRunner { } m_process->CloseOutput(); } + + void PollOutput() + { + if (!m_process || !m_process->IsInputAvailable()) { + return; + } + + wxInputStream *stream = m_process->GetInputStream(); + + while (m_process->IsInputAvailable()) { + char ch; + stream->Read(&ch, 1); + if (stream->LastRead() != 1) { + break; + } + + if (ch == '\n') { + PostLine(m_pending); + m_pending.clear(); + } + else if (ch != '\r') { + m_pending += ch; + } + } + } + + void FlushPendingLine() + { + if (!m_pending.empty()) { + PostLine(m_pending); + m_pending.clear(); + } + } + + void PostLine(const wxString &line) + { + auto *evt = new wxThreadEvent(wxEVT_EXTERNAL_RUNNER_LINE); + evt->SetString(line); + wxQueueEvent(m_parent, evt); + } }; NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, @@ -128,6 +173,8 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, wxTopLevelWindowBase::Layout(); CenterOnParent(); + Bind(wxEVT_EXTERNAL_RUNNER_LINE, &NashMonitorDialog::OnRunnerLine, this); + Start(p_command); } @@ -163,19 +210,23 @@ void NashMonitorDialog::OnIdle(wxIdleEvent &p_event) } if (m_runner->m_process->IsInputAvailable()) { - wxTextInputStream tis(*m_runner->m_process->GetInputStream()); - - wxString msg; - msg << tis.ReadLine(); + m_runner->PollOutput(); + p_event.RequestMore(); + } + else { + m_timer.Start(1000, false); + } +} +void NashMonitorDialog::OnRunnerLine(wxThreadEvent &p_event) +{ + const wxString msg = p_event.GetString(); + std::cout << "Got message " << msg << std::endl; + if (!msg.empty()) { m_doc->DoAddOutput(*m_output, msg); wxString label; label << wxT("Number of equilibria found so far: ") << m_output->NumProfiles(); m_countText->SetLabel(label); - p_event.RequestMore(); - } - else { - m_timer.Start(1000, false); } } @@ -186,19 +237,8 @@ void NashMonitorDialog::OnEndProcess(wxProcessEvent &p_event) m_stopButton->Enable(false); m_timer.Stop(); - while (m_runner->m_process->IsInputAvailable()) { - wxTextInputStream tis(*m_runner->m_process->GetInputStream()); - - wxString msg; - msg << tis.ReadLine(); - - if (!msg.empty()) { - m_doc->DoAddOutput(*m_output, msg); - wxString label; - label << wxT("Number of equilibria found so far: ") << m_output->NumProfiles(); - m_countText->SetLabel(label); - } - } + m_runner->PollOutput(); + m_runner->FlushPendingLine(); if (p_event.GetExitCode() == 0) { m_statusText->SetLabel(wxT("The computation has completed.")); diff --git a/src/gui/dlnashmon.h b/src/gui/dlnashmon.h index 556b1b534a..20355144c8 100644 --- a/src/gui/dlnashmon.h +++ b/src/gui/dlnashmon.h @@ -45,6 +45,7 @@ class NashMonitorDialog final : public wxDialog { void OnStop(wxCommandEvent &); void OnTimer(wxTimerEvent &); void OnIdle(wxIdleEvent &); + void OnRunnerLine(wxThreadEvent &p_event); void OnEndProcess(wxProcessEvent &); public: From 3063480cd0d02a78bde2a77911770cfb141625f2 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 10:40:07 +0100 Subject: [PATCH 03/20] More refactoring into runner versus GUI --- src/gui/dlnashmon.cc | 47 +++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 6a4cbbc1e5..367581f3e7 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -50,12 +50,11 @@ END_EVENT_TABLE() class ExternalProcessRunner { wxEvtHandler *m_parent; - wxString m_pending; - -public: wxProcess *m_process{nullptr}; long m_pid{0}; + wxString m_pending; +public: ExternalProcessRunner(wxEvtHandler *p_parent) : m_parent(p_parent) {} void Start(const wxString &p_command, const wxString &p_stdin) @@ -86,10 +85,24 @@ class ExternalProcessRunner { m_process->CloseOutput(); } - void PollOutput() + void Stop() { + // Per the wxWidgets wiki, under Windows, programs that run + // without a console window don't respond to the more polite + // SIGTERM, so instead we must be rude and SIGKILL it. +#ifdef __WXMSW__ + wxProcess::Kill(m_pid, wxSIGKILL); +#else + wxProcess::Kill(m_pid, wxSIGTERM); +#endif // __WXMSW__ + } + + bool PollOutput() + { + bool result = false; + if (!m_process || !m_process->IsInputAvailable()) { - return; + return result; } wxInputStream *stream = m_process->GetInputStream(); @@ -104,11 +117,13 @@ class ExternalProcessRunner { if (ch == '\n') { PostLine(m_pending); m_pending.clear(); + result = true; } else if (ch != '\r') { m_pending += ch; } } + return result; } void FlushPendingLine() @@ -200,21 +215,20 @@ void NashMonitorDialog::Start(std::shared_ptr p_command) m_stopButton->Enable(true); - m_timer.Start(1000, false); + m_timer.StartOnce(1000); } void NashMonitorDialog::OnIdle(wxIdleEvent &p_event) { - if (!m_runner->m_process) { + if (!m_runner) { return; } - if (m_runner->m_process->IsInputAvailable()) { - m_runner->PollOutput(); + if (m_runner->PollOutput()) { p_event.RequestMore(); } else { - m_timer.Start(1000, false); + m_timer.StartOnce(1000); } } @@ -252,17 +266,10 @@ void NashMonitorDialog::OnEndProcess(wxProcessEvent &p_event) m_okButton->Enable(true); } -void NashMonitorDialog::OnStop(wxCommandEvent &p_event) +void NashMonitorDialog::OnStop(wxCommandEvent &) { - // Per the wxWidgets wiki, under Windows, programs that run - // without a console window don't respond to the more polite - // SIGTERM, so instead we must be rude and SIGKILL it. + m_runner->Stop(); m_stopButton->Enable(false); - -#ifdef __WXMSW__ - wxProcess::Kill(m_runner->m_pid, wxSIGKILL); -#else - wxProcess::Kill(m_runner->m_pid, wxSIGTERM); -#endif // __WXMSW__ } + } // namespace Gambit::GUI From e9f23d04b3a26587b6c7f519f12e8ac6627883fc Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 10:54:18 +0100 Subject: [PATCH 04/20] Timer/polling is responsibility of the runner --- src/gui/dlnashmon.cc | 49 ++++++++++++++++++++++---------------------- src/gui/dlnashmon.h | 7 +------ 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 367581f3e7..f16863aada 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -42,20 +42,36 @@ constexpr int GBT_ID_PROCESS = 1001; BEGIN_EVENT_TABLE(NashMonitorDialog, wxDialog) EVT_END_PROCESS(GBT_ID_PROCESS, NashMonitorDialog::OnEndProcess) -EVT_IDLE(NashMonitorDialog::OnIdle) -EVT_TIMER(GBT_ID_TIMER, NashMonitorDialog::OnTimer) END_EVENT_TABLE() #include "bitmaps/stop.xpm" -class ExternalProcessRunner { +class ExternalProcessRunner : public wxEvtHandler { wxEvtHandler *m_parent; wxProcess *m_process{nullptr}; long m_pid{0}; + wxTimer m_timer; wxString m_pending; + void OnTimer(wxTimerEvent &) + { + PollOutput(); + m_timer.StartOnce(1000); + } + public: - ExternalProcessRunner(wxEvtHandler *p_parent) : m_parent(p_parent) {} + ExternalProcessRunner(wxEvtHandler *p_parent) : m_parent(p_parent), m_timer(this, GBT_ID_TIMER) + { + Bind(wxEVT_TIMER, &ExternalProcessRunner::OnTimer, this, GBT_ID_TIMER); + } + + ~ExternalProcessRunner() override + { + m_timer.Stop(); + if (m_process) { + delete m_process; + } + } void Start(const wxString &p_command, const wxString &p_stdin) { @@ -83,10 +99,12 @@ class ExternalProcessRunner { wxMilliSleep(100); } m_process->CloseOutput(); + m_timer.StartOnce(1000); } void Stop() { + m_timer.Stop(); // Per the wxWidgets wiki, under Windows, programs that run // without a console window don't respond to the more polite // SIGTERM, so instead we must be rude and SIGKILL it. @@ -145,7 +163,7 @@ class ExternalProcessRunner { NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, const std::shared_ptr &p_command) : wxDialog(p_parent, wxID_ANY, wxT("Computing Nash equilibria"), wxDefaultPosition), - m_doc(p_doc), m_timer(this, GBT_ID_TIMER), m_output(p_command) + m_doc(p_doc), m_output(p_command) { auto *sizer = new wxBoxSizer(wxVERTICAL); @@ -214,28 +232,11 @@ void NashMonitorDialog::Start(std::shared_ptr p_command) m_runner->Start(p_command->GetCommand(), wxString(s.str().c_str(), *wxConvCurrent)); m_stopButton->Enable(true); - - m_timer.StartOnce(1000); -} - -void NashMonitorDialog::OnIdle(wxIdleEvent &p_event) -{ - if (!m_runner) { - return; - } - - if (m_runner->PollOutput()) { - p_event.RequestMore(); - } - else { - m_timer.StartOnce(1000); - } } void NashMonitorDialog::OnRunnerLine(wxThreadEvent &p_event) { const wxString msg = p_event.GetString(); - std::cout << "Got message " << msg << std::endl; if (!msg.empty()) { m_doc->DoAddOutput(*m_output, msg); wxString label; @@ -244,15 +245,13 @@ void NashMonitorDialog::OnRunnerLine(wxThreadEvent &p_event) } } -void NashMonitorDialog::OnTimer(wxTimerEvent &p_event) { wxWakeUpIdle(); } - void NashMonitorDialog::OnEndProcess(wxProcessEvent &p_event) { m_stopButton->Enable(false); - m_timer.Stop(); m_runner->PollOutput(); m_runner->FlushPendingLine(); + m_runner->Stop(); if (p_event.GetExitCode() == 0) { m_statusText->SetLabel(wxT("The computation has completed.")); diff --git a/src/gui/dlnashmon.h b/src/gui/dlnashmon.h index 20355144c8..85c201fdbb 100644 --- a/src/gui/dlnashmon.h +++ b/src/gui/dlnashmon.h @@ -29,22 +29,17 @@ namespace Gambit::GUI { -class ExternalProcessRunner; - class NashMonitorDialog final : public wxDialog { GameDocument *m_doc; - std::shared_ptr m_runner; + std::shared_ptr m_runner; wxWindow *m_profileList; wxStaticText *m_statusText, *m_countText; wxButton *m_stopButton, *m_okButton; - wxTimer m_timer; std::shared_ptr m_output; void Start(std::shared_ptr p_command); void OnStop(wxCommandEvent &); - void OnTimer(wxTimerEvent &); - void OnIdle(wxIdleEvent &); void OnRunnerLine(wxThreadEvent &p_event); void OnEndProcess(wxProcessEvent &); From d9258636c773c6bb11ddcee0542a9421c335f10d Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 11:03:22 +0100 Subject: [PATCH 05/20] Moved handling of process completion into runner. --- src/gui/dlnashmon.cc | 30 +++++++++++++++++++----------- src/gui/dlnashmon.h | 6 ++---- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index f16863aada..afcc3b3183 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -37,13 +37,12 @@ namespace Gambit::GUI { wxDECLARE_EVENT(wxEVT_EXTERNAL_RUNNER_LINE, wxThreadEvent); wxDEFINE_EVENT(wxEVT_EXTERNAL_RUNNER_LINE, wxThreadEvent); +wxDECLARE_EVENT(wxEVT_EXTERNAL_RUNNER_FINISHED, wxThreadEvent); +wxDEFINE_EVENT(wxEVT_EXTERNAL_RUNNER_FINISHED, wxThreadEvent); + constexpr int GBT_ID_TIMER = 1000; constexpr int GBT_ID_PROCESS = 1001; -BEGIN_EVENT_TABLE(NashMonitorDialog, wxDialog) -EVT_END_PROCESS(GBT_ID_PROCESS, NashMonitorDialog::OnEndProcess) -END_EVENT_TABLE() - #include "bitmaps/stop.xpm" class ExternalProcessRunner : public wxEvtHandler { @@ -59,10 +58,22 @@ class ExternalProcessRunner : public wxEvtHandler { m_timer.StartOnce(1000); } + void OnEndProcess(wxProcessEvent &p_event) + { + m_timer.Stop(); + PollOutput(); + FlushPendingLine(); + + auto *evt = new wxThreadEvent(wxEVT_EXTERNAL_RUNNER_FINISHED); + evt->SetInt(p_event.GetExitCode()); + wxQueueEvent(m_parent, evt); + } + public: ExternalProcessRunner(wxEvtHandler *p_parent) : m_parent(p_parent), m_timer(this, GBT_ID_TIMER) { Bind(wxEVT_TIMER, &ExternalProcessRunner::OnTimer, this, GBT_ID_TIMER); + Bind(wxEVT_END_PROCESS, &ExternalProcessRunner::OnEndProcess, this, GBT_ID_PROCESS); } ~ExternalProcessRunner() override @@ -75,7 +86,7 @@ class ExternalProcessRunner : public wxEvtHandler { void Start(const wxString &p_command, const wxString &p_stdin) { - m_process = new wxProcess(m_parent, GBT_ID_PROCESS); + m_process = new wxProcess(this, GBT_ID_PROCESS); m_process->Redirect(); m_pid = wxExecute(p_command, wxEXEC_ASYNC, m_process); @@ -207,6 +218,7 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, CenterOnParent(); Bind(wxEVT_EXTERNAL_RUNNER_LINE, &NashMonitorDialog::OnRunnerLine, this); + Bind(wxEVT_EXTERNAL_RUNNER_FINISHED, &NashMonitorDialog::OnRunnerFinished, this); Start(p_command); } @@ -245,15 +257,11 @@ void NashMonitorDialog::OnRunnerLine(wxThreadEvent &p_event) } } -void NashMonitorDialog::OnEndProcess(wxProcessEvent &p_event) +void NashMonitorDialog::OnRunnerFinished(wxThreadEvent &p_event) { m_stopButton->Enable(false); - m_runner->PollOutput(); - m_runner->FlushPendingLine(); - m_runner->Stop(); - - if (p_event.GetExitCode() == 0) { + if (p_event.GetInt() == 0) { m_statusText->SetLabel(wxT("The computation has completed.")); m_statusText->SetForegroundColour(wxColour(0, 192, 0)); } diff --git a/src/gui/dlnashmon.h b/src/gui/dlnashmon.h index 85c201fdbb..53ec64ea99 100644 --- a/src/gui/dlnashmon.h +++ b/src/gui/dlnashmon.h @@ -40,14 +40,12 @@ class NashMonitorDialog final : public wxDialog { void Start(std::shared_ptr p_command); void OnStop(wxCommandEvent &); - void OnRunnerLine(wxThreadEvent &p_event); - void OnEndProcess(wxProcessEvent &); + void OnRunnerLine(wxThreadEvent &); + void OnRunnerFinished(wxThreadEvent &); public: NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, const std::shared_ptr &p_command); - - DECLARE_EVENT_TABLE() }; } // namespace Gambit::GUI From cb9bda6d5f6cc7c87bad0a441a0791bde531efcd Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 11:11:16 +0100 Subject: [PATCH 06/20] Remove hard-coded process IDs. --- src/gui/dlnashmon.cc | 19 +++++++++++-------- src/gui/dlnashmon.h | 2 -- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index afcc3b3183..a4c5aa89da 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -25,6 +25,8 @@ #include #endif // WX_PRECOMP #include +#include +#include "wx/sheet/sheet.h" #include "dlnashmon.h" #include "gamedoc.h" @@ -40,9 +42,6 @@ wxDEFINE_EVENT(wxEVT_EXTERNAL_RUNNER_LINE, wxThreadEvent); wxDECLARE_EVENT(wxEVT_EXTERNAL_RUNNER_FINISHED, wxThreadEvent); wxDEFINE_EVENT(wxEVT_EXTERNAL_RUNNER_FINISHED, wxThreadEvent); -constexpr int GBT_ID_TIMER = 1000; -constexpr int GBT_ID_PROCESS = 1001; - #include "bitmaps/stop.xpm" class ExternalProcessRunner : public wxEvtHandler { @@ -55,7 +54,9 @@ class ExternalProcessRunner : public wxEvtHandler { void OnTimer(wxTimerEvent &) { PollOutput(); - m_timer.StartOnce(1000); + if (m_process) { + m_timer.StartOnce(1000); + } } void OnEndProcess(wxProcessEvent &p_event) @@ -67,13 +68,15 @@ class ExternalProcessRunner : public wxEvtHandler { auto *evt = new wxThreadEvent(wxEVT_EXTERNAL_RUNNER_FINISHED); evt->SetInt(p_event.GetExitCode()); wxQueueEvent(m_parent, evt); + + m_process = nullptr; } public: - ExternalProcessRunner(wxEvtHandler *p_parent) : m_parent(p_parent), m_timer(this, GBT_ID_TIMER) + ExternalProcessRunner(wxEvtHandler *p_parent) : m_parent(p_parent), m_timer(this) { - Bind(wxEVT_TIMER, &ExternalProcessRunner::OnTimer, this, GBT_ID_TIMER); - Bind(wxEVT_END_PROCESS, &ExternalProcessRunner::OnEndProcess, this, GBT_ID_PROCESS); + Bind(wxEVT_TIMER, &ExternalProcessRunner::OnTimer, this); + Bind(wxEVT_END_PROCESS, &ExternalProcessRunner::OnEndProcess, this); } ~ExternalProcessRunner() override @@ -86,7 +89,7 @@ class ExternalProcessRunner : public wxEvtHandler { void Start(const wxString &p_command, const wxString &p_stdin) { - m_process = new wxProcess(this, GBT_ID_PROCESS); + m_process = new wxProcess(this); m_process->Redirect(); m_pid = wxExecute(p_command, wxEXEC_ASYNC, m_process); diff --git a/src/gui/dlnashmon.h b/src/gui/dlnashmon.h index 53ec64ea99..d2342135b1 100644 --- a/src/gui/dlnashmon.h +++ b/src/gui/dlnashmon.h @@ -23,8 +23,6 @@ #ifndef GAMBIT_GUI_DLNASHMON_H #define GAMBIT_GUI_DLNASHMON_H -#include -#include "wx/sheet/sheet.h" #include "gamedoc.h" namespace Gambit::GUI { From d4d51ef92a72538261148a5bf438ee8de90ba4ac Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 11:33:12 +0100 Subject: [PATCH 07/20] Improve handling of writing game to the solver (and various solver startup failure modes) --- src/gui/dlnashmon.cc | 56 ++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index a4c5aa89da..ab9611c061 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -44,7 +44,7 @@ wxDEFINE_EVENT(wxEVT_EXTERNAL_RUNNER_FINISHED, wxThreadEvent); #include "bitmaps/stop.xpm" -class ExternalProcessRunner : public wxEvtHandler { +class ExternalProcessRunner final : public wxEvtHandler { wxEvtHandler *m_parent; wxProcess *m_process{nullptr}; long m_pid{0}; @@ -73,7 +73,9 @@ class ExternalProcessRunner : public wxEvtHandler { } public: - ExternalProcessRunner(wxEvtHandler *p_parent) : m_parent(p_parent), m_timer(this) + enum class RunnerStartResult { Ok, LaunchFailed, NoOutputPipe, StdinWriteFailed }; + + explicit ExternalProcessRunner(wxEvtHandler *p_parent) : m_parent(p_parent), m_timer(this) { Bind(wxEVT_TIMER, &ExternalProcessRunner::OnTimer, this); Bind(wxEVT_END_PROCESS, &ExternalProcessRunner::OnEndProcess, this); @@ -87,33 +89,34 @@ class ExternalProcessRunner : public wxEvtHandler { } } - void Start(const wxString &p_command, const wxString &p_stdin) + RunnerStartResult Start(const wxString &p_command, const wxString &p_stdin) { m_process = new wxProcess(this); m_process->Redirect(); m_pid = wxExecute(p_command, wxEXEC_ASYNC, m_process); + if (m_pid == 0) { + delete m_process; + m_process = nullptr; + return RunnerStartResult::LaunchFailed; + } + + auto out = m_process->GetOutputStream(); + if (!out || !out->IsOk()) { + Stop(); + return RunnerStartResult::NoOutputPipe; + } + + const wxScopedCharBuffer bytes = p_stdin.utf8_str(); + const char *data = bytes.data(); + const size_t len = std::strlen(data); - wxString str(p_stdin); - // It is possible that the whole string won't write on one go, so - // we should take this possibility into account. If this doesn't - // complete the whole way, we take a 100-millisecond siesta and try - // again. (This seems to primarily be an issue with -- you guessed it -- - // Windows!) - while (!str.empty()) { - wxTextOutputStream os(*m_process->GetOutputStream()); - - // It appears that (at least with mingw) the string itself contains - // only '\n' for newlines. If we don't SetMode here, these get - // converted to '\r\n' sequences, and so the number of characters - // LastWrite() returns does not match the number of characters in - // our string. Setting this explicitly solves this problem. - os.SetMode(wxEOL_UNIX); - os.WriteString(str); - str.Remove(0, m_process->GetOutputStream()->LastWrite()); - wxMilliSleep(100); + if (!out->WriteAll(data, len)) { + Stop(); + return RunnerStartResult::StdinWriteFailed; } m_process->CloseOutput(); m_timer.StartOnce(1000); + return RunnerStartResult::Ok; } void Stop() @@ -166,7 +169,7 @@ class ExternalProcessRunner : public wxEvtHandler { } } - void PostLine(const wxString &line) + void PostLine(const wxString &line) const { auto *evt = new wxThreadEvent(wxEVT_EXTERNAL_RUNNER_LINE); evt->SetString(line); @@ -244,7 +247,14 @@ void NashMonitorDialog::Start(std::shared_ptr p_command) } m_runner = std::make_shared(this); - m_runner->Start(p_command->GetCommand(), wxString(s.str().c_str(), *wxConvCurrent)); + if (auto result = + m_runner->Start(p_command->GetCommand(), wxString(s.str().c_str(), *wxConvCurrent)); + result != ExternalProcessRunner::RunnerStartResult::Ok) { + m_statusText->SetLabel("Failed to start solver."); + m_statusText->SetForegroundColour(*wxRED); + m_okButton->Enable(true); + return; + } m_stopButton->Enable(true); } From 18fa99ee574e9f9894a8ae3a8776c1faa7c1d7c4 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 11:36:52 +0100 Subject: [PATCH 08/20] Improve implementation of stopping process. --- src/gui/dlnashmon.cc | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index ab9611c061..999d2bd8fb 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -70,6 +70,7 @@ class ExternalProcessRunner final : public wxEvtHandler { wxQueueEvent(m_parent, evt); m_process = nullptr; + m_pid = 0; } public: @@ -119,17 +120,27 @@ class ExternalProcessRunner final : public wxEvtHandler { return RunnerStartResult::Ok; } - void Stop() + bool Stop() { m_timer.Stop(); - // Per the wxWidgets wiki, under Windows, programs that run - // without a console window don't respond to the more polite - // SIGTERM, so instead we must be rude and SIGKILL it. + if (!m_process || m_pid == 0) { + return false; + } #ifdef __WXMSW__ - wxProcess::Kill(m_pid, wxSIGKILL); + constexpr wxSignal signal = wxSIGKILL; #else - wxProcess::Kill(m_pid, wxSIGTERM); -#endif // __WXMSW__ + constexpr wxSignal signal = wxSIGTERM; +#endif + + switch (const auto rc = wxProcess::Kill(m_pid, signal)) { + case wxKILL_OK: + return true; + case wxKILL_NO_PROCESS: + m_pid = 0; + return false; + default: + return false; + } } bool PollOutput() From c83bb9b7251306e487d2908c1f9d279c8ce77876 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 12:31:07 +0100 Subject: [PATCH 09/20] Tidying up --- src/gui/dlnashmon.cc | 37 ++++++++++++++++++------------------- src/gui/dlnashmon.h | 2 +- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 999d2bd8fb..306a02467a 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -73,6 +73,21 @@ class ExternalProcessRunner final : public wxEvtHandler { m_pid = 0; } + void PostLine(const wxString &line) const + { + auto *evt = new wxThreadEvent(wxEVT_EXTERNAL_RUNNER_LINE); + evt->SetString(line); + wxQueueEvent(m_parent, evt); + } + + void FlushPendingLine() + { + if (!m_pending.empty()) { + PostLine(m_pending); + m_pending.clear(); + } + } + public: enum class RunnerStartResult { Ok, LaunchFailed, NoOutputPipe, StdinWriteFailed }; @@ -101,7 +116,7 @@ class ExternalProcessRunner final : public wxEvtHandler { return RunnerStartResult::LaunchFailed; } - auto out = m_process->GetOutputStream(); + const auto out = m_process->GetOutputStream(); if (!out || !out->IsOk()) { Stop(); return RunnerStartResult::NoOutputPipe; @@ -109,9 +124,8 @@ class ExternalProcessRunner final : public wxEvtHandler { const wxScopedCharBuffer bytes = p_stdin.utf8_str(); const char *data = bytes.data(); - const size_t len = std::strlen(data); - if (!out->WriteAll(data, len)) { + if (const size_t len = std::strlen(data); !out->WriteAll(data, len)) { Stop(); return RunnerStartResult::StdinWriteFailed; } @@ -171,21 +185,6 @@ class ExternalProcessRunner final : public wxEvtHandler { } return result; } - - void FlushPendingLine() - { - if (!m_pending.empty()) { - PostLine(m_pending); - m_pending.clear(); - } - } - - void PostLine(const wxString &line) const - { - auto *evt = new wxThreadEvent(wxEVT_EXTERNAL_RUNNER_LINE); - evt->SetString(line); - wxQueueEvent(m_parent, evt); - } }; NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, @@ -240,7 +239,7 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, Start(p_command); } -void NashMonitorDialog::Start(std::shared_ptr p_command) +void NashMonitorDialog::Start(const std::shared_ptr &p_command) { if (!p_command->IsBehavior()) { // Make sure we have a normal form representation diff --git a/src/gui/dlnashmon.h b/src/gui/dlnashmon.h index d2342135b1..4faddf893b 100644 --- a/src/gui/dlnashmon.h +++ b/src/gui/dlnashmon.h @@ -35,7 +35,7 @@ class NashMonitorDialog final : public wxDialog { wxButton *m_stopButton, *m_okButton; std::shared_ptr m_output; - void Start(std::shared_ptr p_command); + void Start(const std::shared_ptr &p_command); void OnStop(wxCommandEvent &); void OnRunnerLine(wxThreadEvent &); From 54efc6e3bf9227feaaccac4425411236bca6ec63 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 12:37:10 +0100 Subject: [PATCH 10/20] Use standard OK button sizer. --- src/gui/dlnashmon.cc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 306a02467a..b29150321d 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -69,6 +69,7 @@ class ExternalProcessRunner final : public wxEvtHandler { evt->SetInt(p_event.GetExitCode()); wxQueueEvent(m_parent, evt); + delete m_process; m_process = nullptr; m_pid = 0; } @@ -209,8 +210,7 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, m_stopButton->SetToolTip(_("Stop the computation")); startSizer->Add(m_stopButton, 0, wxALL | wxALIGN_CENTER, 5); - Connect(wxID_CANCEL, wxEVT_COMMAND_BUTTON_CLICKED, - wxCommandEventHandler(NashMonitorDialog::OnStop)); + Bind(wxEVT_BUTTON, &NashMonitorDialog::OnStop, this, wxID_CANCEL); sizer->Add(startSizer, 0, wxALL | wxALIGN_CENTER, 5); @@ -223,8 +223,9 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, m_profileList->SetSizeHints(wxSize(500, 300)); sizer->Add(m_profileList, 1, wxALL | wxEXPAND, 5); - m_okButton = new wxButton(this, wxID_OK, wxT("OK")); - sizer->Add(m_okButton, 0, wxALL | wxALIGN_RIGHT, 5); + auto *buttonSizer = CreateStdDialogButtonSizer(wxOK); + sizer->Add(buttonSizer, 0, wxALL | wxEXPAND, 5); + m_okButton = dynamic_cast(FindWindow(wxID_OK)); m_okButton->Enable(false); SetSizer(sizer); From 9cba4ec3efb42d635e6c4ff5ef58ffefcd8c239c Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 12:48:02 +0100 Subject: [PATCH 11/20] Improve behaviour and message on user stopping computation. --- src/gui/dlnashmon.cc | 31 ++++++++++++++++++++++++++++--- src/gui/dlnashmon.h | 2 ++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index b29150321d..ce37e41157 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -210,8 +210,6 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, m_stopButton->SetToolTip(_("Stop the computation")); startSizer->Add(m_stopButton, 0, wxALL | wxALIGN_CENTER, 5); - Bind(wxEVT_BUTTON, &NashMonitorDialog::OnStop, this, wxID_CANCEL); - sizer->Add(startSizer, 0, wxALL | wxALIGN_CENTER, 5); if (p_command->IsBehavior()) { @@ -236,6 +234,8 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, Bind(wxEVT_EXTERNAL_RUNNER_LINE, &NashMonitorDialog::OnRunnerLine, this); Bind(wxEVT_EXTERNAL_RUNNER_FINISHED, &NashMonitorDialog::OnRunnerFinished, this); + Bind(wxEVT_BUTTON, &NashMonitorDialog::OnStop, this, wxID_CANCEL); + Bind(wxEVT_CLOSE_WINDOW, &NashMonitorDialog::OnClose, this); Start(p_command); } @@ -285,7 +285,11 @@ void NashMonitorDialog::OnRunnerFinished(wxThreadEvent &p_event) { m_stopButton->Enable(false); - if (p_event.GetInt() == 0) { + if (m_stopRequested) { + m_statusText->SetLabel("The computation was stopped."); + m_statusText->SetForegroundColour(*wxRED); + } + else if (p_event.GetInt() == 0) { m_statusText->SetLabel(wxT("The computation has completed.")); m_statusText->SetForegroundColour(wxColour(0, 192, 0)); } @@ -299,8 +303,29 @@ void NashMonitorDialog::OnRunnerFinished(wxThreadEvent &p_event) void NashMonitorDialog::OnStop(wxCommandEvent &) { + m_stopRequested = true; m_runner->Stop(); m_stopButton->Enable(false); } +void NashMonitorDialog::OnClose(wxCloseEvent &p_event) +{ + if (!m_runner || !m_stopButton->IsEnabled()) { + p_event.Skip(); + return; + } + + m_stopRequested = true; + m_runner->Stop(); + m_stopButton->Enable(false); + m_statusText->SetLabel("Stopping computation..."); + + if (p_event.CanVeto()) { + p_event.Veto(); + } + else { + p_event.Skip(); + } +} + } // namespace Gambit::GUI diff --git a/src/gui/dlnashmon.h b/src/gui/dlnashmon.h index 4faddf893b..10732b7759 100644 --- a/src/gui/dlnashmon.h +++ b/src/gui/dlnashmon.h @@ -34,9 +34,11 @@ class NashMonitorDialog final : public wxDialog { wxStaticText *m_statusText, *m_countText; wxButton *m_stopButton, *m_okButton; std::shared_ptr m_output; + bool m_stopRequested{false}; void Start(const std::shared_ptr &p_command); + void OnClose(wxCloseEvent &); void OnStop(wxCommandEvent &); void OnRunnerLine(wxThreadEvent &); void OnRunnerFinished(wxThreadEvent &); From ee01dd45dc70ce91bede6b1c43cfa7ec70df8efe Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 12:50:41 +0100 Subject: [PATCH 12/20] Stop button semantics --- src/gui/dlnashmon.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index ce37e41157..68bf253a13 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -205,7 +205,7 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, m_countText = new wxStaticText(this, wxID_STATIC, wxT("Number of equilibria found so far: 0 ")); startSizer->Add(m_countText, 0, wxALL | wxALIGN_CENTER, 5); - m_stopButton = new wxBitmapButton(this, wxID_CANCEL, wxBitmap(stop_xpm)); + m_stopButton = new wxBitmapButton(this, wxID_ANY, wxBitmap(stop_xpm)); m_stopButton->Enable(false); m_stopButton->SetToolTip(_("Stop the computation")); startSizer->Add(m_stopButton, 0, wxALL | wxALIGN_CENTER, 5); @@ -234,7 +234,7 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, Bind(wxEVT_EXTERNAL_RUNNER_LINE, &NashMonitorDialog::OnRunnerLine, this); Bind(wxEVT_EXTERNAL_RUNNER_FINISHED, &NashMonitorDialog::OnRunnerFinished, this); - Bind(wxEVT_BUTTON, &NashMonitorDialog::OnStop, this, wxID_CANCEL); + m_stopButton->Bind(wxEVT_BUTTON, &NashMonitorDialog::OnStop, this); Bind(wxEVT_CLOSE_WINDOW, &NashMonitorDialog::OnClose, this); Start(p_command); From c4d4636e4e57dd9527c417d8f70e16d0f9cfc6ea Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 12:54:16 +0100 Subject: [PATCH 13/20] Modernise sizing --- src/gui/dlnashmon.cc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 68bf253a13..4942c07363 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -218,7 +218,7 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, else { m_profileList = new MixedProfileList(this, m_doc); } - m_profileList->SetSizeHints(wxSize(500, 300)); + m_profileList->SetMinSize(wxSize(500, 300)); sizer->Add(m_profileList, 1, wxALL | wxEXPAND, 5); auto *buttonSizer = CreateStdDialogButtonSizer(wxOK); @@ -226,10 +226,9 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, m_okButton = dynamic_cast(FindWindow(wxID_OK)); m_okButton->Enable(false); - SetSizer(sizer); + SetSizerAndFit(sizer); sizer->Fit(this); sizer->SetSizeHints(this); - wxTopLevelWindowBase::Layout(); CenterOnParent(); Bind(wxEVT_EXTERNAL_RUNNER_LINE, &NashMonitorDialog::OnRunnerLine, this); From 3787492eb7d6a5ecbed5a5496382e2f4256bffcc Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 12:58:26 +0100 Subject: [PATCH 14/20] Better error message on launch failure. --- src/gui/dlnashmon.cc | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 4942c07363..7b1c88c6a2 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -257,16 +257,23 @@ void NashMonitorDialog::Start(const std::shared_ptr &p_command) } m_runner = std::make_shared(this); - if (auto result = - m_runner->Start(p_command->GetCommand(), wxString(s.str().c_str(), *wxConvCurrent)); - result != ExternalProcessRunner::RunnerStartResult::Ok) { - m_statusText->SetLabel("Failed to start solver."); - m_statusText->SetForegroundColour(*wxRED); - m_okButton->Enable(true); + switch (const auto result = m_runner->Start(p_command->GetCommand(), + wxString(s.str().c_str(), *wxConvCurrent))) { + case ExternalProcessRunner::RunnerStartResult::Ok: + m_stopButton->Enable(true); return; + case ExternalProcessRunner::RunnerStartResult::LaunchFailed: + m_statusText->SetLabel("Failed to launch solver."); + break; + case ExternalProcessRunner::RunnerStartResult::NoOutputPipe: + m_statusText->SetLabel("Solver launched, but I/O redirection failed."); + break; + case ExternalProcessRunner::RunnerStartResult::StdinWriteFailed: + m_statusText->SetLabel("Failed to send input to solver."); + break; } - - m_stopButton->Enable(true); + m_statusText->SetForegroundColour(*wxRED); + m_okButton->Enable(true); } void NashMonitorDialog::OnRunnerLine(wxThreadEvent &p_event) From e0bd713ddabb060357eeb4c31cd93855b96850ea Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 13:09:46 +0100 Subject: [PATCH 15/20] Only expose function for showing the equilibrium computation monitor --- Makefile.am | 1 - src/gui/dlnashmon.cc | 39 ++++++++++++++++++++++++++++++--- src/gui/dlnashmon.h | 52 -------------------------------------------- src/gui/gameframe.cc | 9 ++++---- 4 files changed, 40 insertions(+), 61 deletions(-) delete mode 100644 src/gui/dlnashmon.h diff --git a/Makefile.am b/Makefile.am index e55a43a9ba..dd2d49d0cd 100644 --- a/Makefile.am +++ b/Makefile.am @@ -566,7 +566,6 @@ gambit_SOURCES = \ src/gui/dlnash.cc \ src/gui/dlnash.h \ src/gui/dlnashmon.cc \ - src/gui/dlnashmon.h \ src/gui/dlnfglogit.cc \ src/gui/efgdisplay.cc \ src/gui/efgdisplay.h \ diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 7b1c88c6a2..41bf8cdcd7 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -28,13 +28,14 @@ #include #include "wx/sheet/sheet.h" -#include "dlnashmon.h" #include "gamedoc.h" #include "efgprofile.h" #include "nfgprofile.h" -namespace Gambit::GUI { +using namespace Gambit::GUI; + +namespace { wxDECLARE_EVENT(wxEVT_EXTERNAL_RUNNER_LINE, wxThreadEvent); wxDEFINE_EVENT(wxEVT_EXTERNAL_RUNNER_LINE, wxThreadEvent); @@ -188,6 +189,27 @@ class ExternalProcessRunner final : public wxEvtHandler { } }; +class NashMonitorDialog final : public wxDialog { + GameDocument *m_doc; + std::unique_ptr m_runner; + wxWindow *m_profileList; + wxStaticText *m_statusText, *m_countText; + wxButton *m_stopButton, *m_okButton; + std::shared_ptr m_output; + bool m_stopRequested{false}; + + void Start(const std::shared_ptr &p_command); + + void OnClose(wxCloseEvent &); + void OnStop(wxCommandEvent &); + void OnRunnerLine(wxThreadEvent &); + void OnRunnerFinished(wxThreadEvent &); + +public: + NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, + const std::shared_ptr &p_command); +}; + NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, const std::shared_ptr &p_command) : wxDialog(p_parent, wxID_ANY, wxT("Computing Nash equilibria"), wxDefaultPosition), @@ -256,7 +278,7 @@ void NashMonitorDialog::Start(const std::shared_ptr &p_command) m_doc->GetGame()->Write(s, "nfg"); } - m_runner = std::make_shared(this); + m_runner = std::make_unique(this); switch (const auto result = m_runner->Start(p_command->GetCommand(), wxString(s.str().c_str(), *wxConvCurrent))) { case ExternalProcessRunner::RunnerStartResult::Ok: @@ -334,4 +356,15 @@ void NashMonitorDialog::OnClose(wxCloseEvent &p_event) } } +} // anonymous namespace + +namespace Gambit::GUI { + +void ShowNashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, + const std::shared_ptr &p_command) +{ + NashMonitorDialog dialog(p_parent, p_doc, p_command); + dialog.ShowModal(); +} + } // namespace Gambit::GUI diff --git a/src/gui/dlnashmon.h b/src/gui/dlnashmon.h deleted file mode 100644 index 10732b7759..0000000000 --- a/src/gui/dlnashmon.h +++ /dev/null @@ -1,52 +0,0 @@ -// -// This file is part of Gambit -// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) -// -// FILE: src/gui/dlnashmon.h -// Dialog for monitoring Nash equilibrium computation progress -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// - -#ifndef GAMBIT_GUI_DLNASHMON_H -#define GAMBIT_GUI_DLNASHMON_H - -#include "gamedoc.h" - -namespace Gambit::GUI { - -class NashMonitorDialog final : public wxDialog { - GameDocument *m_doc; - std::shared_ptr m_runner; - wxWindow *m_profileList; - wxStaticText *m_statusText, *m_countText; - wxButton *m_stopButton, *m_okButton; - std::shared_ptr m_output; - bool m_stopRequested{false}; - - void Start(const std::shared_ptr &p_command); - - void OnClose(wxCloseEvent &); - void OnStop(wxCommandEvent &); - void OnRunnerLine(wxThreadEvent &); - void OnRunnerFinished(wxThreadEvent &); - -public: - NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, - const std::shared_ptr &p_command); -}; -} // namespace Gambit::GUI - -#endif // GAMBIT_GUI_DLNASHMON_H diff --git a/src/gui/gameframe.cc b/src/gui/gameframe.cc index a3ec7ac6c4..07984ca167 100644 --- a/src/gui/gameframe.cc +++ b/src/gui/gameframe.cc @@ -50,7 +50,6 @@ #include "dlexcept.h" #include "dlgameprop.h" #include "dlnash.h" -#include "dlnashmon.h" #include "dlefglogit.h" #include "dlabout.h" @@ -1200,6 +1199,9 @@ void GameFrame::OnToolsDominance(wxCommandEvent &p_event) } } +extern void ShowNashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, + const std::shared_ptr &p_command); + void GameFrame::OnToolsEquilibrium(wxCommandEvent &) { if (!m_doc->GetGame()->IsPerfectRecall()) { @@ -1228,10 +1230,7 @@ void GameFrame::OnToolsEquilibrium(wxCommandEvent &) } } - auto command = dialog.GetCommand(); - - NashMonitorDialog monitordialog(this, m_doc, command); - monitordialog.ShowModal(); + ShowNashMonitorDialog(this, m_doc, dialog.GetCommand()); if (!m_splitter->IsSplit()) { if (m_efgPanel && m_efgPanel->IsShown()) { From 7d881d89a39197c792e87ea94d5f5f532d54b2ad Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 13:11:55 +0100 Subject: [PATCH 16/20] Rename PollOutput --- src/gui/dlnashmon.cc | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 41bf8cdcd7..fd5aac6e3b 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -54,7 +54,7 @@ class ExternalProcessRunner final : public wxEvtHandler { void OnTimer(wxTimerEvent &) { - PollOutput(); + ReadAvailableOutput(); if (m_process) { m_timer.StartOnce(1000); } @@ -63,7 +63,7 @@ class ExternalProcessRunner final : public wxEvtHandler { void OnEndProcess(wxProcessEvent &p_event) { m_timer.Stop(); - PollOutput(); + ReadAvailableOutput(); FlushPendingLine(); auto *evt = new wxThreadEvent(wxEVT_EXTERNAL_RUNNER_FINISHED); @@ -159,12 +159,10 @@ class ExternalProcessRunner final : public wxEvtHandler { } } - bool PollOutput() + void ReadAvailableOutput() { - bool result = false; - if (!m_process || !m_process->IsInputAvailable()) { - return result; + return; } wxInputStream *stream = m_process->GetInputStream(); @@ -179,13 +177,11 @@ class ExternalProcessRunner final : public wxEvtHandler { if (ch == '\n') { PostLine(m_pending); m_pending.clear(); - result = true; } else if (ch != '\r') { m_pending += ch; } } - return result; } }; From bb5b45987e6641885592339b38a4e8710fb8b4fb Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 13:25:43 +0100 Subject: [PATCH 17/20] Organise UI mutating calls --- src/gui/dlnashmon.cc | 64 ++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index fd5aac6e3b..56757f9992 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -201,6 +201,40 @@ class NashMonitorDialog final : public wxDialog { void OnRunnerLine(wxThreadEvent &); void OnRunnerFinished(wxThreadEvent &); + void SetStatusRunning() const + { + m_statusText->SetLabel(wxT("The computation is currently in progress.")); + m_stopButton->Enable(true); + m_okButton->Enable(false); + } + void SetStatusStopping() const + { + m_statusText->SetLabel(wxT("Stopping computation...")); + m_statusText->SetForegroundColour(wxColour(196, 128, 0)); + m_stopButton->Enable(false); + } + void SetStatusStopped() const + { + m_statusText->SetLabel(wxT("The computation has been stopped.")); + m_statusText->SetForegroundColour(wxColour(196, 128, 0)); + m_okButton->Enable(true); + m_stopButton->Enable(false); + } + void SetStatusFinishedNormally() const + { + m_statusText->SetLabel(wxT("The computation has completed.")); + m_statusText->SetForegroundColour(wxColour(0, 192, 0)); + m_okButton->Enable(true); + m_stopButton->Enable(false); + } + void SetStatusFinishedAbnormally(const wxString &p_message) const + { + m_statusText->SetLabel(p_message); + m_statusText->SetForegroundColour(*wxRED); + m_okButton->Enable(true); + m_stopButton->Enable(false); + } + public: NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, const std::shared_ptr &p_command); @@ -215,9 +249,7 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, auto *startSizer = new wxBoxSizer(wxHORIZONTAL); - m_statusText = - new wxStaticText(this, wxID_STATIC, wxT("The computation is currently in progress.")); - m_statusText->SetForegroundColour(*wxBLUE); + m_statusText = new wxStaticText(this, wxID_STATIC, "The computation is currently in progress."); startSizer->Add(m_statusText, 0, wxALL | wxALIGN_CENTER, 5); m_countText = new wxStaticText(this, wxID_STATIC, wxT("Number of equilibria found so far: 0 ")); @@ -278,20 +310,18 @@ void NashMonitorDialog::Start(const std::shared_ptr &p_command) switch (const auto result = m_runner->Start(p_command->GetCommand(), wxString(s.str().c_str(), *wxConvCurrent))) { case ExternalProcessRunner::RunnerStartResult::Ok: - m_stopButton->Enable(true); - return; + SetStatusRunning(); + break; case ExternalProcessRunner::RunnerStartResult::LaunchFailed: - m_statusText->SetLabel("Failed to launch solver."); + SetStatusFinishedAbnormally("Failed to launch solver."); break; case ExternalProcessRunner::RunnerStartResult::NoOutputPipe: - m_statusText->SetLabel("Solver launched, but I/O redirection failed."); + SetStatusFinishedAbnormally("Solver launched, but I/O redirection failed."); break; case ExternalProcessRunner::RunnerStartResult::StdinWriteFailed: - m_statusText->SetLabel("Failed to send input to solver."); + SetStatusFinishedAbnormally("Failed to send input to solver."); break; } - m_statusText->SetForegroundColour(*wxRED); - m_okButton->Enable(true); } void NashMonitorDialog::OnRunnerLine(wxThreadEvent &p_event) @@ -310,19 +340,14 @@ void NashMonitorDialog::OnRunnerFinished(wxThreadEvent &p_event) m_stopButton->Enable(false); if (m_stopRequested) { - m_statusText->SetLabel("The computation was stopped."); - m_statusText->SetForegroundColour(*wxRED); + SetStatusStopped(); } else if (p_event.GetInt() == 0) { - m_statusText->SetLabel(wxT("The computation has completed.")); - m_statusText->SetForegroundColour(wxColour(0, 192, 0)); + SetStatusFinishedNormally(); } else { - m_statusText->SetLabel(wxT("The computation ended abnormally.")); - m_statusText->SetForegroundColour(*wxRED); + SetStatusFinishedAbnormally(wxT("The computation ended abnormally.")); } - - m_okButton->Enable(true); } void NashMonitorDialog::OnStop(wxCommandEvent &) @@ -341,8 +366,7 @@ void NashMonitorDialog::OnClose(wxCloseEvent &p_event) m_stopRequested = true; m_runner->Stop(); - m_stopButton->Enable(false); - m_statusText->SetLabel("Stopping computation..."); + SetStatusStopping(); if (p_event.CanVeto()) { p_event.Veto(); From 68fd071b1abc559d0acffa6de2b7ed19abeac65f Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 8 Apr 2026 13:32:09 +0100 Subject: [PATCH 18/20] Final cleanups. --- src/gui/dlnashmon.cc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 56757f9992..4e40546b78 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -204,6 +204,7 @@ class NashMonitorDialog final : public wxDialog { void SetStatusRunning() const { m_statusText->SetLabel(wxT("The computation is currently in progress.")); + m_statusText->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)); m_stopButton->Enable(true); m_okButton->Enable(false); } @@ -277,7 +278,6 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, m_okButton->Enable(false); SetSizerAndFit(sizer); - sizer->Fit(this); sizer->SetSizeHints(this); CenterOnParent(); @@ -346,15 +346,16 @@ void NashMonitorDialog::OnRunnerFinished(wxThreadEvent &p_event) SetStatusFinishedNormally(); } else { - SetStatusFinishedAbnormally(wxT("The computation ended abnormally.")); + SetStatusFinishedAbnormally( + wxString::Format("The computation ended abnormally (code %d)", p_event.GetInt())); } } void NashMonitorDialog::OnStop(wxCommandEvent &) { m_stopRequested = true; + SetStatusStopping(); m_runner->Stop(); - m_stopButton->Enable(false); } void NashMonitorDialog::OnClose(wxCloseEvent &p_event) @@ -365,8 +366,8 @@ void NashMonitorDialog::OnClose(wxCloseEvent &p_event) } m_stopRequested = true; - m_runner->Stop(); SetStatusStopping(); + m_runner->Stop(); if (p_event.CanVeto()) { p_event.Veto(); From e4af400638070d51d9fe19aff0e0cee890e561ae Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 15 Jul 2026 16:50:37 -0700 Subject: [PATCH 19/20] Removed file from Makefile.am --- Makefile.am | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 573c3780bb..20d84943f1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -498,7 +498,6 @@ gambit_SOURCES = \ src/gui/dlnash.cc \ src/gui/dlnash.h \ src/gui/dlnashmon.cc \ - src/gui/dlnashmon.h \ src/gui/dlnewtable.cc \ src/gui/dlnewtable.h \ src/gui/dlnfglogit.cc \ From 868506fb17ba65700a8305493a19cd69dd208103 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 20 Jul 2026 15:13:39 -0400 Subject: [PATCH 20/20] Relocate parsing of profiles from external tools This moves the parsing of profiles read from external command-line tools into the process runner, and out of the analysis profile list. --- src/gui/analysis.cc | 74 ++++-------------------------- src/gui/analysis.h | 5 +-- src/gui/dlnashmon.cc | 105 ++++++++++++++++++++++++++++++++++--------- src/gui/gamedoc.cc | 6 +-- src/gui/gamedoc.h | 2 +- 5 files changed, 98 insertions(+), 94 deletions(-) diff --git a/src/gui/analysis.cc b/src/gui/analysis.cc index 84f1d3a7f3..6ccb6981bf 100644 --- a/src/gui/analysis.cc +++ b/src/gui/analysis.cc @@ -25,7 +25,6 @@ #include #endif // WX_PRECOMP #include - #include "gambit.h" #include "core/tinyxml.h" // for XML parser for Load() @@ -37,77 +36,22 @@ namespace Gambit::GUI { // class AnalysisProfileList //========================================================================= -// Use anonymous namespace to make these helpers private -namespace { - -class NotNashException final : public std::runtime_error { -public: - NotNashException() : std::runtime_error("Output line does not contain a Nash equilibrium") {} - ~NotNashException() noexcept override = default; -}; - template -MixedStrategyProfile OutputToMixedProfile(GameDocument *p_doc, const wxString &p_text) +void AnalysisProfileList::AddProfile(const MixedStrategyProfile &p_profile) { - MixedStrategyProfile profile(p_doc->GetGame()->NewMixedStrategyProfile(static_cast(0.0))); - - if (wxStringTokenizer tok(p_text, wxT(",")); tok.GetNextToken() == wxT("NE")) { - if (tok.CountTokens() == static_cast(profile.MixedProfileLength())) { - for (size_t i = 1; i <= profile.MixedProfileLength(); i++) { - profile[i] = - lexical_cast(std::string((const char *)tok.GetNextToken().mb_str())); - } - return profile; - } + m_mixedProfiles.push_back(std::make_shared>(p_profile)); + if (m_doc->GetGame()->IsTree()) { + m_behavProfiles.push_back(std::make_shared>(p_profile)); } - - throw NotNashException(); + m_current = m_mixedProfiles.size(); } template -MixedBehaviorProfile OutputToBehavProfile(GameDocument *p_doc, const wxString &p_text) -{ - MixedBehaviorProfile profile(p_doc->GetGame()); - - wxStringTokenizer tok(p_text, wxT(",")); - - if (tok.GetNextToken() == wxT("NE")) { - if (tok.CountTokens() == static_cast(profile.BehaviorProfileLength())) { - for (size_t i = 1; i <= profile.BehaviorProfileLength(); i++) { - profile[i] = lexical_cast(std::string(tok.GetNextToken().mb_str())); - } - return profile; - } - } - - throw NotNashException(); -} - -} // end anonymous namespace - -template void AnalysisProfileList::AddOutput(const wxString &p_output) +void AnalysisProfileList::AddProfile(const MixedBehaviorProfile &p_profile) { - try { - if (m_isBehav) { - auto profile = - std::make_shared>(OutputToBehavProfile(m_doc, p_output)); - m_behavProfiles.push_back(profile); - m_mixedProfiles.push_back( - std::make_shared>(profile->ToMixedProfile())); - m_current = m_behavProfiles.size(); - } - else { - auto profile = - std::make_shared>(OutputToMixedProfile(m_doc, p_output)); - m_mixedProfiles.push_back(profile); - if (m_doc->GetGame()->IsTree()) { - m_behavProfiles.push_back(std::make_shared>(*profile)); - } - m_current = m_mixedProfiles.size(); - } - } - catch (NotNashException &) { - } + m_behavProfiles.push_back(std::make_shared>(p_profile)); + m_mixedProfiles.push_back(std::make_shared>(p_profile.ToMixedProfile())); + m_current = m_behavProfiles.size(); } template void AnalysisProfileList::BuildNfg() diff --git a/src/gui/analysis.h b/src/gui/analysis.h index 24a79e6656..70a53107c3 100644 --- a/src/gui/analysis.h +++ b/src/gui/analysis.h @@ -98,8 +98,6 @@ class AnalysisOutput { virtual std::string GetStrategyProb(int p_strategy, int p_index = -1) const = 0; virtual std::string GetStrategyValue(int p_strategy, int p_index = -1) const = 0; - virtual void AddOutput(const wxString &) = 0; - /// Map all behavior profiles to corresponding mixed profiles virtual void BuildNfg() = 0; @@ -169,7 +167,8 @@ template class AnalysisProfileList final : public AnalysisOutput { //! @name Adding profiles to the list //! //@{ - void AddOutput(const wxString &) override; + void AddProfile(const MixedStrategyProfile &); + void AddProfile(const MixedBehaviorProfile &); /// Map all behavior profiles to corresponding mixed profiles void BuildNfg() override; diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index 3350475be8..b839513465 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -26,6 +26,9 @@ #endif // WX_PRECOMP #include #include +#include +#include +#include #include "wx/sheet/sheet.h" #include "gamedoc.h" @@ -33,20 +36,38 @@ #include "efgprofile.h" #include "nfgprofile.h" +using namespace Gambit; using namespace Gambit::GUI; namespace { -wxDECLARE_EVENT(wxEVT_EXTERNAL_RUNNER_LINE, wxThreadEvent); -wxDEFINE_EVENT(wxEVT_EXTERNAL_RUNNER_LINE, wxThreadEvent); +wxDECLARE_EVENT(wxEVT_EXTERNAL_RUNNER_PROFILE, wxThreadEvent); +wxDEFINE_EVENT(wxEVT_EXTERNAL_RUNNER_PROFILE, wxThreadEvent); wxDECLARE_EVENT(wxEVT_EXTERNAL_RUNNER_FINISHED, wxThreadEvent); wxDEFINE_EVENT(wxEVT_EXTERNAL_RUNNER_FINISHED, wxThreadEvent); #include "bitmaps/stop.xpm" +using ComputedProfile = std::variant, MixedStrategyProfile, + MixedBehaviorProfile, MixedBehaviorProfile>; + +template +void AddProfile(AnalysisOutput &p_output, const MixedStrategyProfile &p_profile) +{ + dynamic_cast &>(p_output).AddProfile(p_profile); +} + +template +void AddProfile(AnalysisOutput &p_output, const MixedBehaviorProfile &p_profile) +{ + dynamic_cast &>(p_output).AddProfile(p_profile); +} + class ExternalProcessRunner final : public wxEvtHandler { wxEvtHandler *m_parent; + Game m_game; + const AnalysisOutput &m_output; wxProcess *m_process{nullptr}; long m_pid{0}; wxTimer m_timer; @@ -75,17 +96,61 @@ class ExternalProcessRunner final : public wxEvtHandler { m_pid = 0; } - void PostLine(const wxString &line) const + template std::optional ParseProfile(const wxString &p_line) const { - auto *evt = new wxThreadEvent(wxEVT_EXTERNAL_RUNNER_LINE); - evt->SetString(line); - wxQueueEvent(m_parent, evt); + wxStringTokenizer tokens(p_line, wxT(",")); + if (tokens.GetNextToken() != wxT("NE")) { + return std::nullopt; + } + + if (m_output.IsBehavior()) { + MixedBehaviorProfile profile(m_game); + if (tokens.CountTokens() != profile.BehaviorProfileLength()) { + return std::nullopt; + } + for (size_t i = 1; i <= profile.BehaviorProfileLength(); ++i) { + profile[i] = lexical_cast(std::string(tokens.GetNextToken().mb_str())); + } + return ComputedProfile(std::move(profile)); + } + else { + auto profile = m_game->NewMixedStrategyProfile(static_cast(0)); + if (tokens.CountTokens() != profile.MixedProfileLength()) { + return std::nullopt; + } + for (size_t i = 1; i <= profile.MixedProfileLength(); ++i) { + profile[i] = lexical_cast(std::string(tokens.GetNextToken().mb_str())); + } + return ComputedProfile(std::move(profile)); + } + } + + void ProcessLine(const wxString &p_line) const + { + try { + std::optional profile; + if (dynamic_cast *>(&m_output)) { + profile = ParseProfile(p_line); + } + else if (dynamic_cast *>(&m_output)) { + profile = ParseProfile(p_line); + } + if (!profile) { + return; + } + + auto *event = new wxThreadEvent(wxEVT_EXTERNAL_RUNNER_PROFILE); + event->SetPayload(std::move(*profile)); + wxQueueEvent(m_parent, event); + } + catch (const std::exception &) { + } } void FlushPendingLine() { if (!m_pending.empty()) { - PostLine(m_pending); + ProcessLine(m_pending); m_pending.clear(); } } @@ -93,7 +158,8 @@ class ExternalProcessRunner final : public wxEvtHandler { public: enum class RunnerStartResult { Ok, LaunchFailed, NoOutputPipe, StdinWriteFailed }; - explicit ExternalProcessRunner(wxEvtHandler *p_parent) : m_parent(p_parent), m_timer(this) + ExternalProcessRunner(wxEvtHandler *p_parent, const Game &p_game, const AnalysisOutput &p_output) + : m_parent(p_parent), m_game(p_game), m_output(p_output), m_timer(this) { Bind(wxEVT_TIMER, &ExternalProcessRunner::OnTimer, this); Bind(wxEVT_END_PROCESS, &ExternalProcessRunner::OnEndProcess, this); @@ -175,7 +241,7 @@ class ExternalProcessRunner final : public wxEvtHandler { } if (ch == '\n') { - PostLine(m_pending); + ProcessLine(m_pending); m_pending.clear(); } else if (ch != '\r') { @@ -198,7 +264,7 @@ class NashMonitorDialog final : public wxDialog { void OnClose(wxCloseEvent &); void OnStop(wxCommandEvent &); - void OnRunnerLine(wxThreadEvent &); + void OnRunnerProfile(wxThreadEvent &); void OnRunnerFinished(wxThreadEvent &); void SetStatusRunning() const @@ -281,7 +347,7 @@ NashMonitorDialog::NashMonitorDialog(wxWindow *p_parent, GameDocument *p_doc, sizer->SetSizeHints(this); CenterOnParent(); - Bind(wxEVT_EXTERNAL_RUNNER_LINE, &NashMonitorDialog::OnRunnerLine, this); + Bind(wxEVT_EXTERNAL_RUNNER_PROFILE, &NashMonitorDialog::OnRunnerProfile, this); Bind(wxEVT_EXTERNAL_RUNNER_FINISHED, &NashMonitorDialog::OnRunnerFinished, this); m_stopButton->Bind(wxEVT_BUTTON, &NashMonitorDialog::OnStop, this); Bind(wxEVT_CLOSE_WINDOW, &NashMonitorDialog::OnClose, this); @@ -306,7 +372,7 @@ void NashMonitorDialog::Start(const std::shared_ptr &p_command) m_doc->GetGame()->Write(s, "nfg"); } - m_runner = std::make_unique(this); + m_runner = std::make_unique(this, m_doc->GetGame(), *m_output); switch (const auto result = m_runner->Start(p_command->GetCommand(), wxString(s.str().c_str(), *wxConvCurrent))) { case ExternalProcessRunner::RunnerStartResult::Ok: @@ -324,15 +390,14 @@ void NashMonitorDialog::Start(const std::shared_ptr &p_command) } } -void NashMonitorDialog::OnRunnerLine(wxThreadEvent &p_event) +void NashMonitorDialog::OnRunnerProfile(wxThreadEvent &p_event) { - const wxString msg = p_event.GetString(); - if (!msg.empty()) { - m_doc->DoAddOutput(*m_output, msg); - wxString label; - label << wxT("Number of equilibria found so far: ") << m_output->NumProfiles(); - m_countText->SetLabel(label); - } + const auto &profile = p_event.GetPayload(); + std::visit([this](const auto &p) { AddProfile(*m_output, p); }, profile); + m_doc->DoAnalysisOutputChanged(); + wxString label; + label << wxT("Number of equilibria found so far: ") << m_output->NumProfiles(); + m_countText->SetLabel(label); } void NashMonitorDialog::OnRunnerFinished(wxThreadEvent &p_event) diff --git a/src/gui/gamedoc.cc b/src/gui/gamedoc.cc index 401072d275..e4eb781ead 100644 --- a/src/gui/gamedoc.cc +++ b/src/gui/gamedoc.cc @@ -397,11 +397,7 @@ void GameDocument::DoAddEquilibriumOutput(std::shared_ptr p_prof NotifyChanged(GameModificationType::Workspace); } -void GameDocument::DoAddOutput(AnalysisOutput &p_list, const wxString &p_output) -{ - p_list.AddOutput(p_output); - NotifyChanged(GameModificationType::Workspace); -} +void GameDocument::DoAnalysisOutputChanged() { NotifyChanged(GameModificationType::Workspace); } void GameDocument::DoSelectEquilibriumOutput(int p_index) { diff --git a/src/gui/gamedoc.h b/src/gui/gamedoc.h index b9dbb1dd2f..164128b67d 100644 --- a/src/gui/gamedoc.h +++ b/src/gui/gamedoc.h @@ -326,7 +326,7 @@ class GameDocument { void DoCopyOutcome(GameNode p_node, GameOutcome p_outcome); void DoSetPayoff(GameOutcome p_outcome, int p_player, const wxString &p_value); - void DoAddOutput(AnalysisOutput &p_list, const wxString &p_output); + void DoAnalysisOutputChanged(); }; inline GameDocument *NewTreeDocument()