From da0669d9883d8510e4d2a83b5b61f250cae3c2e5 Mon Sep 17 00:00:00 2001 From: Axel Kohlmeyer Date: Thu, 27 Jul 2023 02:56:07 -0400 Subject: [PATCH] implement running LAMMPS asynchonously in a separate thread --- tools/lammps-gui/CMakeLists.txt | 2 + tools/lammps-gui/TODO.md | 3 +- tools/lammps-gui/lammpsgui.cpp | 213 ++++++++++++++++++++---------- tools/lammps-gui/lammpsgui.h | 12 +- tools/lammps-gui/lammpsrunner.cpp | 44 ++++++ tools/lammps-gui/lammpsrunner.h | 48 +++++++ tools/lammps-gui/stdcapture.cpp | 28 +++- tools/lammps-gui/stdcapture.h | 6 +- 8 files changed, 279 insertions(+), 77 deletions(-) create mode 100644 tools/lammps-gui/lammpsrunner.cpp create mode 100644 tools/lammps-gui/lammpsrunner.h diff --git a/tools/lammps-gui/CMakeLists.txt b/tools/lammps-gui/CMakeLists.txt index 5b0bc0c533..ef68470c94 100644 --- a/tools/lammps-gui/CMakeLists.txt +++ b/tools/lammps-gui/CMakeLists.txt @@ -69,6 +69,8 @@ set(PROJECT_SOURCES lammpsgui.cpp lammpsgui.h lammpsgui.ui + lammpsrunner.cpp + lammpsrunner.h stdcapture.cpp ${PLUGIN_LOADER_SRC} ) diff --git a/tools/lammps-gui/TODO.md b/tools/lammps-gui/TODO.md index a589592464..b01df689e3 100644 --- a/tools/lammps-gui/TODO.md +++ b/tools/lammps-gui/TODO.md @@ -7,7 +7,7 @@ LAMMPS-GUI TODO list: - add "Help" entry to menu bar. Should open a popup window with a one page description of how to use it. Use HTML or Markdown text. - add "View" entry to menu bar, where dialog windows may be enabled/disabled (e.g. Render) - add dialog when exiting asking if file should be saved when it is modified -- add CTRL-q hotkey to log windows so you can exit the entire application +- add CTRL-q hotkey to log windows so you can exit the entire application (add do you really want to? dialog to this) - add "render" dialog where a "write_dump image" can be triggered. dialog should offer options for size, zoom, rotation, colors(?) - add "syntax check" with enabled "-skiprun" flag. - add settings dialog where certain properties can be set through customizing the LAMMPS command line @@ -19,7 +19,6 @@ LAMMPS-GUI TODO list: # Long term ideas - support single stepping, i.e. process input line by line (need to detect continuation chars!) with highlighting active line(s) -- run LAMMMPS instance in a separate thread, modify capture to regularly poll pipe and retrieve/append output while job is running - have command text input file in/above status bar where individual commands can be tested. have insert button to copy line into file at the current point - support text completion as done with lammps-shell - have context menu for known commands to offer retrieving help by dispatching URL to webbrowser (process index from sphinx for that purpose) diff --git a/tools/lammps-gui/lammpsgui.cpp b/tools/lammps-gui/lammpsgui.cpp index 742211e07d..a3a1039308 100644 --- a/tools/lammps-gui/lammpsgui.cpp +++ b/tools/lammps-gui/lammpsgui.cpp @@ -12,18 +12,24 @@ ------------------------------------------------------------------------- */ #include "lammpsgui.h" + #include "highlighter.h" +#include "lammpsrunner.h" #include "stdcapture.h" #include "ui_lammpsgui.h" -//#include #include #include #include +#include #include #include +#include #include +#include #include +#include +#include #include #if defined(LAMMPS_GUI_USE_PLUGIN) @@ -33,7 +39,9 @@ #endif LammpsGui::LammpsGui(QWidget *parent, const char *filename) : - QMainWindow(parent), ui(new Ui::LammpsGui), lammps_handle(nullptr), plugin_handle(nullptr) + QMainWindow(parent), ui(new Ui::LammpsGui), highlighter(nullptr), capturer(nullptr), + status(nullptr), logwindow(nullptr), logupdater(nullptr), progress(nullptr), + lammps_handle(nullptr), plugin_handle(nullptr), is_running(false) { ui->setupUi(this); this->setCentralWidget(ui->textEdit); @@ -74,17 +82,21 @@ LammpsGui::LammpsGui(QWidget *parent, const char *filename) : status = new QLabel("Ready."); status->setFixedWidth(300); ui->statusbar->addWidget(status); + progress = new QProgressBar(); + progress->setRange(0, 1000); + progress->setFixedWidth(500); + ui->statusbar->addWidget(progress); #if defined(LAMMPS_GUI_USE_PLUGIN) - liblammpsplugin_t *plugin = liblammpsplugin_load("liblammps.so"); - if (!plugin) plugin = liblammpsplugin_load("liblammps.dylib"); - if (!plugin) plugin = liblammpsplugin_load("liblammps.dll"); - bool do_exit = !plugin || (plugin && plugin->abiversion != LAMMPSPLUGIN_ABI_VERSION); - if (!plugin) QMessageBox::critical(this, "Warning", "Cannot open LAMMPS shared library file"); - if (plugin && (plugin->abiversion != LAMMPSPLUGIN_ABI_VERSION)) + liblammpsplugin_t *lammps = liblammpsplugin_load("liblammps.so"); + if (!lammps) lammps = liblammpsplugin_load("liblammps.dylib"); + if (!lammps) lammps = liblammpsplugin_load("liblammps.dll"); + bool do_exit = !lammps || (lammps && lammps->abiversion != LAMMPSPLUGIN_ABI_VERSION); + if (!lammps) QMessageBox::critical(this, "Warning", "Cannot open LAMMPS shared library file"); + if (lammps && (lammps->abiversion != LAMMPSPLUGIN_ABI_VERSION)) QMessageBox::critical(this, "Warning", "ERROR: LAMMPS lib plugin ABI version does not match"); - plugin_handle = plugin; + plugin_handle = lammps; if (do_exit) exit(1); #endif } @@ -95,6 +107,7 @@ LammpsGui::~LammpsGui() delete highlighter; delete capturer; delete status; + delete logwindow; } void LammpsGui::new_document() @@ -176,11 +189,11 @@ void LammpsGui::quit() { #if defined(LAMMPS_GUI_USE_PLUGIN) if (lammps_handle) { - liblammpsplugin_t *plugin = (liblammpsplugin_t *)plugin_handle; - plugin->close(lammps_handle); - plugin->mpi_finalize(); - plugin->kokkos_finalize(); - plugin->python_finalize(); + liblammpsplugin_t *lammps = (liblammpsplugin_t *)plugin_handle; + lammps->close(lammps_handle); + lammps->mpi_finalize(); + lammps->kokkos_finalize(); + lammps->python_finalize(); } #else if (lammps_handle) { @@ -224,6 +237,79 @@ void LammpsGui::redo() ui->textEdit->redo(); } +void LammpsGui::logupdate() +{ + double t_elapsed, t_remain, t_total; + int completed = 1000; + + if (is_running) { +#if defined(LAMMPS_GUI_USE_PLUGIN) + liblammpsplugin_t *lammps = (liblammpsplugin_t *)plugin_handle; + if (lammps->is_running(lammps_handle)) { + t_elapsed = lammps->get_thermo(lammps_handle, "cpu"); + t_remain = lammps->get_thermo(lammps_handle, "cpuremain"); + t_total = t_elapsed + t_remain + 1.0e-10; + completed = t_elapsed / t_total * 1000.0; + } +#else + if (lammps_is_running(lammps_handle)) { + t_elapsed = lammps_get_thermo(lammps_handle, "cpu"); + t_remain = lammps_get_thermo(lammps_handle, "cpuremain"); + t_total = t_elapsed + t_remain + 1.0e-10; + completed = t_elapsed / t_total * 1000.0; + } +#endif + } + progress->setValue(completed); + if (logwindow) { + const auto text = capturer->GetChunk(); + if (text.size() > 0) { + logwindow->insertPlainText(text.c_str()); + logwindow->moveCursor(QTextCursor::End); + logwindow->textCursor().deleteChar(); + } + } +} + +void LammpsGui::run_done() +{ + logupdater->stop(); + delete logupdater; + logupdater = nullptr; + progress->setValue(1000); + + capturer->EndCapture(); + auto log = capturer->GetCapture(); + logwindow->insertPlainText(log.c_str()); + logwindow->moveCursor(QTextCursor::End); + + bool success = true; + constexpr int BUFLEN = 1024; + char errorbuf[BUFLEN]; + +#if defined(LAMMPS_GUI_USE_PLUGIN) + liblammpsplugin_t *lammps = (liblammpsplugin_t *)plugin_handle; + if (lammps->has_error(lammps_handle)) { + lammps->get_last_error_message(lammps_handle, errorbuf, BUFLEN); + success = false; + } +#else + if (lammps_has_error(lammps_handle)) { + lammps_get_last_error_message(lammps_handle, errorbuf, BUFLEN); + success = false; + } +#endif + + if (success) { + status->setText("Ready."); + } else { + status->setText("Failed."); + QMessageBox::critical(this, "LAMMPS-GUI Error", + QString("Error running LAMMPS:\n\n") + errorbuf); + } + is_running = false; +} + void LammpsGui::run_buffer() { status->setText("Running LAMMPS. Please wait..."); @@ -232,51 +318,38 @@ void LammpsGui::run_buffer() if (!lammps_handle) return; clear(); capturer->BeginCapture(); - std::string buffer = ui->textEdit->toPlainText().toStdString(); -#if defined(LAMMPS_GUI_USE_PLUGIN) - liblammpsplugin_t *plugin = (liblammpsplugin_t *)plugin_handle; - plugin->commands_string(lammps_handle, buffer.c_str()); -#else - lammps_commands_string(lammps_handle, buffer.c_str()); -#endif - capturer->EndCapture(); - auto log = capturer->GetCapture(); - status->setText("Ready."); - auto box = new QPlainTextEdit(); - box->document()->setPlainText(log.c_str()); - box->setReadOnly(true); - box->setWindowTitle("LAMMPS-GUI - Output from running LAMMPS on buffer - " + current_file); + std::string buffer = ui->textEdit->toPlainText().toStdString(); + char *input = new char[buffer.size() + 1]; + memcpy(input, buffer.c_str(), buffer.size() + 1); + + is_running = true; + LammpsRunner *runner = new LammpsRunner(this); + runner->setup_run(lammps_handle, input, plugin_handle); + connect(runner, &LammpsRunner::resultReady, this, &LammpsGui::run_done); + connect(runner, &LammpsRunner::finished, runner, &QObject::deleteLater); + runner->start(); + + logwindow = new QPlainTextEdit(); + logwindow->setReadOnly(true); + logwindow->setCenterOnScroll(true); + logwindow->moveCursor(QTextCursor::End); + logwindow->setWindowTitle("LAMMPS-GUI - Output from running LAMMPS on buffer - " + + current_file); QFont text_font; text_font.setFamilies(QStringList({"Consolas", "Monospace", "Sans", "Courier"})); text_font.setFixedPitch(true); text_font.setStyleHint(QFont::TypeWriter); - box->document()->setDefaultFont(text_font); - box->setLineWrapMode(QPlainTextEdit::NoWrap); - box->setMinimumSize(800, 600); - QShortcut *shortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), box); - QObject::connect(shortcut, &QShortcut::activated, box, &QPlainTextEdit::close); - box->show(); + logwindow->document()->setDefaultFont(text_font); + logwindow->setLineWrapMode(QPlainTextEdit::NoWrap); + logwindow->setMinimumSize(800, 600); + QShortcut *shortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), logwindow); + QObject::connect(shortcut, &QShortcut::activated, logwindow, &QPlainTextEdit::close); + logwindow->show(); -#if defined(LAMMPS_GUI_USE_PLUGIN) - if (plugin->has_error(lammps_handle)) { - constexpr int BUFLEN = 1024; - char errorbuf[BUFLEN]; - plugin->get_last_error_message(lammps_handle, errorbuf, BUFLEN); - - QMessageBox::critical(this, "LAMMPS-GUI Error", - QString("Error running LAMMPS:\n\n") + errorbuf); - } -#else - if (lammps_has_error(lammps_handle)) { - constexpr int BUFLEN = 1024; - char errorbuf[BUFLEN]; - lammps_get_last_error_message(lammps_handle, errorbuf, BUFLEN); - - QMessageBox::critical(this, "LAMMPS-GUI Error", - QString("Error running LAMMPS:\n\n") + errorbuf); - } -#endif + logupdater = new QTimer(this); + connect(logupdater, &QTimer::timeout, this, &LammpsGui::logupdate); + logupdater->start(1000); } void LammpsGui::clear() @@ -293,23 +366,29 @@ void LammpsGui::clear() void LammpsGui::about() { - start_lammps(); - std::string version = "This is LAMMPS-GUI version 0.1"; - capturer->BeginCapture(); + std::string info = "LAMMPS is currently running. LAMMPS config info not available."; + + // LAMMPS is not re-entrant, so we can only query LAMMPS when it is not running + if (!is_running) { + start_lammps(); + capturer->BeginCapture(); #if defined(LAMMPS_GUI_USE_PLUGIN) - ((liblammpsplugin_t *)plugin_handle)->commands_string(lammps_handle, "info config"); + ((liblammpsplugin_t *)plugin_handle)->commands_string(lammps_handle, "info config"); #else - lammps_commands_string(lammps_handle, "info config"); + lammps_commands_string(lammps_handle, "info config"); #endif - capturer->EndCapture(); - std::string info = capturer->GetCapture(); - auto start = info.find("LAMMPS version:"); - auto end = info.find("Info-Info-Info", start); + capturer->EndCapture(); + info = capturer->GetCapture(); + auto start = info.find("LAMMPS version:"); + auto end = info.find("Info-Info-Info", start); + info = std::string(info, start, end - start); + } + QMessageBox msg; msg.setWindowTitle("About LAMMPS-GUI"); msg.setText(version.c_str()); - msg.setInformativeText(std::string(info, start, end - start).c_str()); + msg.setInformativeText(info.c_str()); msg.setIcon(QMessageBox::NoIcon); msg.setStandardButtons(QMessageBox::Ok); QFont font; @@ -328,14 +407,14 @@ void LammpsGui::start_lammps() int nargs = sizeof(args) / sizeof(char *); #if defined(LAMMPS_GUI_USE_PLUGIN) - liblammpsplugin_t *plugin = (liblammpsplugin_t *)plugin_handle; + liblammpsplugin_t *lammps = (liblammpsplugin_t *)plugin_handle; // Create LAMMPS instance if not already present - if (!lammps_handle) lammps_handle = plugin->open_no_mpi(nargs, args, nullptr); - if (plugin->has_error(lammps_handle)) { + if (!lammps_handle) lammps_handle = lammps->open_no_mpi(nargs, args, nullptr); + if (lammps->has_error(lammps_handle)) { constexpr int BUFLEN = 1024; char errorbuf[BUFLEN]; - plugin->get_last_error_message(lammps_handle, errorbuf, BUFLEN); + lammps->get_last_error_message(lammps_handle, errorbuf, BUFLEN); QMessageBox::critical(this, "LAMMPS-GUI Error", QString("Error launching LAMMPS:\n\n") + errorbuf); diff --git a/tools/lammps-gui/lammpsgui.h b/tools/lammps-gui/lammpsgui.h index 875b9c304c..c6e768ffed 100644 --- a/tools/lammps-gui/lammpsgui.h +++ b/tools/lammps-gui/lammpsgui.h @@ -16,7 +16,6 @@ #include #include -#include // forward declarations @@ -26,6 +25,11 @@ class LammpsGui; } QT_END_NAMESPACE +class QLabel; +class QPlainTextEdit; +class QProgressBar; +class QTimer; + class Highlighter; class StdCapture; @@ -40,6 +44,7 @@ protected: void open_file(const QString &filename); void write_file(const QString &filename); void start_lammps(); + void run_done(); private slots: void new_document(); @@ -55,17 +60,22 @@ private slots: void clear(); void run_buffer(); void about(); + void logupdate(); private: Ui::LammpsGui *ui; Highlighter *highlighter; StdCapture *capturer; QLabel *status; + QPlainTextEdit *logwindow; + QTimer *logupdater; + QProgressBar *progress; QString current_file; QString current_dir; void *lammps_handle; void *plugin_handle; + bool is_running; }; #endif // LAMMPSGUI_H diff --git a/tools/lammps-gui/lammpsrunner.cpp b/tools/lammps-gui/lammpsrunner.cpp new file mode 100644 index 0000000000..1b078d67bf --- /dev/null +++ b/tools/lammps-gui/lammpsrunner.cpp @@ -0,0 +1,44 @@ +/* ---------------------------------------------------------------------- + LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator + https://www.lammps.org/, Sandia National Laboratories + LAMMPS development team: developers@lammps.org + + Copyright (2003) Sandia Corporation. Under the terms of Contract + DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains + certain rights in this software. This software is distributed under + the GNU General Public License. + + See the README file in the top-level LAMMPS directory. + ------------------------------------------------------------------------- */ + +#include "lammpsrunner.h" + +#if defined(LAMMPS_GUI_USE_PLUGIN) +#include "liblammpsplugin.h" +#else +#include "library.h" +#endif + +#include + +LammpsRunner::LammpsRunner(QObject *parent) : + QThread(parent), handle(nullptr), plugin(nullptr), input(nullptr) +{ +} + +void LammpsRunner::run() +{ + if (handle) { +#if defined(LAMMPS_GUI_USE_PLUGIN) + liblammpsplugin_t *lammps = (liblammpsplugin_t *)plugin; + lammps->commands_string(handle, input); +#else + lammps_commands_string(handle, input); +#endif + } + emit resultReady(); +} + +// Local Variables: +// c-basic-offset: 4 +// End: diff --git a/tools/lammps-gui/lammpsrunner.h b/tools/lammps-gui/lammpsrunner.h new file mode 100644 index 0000000000..57c20226e8 --- /dev/null +++ b/tools/lammps-gui/lammpsrunner.h @@ -0,0 +1,48 @@ +/* -*- c++ -*- ---------------------------------------------------------- + LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator + https://www.lammps.org/, Sandia National Laboratories + LAMMPS development team: developers@lammps.org + + Copyright (2003) Sandia Corporation. Under the terms of Contract + DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains + certain rights in this software. This software is distributed under + the GNU General Public License. + + See the README file in the top-level LAMMPS directory. +------------------------------------------------------------------------- */ + +#ifndef LAMMPSRUNNER_H +#define LAMMPSRUNNER_H + +#include + +class LammpsRunner : public QThread { + Q_OBJECT + +public: + LammpsRunner(QObject *parent = nullptr); + ~LammpsRunner() = default; + + // execute LAMMPS run in runner thread + void run() override; + + // transfer info to worker thread + void setup_run(void *_handle, const char *_input, void *_plugin = nullptr) + { + handle = _handle; + plugin = _plugin; + input = _input; + } + +signals: + void resultReady(); + +private: + void *handle, *plugin; + const char *input; +}; + +#endif +// Local Variables: +// c-basic-offset: 4 +// End: diff --git a/tools/lammps-gui/stdcapture.cpp b/tools/lammps-gui/stdcapture.cpp index 33c7ee9ef3..c1fac5dc3d 100644 --- a/tools/lammps-gui/stdcapture.cpp +++ b/tools/lammps-gui/stdcapture.cpp @@ -24,7 +24,6 @@ #define dup2 _dup2 #define fileno _fileno #define close _close -#define pipe _pipe #define read _read #define eof _eof #else @@ -44,9 +43,9 @@ StdCapture::StdCapture() : m_oldStdOut(0), m_capturing(false) m_pipe[READ] = 0; m_pipe[WRITE] = 0; #if _WIN32 - if (pipe(m_pipe, 65536, O_BINARY) == -1) return; + if (_pipe(m_pipe, 65536, O_BINARY) == -1) return; #else - if (pipe(m_pipe) == -1) return; + if (pipe2(m_pipe, O_NONBLOCK) == -1) return; #endif m_oldStdOut = dup(fileno(stdout)); if (m_oldStdOut == -1) return; @@ -75,8 +74,6 @@ bool StdCapture::EndCapture() dup2(m_oldStdOut, fileno(stdout)); m_captured.clear(); - constexpr int bufSize = 1025; - char buf[bufSize]; int bytesRead; bool fd_blocked; @@ -104,7 +101,26 @@ bool StdCapture::EndCapture() return true; } -std::string StdCapture::GetCapture() const +std::string StdCapture::GetChunk() +{ + if (!m_capturing) return std::string(); + int bytesRead = 0; + buf[0] = '\0'; + +#ifdef _WIN32 + if (!eof(m_pipe[READ])) { + bytesRead = read(m_pipe[READ], buf, bufSize - 1); + } +#else + bytesRead = read(m_pipe[READ], buf, bufSize - 1); +#endif + if (bytesRead > 0) { + buf[bytesRead] = '\0'; + } + return std::string(buf); +} + +std::string StdCapture::GetCapture() { std::string::size_type idx = m_captured.find_last_not_of("\r\n"); if (idx == std::string::npos) { diff --git a/tools/lammps-gui/stdcapture.h b/tools/lammps-gui/stdcapture.h index 2afd494a36..ee8bb44dd3 100644 --- a/tools/lammps-gui/stdcapture.h +++ b/tools/lammps-gui/stdcapture.h @@ -23,7 +23,8 @@ public: void BeginCapture(); bool EndCapture(); - std::string GetCapture() const; + std::string GetCapture(); + std::string GetChunk(); private: enum PIPES { READ, WRITE }; @@ -32,6 +33,9 @@ private: bool m_capturing; bool m_init; std::string m_captured; + + static constexpr int bufSize = 1025; + char buf[bufSize]; }; #endif