From 8ba556f86aea7f2a05c6e02b6b2428be699f804b Mon Sep 17 00:00:00 2001 From: Jack Bennett Date: Sun, 21 Apr 2024 01:47:25 +0100 Subject: [PATCH] IPC between texedit and tecomp with piped+async subprocesses IO redirection from tecomp for texedit to recieve its output while it's run in the background --- .tokeignore | 2 +- texedit/CMakeLists.txt | 4 + texedit/gui/main_frame.cpp | 14 ++- texedit/gui/main_frame.hpp | 8 ++ texedit/main.cpp | 41 ++++++--- texedit/process/compiler/tecomp_proc.cpp | 30 +++++++ texedit/process/compiler/tecomp_proc.hpp | 33 +++++++ texedit/process/process.cpp | 45 ++++++++++ texedit/process/process.hpp | 43 +++++++++ texedit/process/process_mgr.cpp | 109 +++++++++++++++++++++++ texedit/process/process_mgr.hpp | 41 +++++++++ texedit/util/except.hpp | 9 +- texedit/util/resources.cpp | 38 ++++++++ texedit/util/resources.hpp | 24 +++++ 14 files changed, 421 insertions(+), 20 deletions(-) create mode 100644 texedit/process/compiler/tecomp_proc.cpp create mode 100644 texedit/process/compiler/tecomp_proc.hpp create mode 100644 texedit/process/process.cpp create mode 100644 texedit/process/process.hpp create mode 100644 texedit/process/process_mgr.cpp create mode 100644 texedit/process/process_mgr.hpp create mode 100644 texedit/util/resources.cpp create mode 100644 texedit/util/resources.hpp diff --git a/.tokeignore b/.tokeignore index c1e740b..6473235 100644 --- a/.tokeignore +++ b/.tokeignore @@ -1,5 +1,5 @@ vendor/ -tests/ +examples/ deps/ docs/ build/ diff --git a/texedit/CMakeLists.txt b/texedit/CMakeLists.txt index 68f68c0..eb6e50d 100644 --- a/texedit/CMakeLists.txt +++ b/texedit/CMakeLists.txt @@ -30,7 +30,11 @@ set(SRCS "gui/main_frame.cpp" "gui/preview_panel.cpp" "gui/prog_info.cpp" + "process/compiler/tecomp_proc.cpp" + "process/process_mgr.cpp" + "process/process.cpp" "util/log.cpp" + "util/resources.cpp" "main.cpp" ) diff --git a/texedit/gui/main_frame.cpp b/texedit/gui/main_frame.cpp index 86d73e5..0f71ada 100644 --- a/texedit/gui/main_frame.cpp +++ b/texedit/gui/main_frame.cpp @@ -7,8 +7,10 @@ #include "main_frame.hpp" +#include "process/process.hpp" #include "util/log.hpp" #include "util/except.hpp" +#include "util/resources.hpp" #include "command_ids.hpp" #include "editor_panel.hpp" #include "preview_panel.hpp" @@ -18,7 +20,9 @@ #include namespace te { - MainFrame::MainFrame() : wxFrame(nullptr, wxID_ANY, "TexEdit", wxDefaultPosition, wxSize{1024, 640}) { + MainFrame::MainFrame() : wxFrame{nullptr, wxID_ANY, "TexEdit", wxDefaultPosition, wxSize{1024, 640}}, _proc_mgr{this}, _tecomp{_proc_mgr} { + _tecomp.Start(); + BuildMenuBar(); BuildSplitLayout(); } @@ -64,6 +68,13 @@ namespace te { SetMenuBar(menuBar); } + void MainFrame::OnIdle(wxIdleEvent &ev) { + wxString s = _proc_mgr.PollPipedOutput(); + if (!s.IsEmpty()) { + std::cout << s; + } + } + void MainFrame::ShowURL(const std::string &url) { if (!wxLaunchDefaultBrowser(url)) { util::log::Error("Failed to open URL \"" + url + "\""); @@ -96,6 +107,7 @@ namespace te { } wxBEGIN_EVENT_TABLE(MainFrame, wxFrame) + EVT_IDLE(MainFrame::OnIdle) EVT_MENU(wxID_EXIT, MainFrame::OnMenuQuit) EVT_MENU(wxID_ABOUT, MainFrame::OnMenuAbout) EVT_MENU(cmds::Menu_URLSourcePage, MainFrame::OnMenuURLSourcePage) diff --git a/texedit/gui/main_frame.hpp b/texedit/gui/main_frame.hpp index 2a5997d..bca5d63 100644 --- a/texedit/gui/main_frame.hpp +++ b/texedit/gui/main_frame.hpp @@ -11,15 +11,23 @@ #include +#include "process/process_mgr.hpp" +#include "process/compiler/tecomp_proc.hpp" + namespace te { class MainFrame : public wxFrame { public: MainFrame(); private: + ProcessManager _proc_mgr; + TECompProcess _tecomp; + void BuildSplitLayout(); void BuildMenuBar(); + void OnIdle(wxIdleEvent &ev); + void ShowURL(const std::string &url); void OnMenuAbout(wxCommandEvent &event); diff --git a/texedit/main.cpp b/texedit/main.cpp index 140393a..1322cde 100644 --- a/texedit/main.cpp +++ b/texedit/main.cpp @@ -14,27 +14,44 @@ wxIMPLEMENT_APP(te::Application); namespace te { bool Application::OnExceptionInMainLoop() { + wxString what; try { throw; } catch (const std::exception &e) { - util::log::Fatal(e.what()); - - if (wxMessageBox("An unexpected exception has occurred:\n" - "\"" + std::string{e.what()} + "\"\n\n" - "TexEdit can attempt to keep running so you can save your data. Do you want to try?", - "Fatal Error", wxYES | wxNO | wxICON_ERROR) - == wxYES) { - util::log::Warn("Attempting to continue execution following a potentially fatal exception"); - return true; - } + what = e.what(); + } catch (...) { + what = "Unknown runtime error"; + } + + util::log::Fatal(what.ToStdString()); + + if (wxMessageBox("An unexpected exception has occurred:\n" + "\"" + what + "\"\n\n" + "TexEdit can attempt to keep running so you can save your data. Do you want to try?", + "Fatal Error", wxYES | wxNO | wxICON_ERROR) + == wxYES) { + util::log::Warn("Attempting to continue execution following a potentially fatal exception"); + return true; } return false; } void Application::OnUnhandledException() { - wxMessageBox("An unhandled exception has occurred and TexEdit cannot recover.\n" - "The program will now terminate.", + wxString what; + try { + throw; + } catch (const std::exception &e) { + what = e.what(); + } catch (...) { + what = "Unknown runtime error"; + } + + util::log::Fatal("(unhandled) " + what.ToStdString()); + + wxMessageBox("An unexpected exception has occurred:\n" + "\"" + what + "\"\n\n" + "This was unhandled and TexEdit cannot recover. The program will now terminate.", "Unhandled Exception", wxOK | wxICON_ERROR); } diff --git a/texedit/process/compiler/tecomp_proc.cpp b/texedit/process/compiler/tecomp_proc.cpp new file mode 100644 index 0000000..dbb3f84 --- /dev/null +++ b/texedit/process/compiler/tecomp_proc.cpp @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Jack Bennett. + * All Rights Reserved. + * + * See the LICENCE file for more information. + */ + +#include "tecomp_proc.hpp" + +#include "util/except.hpp" +#include "util/resources.hpp" + +namespace te { + TECompProcess::TECompProcess(ProcessManager &mgr) : _mgr{mgr}, _id{1} { + _cmd = util::res::RelToExec("tecomp"); + if (!util::res::ValidateExecutable(_cmd.ToStdString())) { + throw util::except::MissingComponentException("tecomp"); + } + } + + void TECompProcess::Start() { + const char *const argv[] = { + _cmd.ToUTF8(), + "watch", "examples", "examples/HelloWorld.tex", + 0 + }; + + _mgr.ExecutePipedAsync(_id, argv); + } +} diff --git a/texedit/process/compiler/tecomp_proc.hpp b/texedit/process/compiler/tecomp_proc.hpp new file mode 100644 index 0000000..5f9a956 --- /dev/null +++ b/texedit/process/compiler/tecomp_proc.hpp @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 Jack Bennett. + * All Rights Reserved. + * + * See the LICENCE file for more information. + */ + +#pragma once +#ifndef __texedit__tecomp_proc_hpp__ +#define __texedit__tecomp_proc_hpp__ + +#include + +#include "process/process_mgr.hpp" + +namespace te { + class TECompProcess { + public: + TECompProcess(ProcessManager &mgr); + + void Start(); + + inline int GetID() { return _id; } + + private: + ProcessManager &_mgr; + wxString _cmd; + + int _id; + }; +} + +#endif diff --git a/texedit/process/process.cpp b/texedit/process/process.cpp new file mode 100644 index 0000000..f32f1ea --- /dev/null +++ b/texedit/process/process.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024 Jack Bennett. + * All Rights Reserved. + * + * See the LICENCE file for more information. + */ + +#include "process.hpp" + +#include "process_mgr.hpp" + +#include +#include + +namespace te { + Process::Process(wxEvtHandler *parent, ProcessManager *mgr) : wxProcess(parent), _mgr{mgr} { + } + + void Process::OnTerminate(int pid, int status) { + _mgr->HandleProcessTerminated(this, pid, status); + } + + PipedProcess::PipedProcess(wxEvtHandler *parent, ProcessManager *mgr) : Process(parent, mgr) { + Redirect(); + } + + wxString PipedProcess::ReadLineStdout() { + wxString r{""}; + + if (IsInputAvailable()) { + wxTextInputStream tis(*GetInputStream()); + r << _cmd << " (stdout): " << tis.ReadLine() << "\n"; + } + if (IsErrorAvailable()) { + wxTextInputStream tis(*GetErrorStream()); + r << _cmd << " (stderr): " << tis.ReadLine() << "\n"; + } + + return r; + } + + void PipedProcess::OnTerminate(int pid, int status) { + _mgr->HandlePipedProcessTerminated(this, pid, status); + } +} diff --git a/texedit/process/process.hpp b/texedit/process/process.hpp new file mode 100644 index 0000000..efc9632 --- /dev/null +++ b/texedit/process/process.hpp @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Jack Bennett. + * All Rights Reserved. + * + * See the LICENCE file for more information. + */ + +#pragma once +#ifndef __texedit__process_mgr_hpp__ +#define __texedit__process_mgr_hpp__ + +#include + +#include + +namespace te { + class ProcessManager; + + class Process : public wxProcess { + public: + Process(wxEvtHandler *parent, ProcessManager *mgr); + + virtual void OnTerminate(int pid, int status) override; + + inline void SetCmd(const wxString &cmd) { _cmd = cmd; } + inline wxString GetCmd() { return _cmd; } + + protected: + ProcessManager *_mgr; + wxString _cmd{}; + }; + + class PipedProcess : public Process { + public: + PipedProcess(wxEvtHandler *parent, ProcessManager *mgr); + + wxString ReadLineStdout(); + + virtual void OnTerminate(int pid, int status) override; + }; +} + +#endif diff --git a/texedit/process/process_mgr.cpp b/texedit/process/process_mgr.cpp new file mode 100644 index 0000000..52d094a --- /dev/null +++ b/texedit/process/process_mgr.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2024 Jack Bennett. + * All Rights Reserved. + * + * See the LICENCE file for more information. + */ + +#include "process_mgr.hpp" + +#include "util/log.hpp" + +namespace te { + ProcessManager::ProcessManager(wxEvtHandler *cmd_parent) : _cmd_parent{cmd_parent} { + } + + ProcessManager::~ProcessManager() { + // stop and delete all processes if they are still running when the process manager is destroyed + for (auto p : _all_async) { + delete p.second; + } + } + + void ProcessManager::ExecuteAsync(int id, const char *const *argv) { + Process *proc = new Process(_cmd_parent, this); + proc->SetCmd(argv[0]); + + // make a string with the given arguments so it can be logged + wxString cmdstr; + for (int i = 0; argv[i] != 0; i++) + cmdstr << argv[i] << " "; + util::log::Info("Executing command: " + cmdstr.ToStdString()); + + wxExecute(argv, wxEXEC_ASYNC, proc); + + _all_async.emplace(id, proc); + } + + void ProcessManager::ExecutePipedAsync(int id, const char *const *argv) { + PipedProcess *piproc = new PipedProcess(_cmd_parent, this); + piproc->SetCmd(argv[0]); + + // make a string with the given arguments so it can be logged + wxString cmdstr; + for (int i = 0; argv[i] != 0; i++) + cmdstr << argv[i] << " "; + util::log::Info("Executing command: " + cmdstr.ToStdString()); + + wxExecute(argv, wxEXEC_ASYNC, piproc); + + _piped_running.emplace(id, piproc); + _all_async.emplace(id, piproc); + } + + wxString ProcessManager::PollPipedOutput(int id) { + wxString r{""}; + + // merge all output if no id was specified + if (id == wxINT32_MAX) { + for (auto p : _piped_running) { + r << p.second->ReadLineStdout(); + } + + return r; + } + + // otherwise just get output of process with given id + if (_piped_running.find(id) != _piped_running.end()) { + r << _piped_running.at(id)->ReadLineStdout(); + } + + return r; + } + + void ProcessManager::HandleProcessTerminated(Process *p, int pid, int status) { + util::log::Info("Process " + std::to_string(pid) + " (" + p->GetCmd().ToStdString() + ") terminated with code " + std::to_string(status)); + + RemoveProcess(p); + } + + void ProcessManager::HandlePipedProcessTerminated(PipedProcess *p, int pid, int status) { + util::log::Info("Process " + std::to_string(pid) + " (" + p->GetCmd().ToStdString() + ") terminated with code " + std::to_string(status)); + + RemovePipedProcess(p); + } + + void ProcessManager::RemoveProcess(Process *p) { + auto it = _all_async.begin(); + while (it != _all_async.end()) { + if (it->second == p) { + it = _all_async.erase(it); + } else { + it++; + } + } + } + + void ProcessManager::RemovePipedProcess(PipedProcess *p) { + auto it = _piped_running.begin(); + while (it != _piped_running.end()) { + if (it->second == p) { + it = _piped_running.erase(it); + } else { + it++; + } + } + + RemoveProcess(p); + } +} diff --git a/texedit/process/process_mgr.hpp b/texedit/process/process_mgr.hpp new file mode 100644 index 0000000..9780fe9 --- /dev/null +++ b/texedit/process/process_mgr.hpp @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Jack Bennett. + * All Rights Reserved. + * + * See the LICENCE file for more information. + */ + +#pragma once +#ifndef __texedit__pdf_server_hpp__ +#define __texedit__pdf_server_hpp__ + +#include "process.hpp" + +#include + +namespace te { + class ProcessManager { + public: + ProcessManager(wxEvtHandler *cmd_parent); + ~ProcessManager(); + + void ExecuteAsync(int id, const char *const *argv); + void ExecutePipedAsync(int id, const char *const *argv); + + wxString PollPipedOutput(int id = wxINT32_MAX); + + void HandleProcessTerminated(Process *p, int pid, int status); + void HandlePipedProcessTerminated(PipedProcess *p, int pid, int status); + + private: + wxEvtHandler *_cmd_parent; + + std::unordered_map _all_async{}; + std::unordered_map _piped_running{}; + + void RemoveProcess(Process *p); + void RemovePipedProcess(PipedProcess *p); + }; +} + +#endif diff --git a/texedit/util/except.hpp b/texedit/util/except.hpp index 36efc9c..b7c68eb 100644 --- a/texedit/util/except.hpp +++ b/texedit/util/except.hpp @@ -9,14 +9,11 @@ #ifndef __texedit__except_hpp__ #define __texedit__except_hpp__ -#include - namespace te::util::except { - class NotImplementedException : public std::exception { + class MissingComponentException : public std::runtime_error { public: - inline const char *what() const noexcept override { - return "Attempted to invoke an operation that is not yet implemented."; - } + inline MissingComponentException(const std::string &name) + : std::runtime_error{"Missing " + name} {} }; } diff --git a/texedit/util/resources.cpp b/texedit/util/resources.cpp new file mode 100644 index 0000000..2ed0538 --- /dev/null +++ b/texedit/util/resources.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Jack Bennett. + * All Rights Reserved. + * + * See the LICENCE file for more information. + */ + +#include "resources.hpp" + +#include +#include + +#include "util/log.hpp" + +namespace te::util::res { + std::string GetTexeditDir() { + wxFileName f = wxStandardPaths::Get().GetExecutablePath(); + return f.GetPath().ToStdString(); + } + + std::string RelToExec(std::vector dirs, std::string filename) { + wxFileName f = wxFileName::DirName(GetTexeditDir()); + for (auto d : dirs) { + f.AppendDir(d); + } + + return f.GetPath().ToStdString() + wxString{wxFileName::GetPathSeparator()}.ToStdString() + filename; + } + + std::string RelToExec(std::string filename) { + return RelToExec({}, filename); + } + + bool ValidateExecutable(std::string path) { + wxFileName f{path}; + return f.FileExists() && f.IsFileExecutable(); + } +} diff --git a/texedit/util/resources.hpp b/texedit/util/resources.hpp new file mode 100644 index 0000000..7ee40a5 --- /dev/null +++ b/texedit/util/resources.hpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Jack Bennett. + * All Rights Reserved. + * + * See the LICENCE file for more information. + */ + +#pragma once +#ifndef __texedit__resources_hpp__ +#define __texedit__resources_hpp__ + +#include +#include + +namespace te::util::res { + std::string GetTexeditDir(); + + std::string RelToExec(std::vector dirs, std::string filename); + std::string RelToExec(std::string filename); + + bool ValidateExecutable(std::string path); +} + +#endif