diff --git a/CMakeLists.txt b/CMakeLists.txt index 44109b02..60aaa273 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,12 @@ else () message(CHECK_PASS "no") endif (ENABLE_TCP_SSL) +option(DISABLE_WEBSOCKETS "Disable WebSocket support" off) +if (DISABLE_WEBSOCKETS) + message(CHECK_START "Disable WebSocket support - enabled") + add_definitions(-DNO_WEBSOCKETS) +endif (DISABLE_WEBSOCKETS) + # Cross-compile options option(CROSS_COMPILE_ARM "Cross-compile for 32-bit ARM" off) option(CROSS_COMPILE_AARCH64 "Cross-compile for 64-bit ARM" off) @@ -192,6 +198,21 @@ if (NOT ASIO_INCLUDED) endif (WITH_ASIO) endif (NOT ASIO_INCLUDED) +if (NOT DISABLE_WEBSOCKETS) + if (NOT WEBSOCKETPP_INCLUDED) + message("-- Cloning WebSocket++") + include(FetchContent) + FetchContent_Declare( + WEBSOCKETPP + GIT_REPOSITORY https://github.com/zaphoyd/websocketpp.git + GIT_TAG 56123c87598f8b1dd471be83ca841ceae07f95ba # 0.8.2 + ) + FetchContent_MakeAvailable(WEBSOCKETPP) + add_subdirectory(${websocketpp_SOURCE_DIR} ${websocketpp_BINARY_DIR} EXCLUDE_FROM_ALL) + set(WEBSOCKETPP_INCLUDED 1) + endif (NOT WEBSOCKETPP_INCLUDED) +endif (NOT DISABLE_WEBSOCKETS) + if (ENABLE_TUI_SUPPORT AND NOT FC_INCLUDED) message("-- Cloning finalcut") include(FetchContent) diff --git a/configs/fne-sysview.example.yml b/configs/fne-sysview.example.yml index b5981129..a565a81c 100644 --- a/configs/fne-sysview.example.yml +++ b/configs/fne-sysview.example.yml @@ -2,6 +2,31 @@ # Digital Voice Modem - FNE System View # +# +# Logging Configuration (only used in WebSocket mode) +# +# Logging Levels: +# 1 - Debug +# 2 - Message +# 3 - Informational +# 4 - Warning +# 5 - Error +# 6 - Fatal +# +log: + # Console display logging level (used when in foreground). + displayLevel: 1 + # File logging level. + fileLevel: 1 + # Flag indicating file logs should be sent to syslog instead of a file. + useSyslog: false + # Full path for the directory to store the log files. + filePath: . + # Full path for the directory to store the activity log files. + activityFilePath: . + # Log filename prefix. + fileRoot: SYSVIEW + # # Radio ID ACL Configuration # @@ -58,4 +83,11 @@ fne: # Flag indicating whether or not REST API is operating in SSL mode. restSsl: false # REST API authentication password. - restPassword: "PASSWORD" \ No newline at end of file + restPassword: "PASSWORD" + +# +# WebSocket Configuration (only used in WebSocket mode) +# +websocket: + # Port number of the WebSocket should listen on. + port: 8443 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 05a05b26..d32bc131 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -96,7 +96,11 @@ if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) include(src/sysview/CMakeLists.txt) add_executable(sysview ${common_INCLUDE} ${sysView_SRC}) target_link_libraries(sysview PRIVATE common ${OPENSSL_LIBRARIES} asio::asio finalcut Threads::Threads) - target_include_directories(sysview PRIVATE ${OPENSSL_INCLUDE_DIR} src src/host src/sysview) + if (NOT DISABLE_WEBSOCKETS) + target_include_directories(sysview PRIVATE ${OPENSSL_INCLUDE_DIR} ${websocketpp_SOURCE_DIR} src src/host src/sysview) + else () + target_include_directories(sysview PRIVATE ${OPENSSL_INCLUDE_DIR} src src/host src/sysview) + endif (NOT DISABLE_WEBSOCKETS) endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) # @@ -106,7 +110,7 @@ if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) include(src/tged/CMakeLists.txt) add_executable(tged ${common_INCLUDE} ${tged_SRC}) target_link_libraries(tged PRIVATE common ${OPENSSL_LIBRARIES} asio::asio finalcut Threads::Threads) - target_include_directories(tged PRIVATE ${OPENSSL_INCLUDE_DIR} src src/host src/tged) + target_include_directories(tged PRIVATE ${OPENSSL_INCLUDE_DIR} websocketpp src src/host src/tged) endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) # diff --git a/src/sysview/HostWS.cpp b/src/sysview/HostWS.cpp new file mode 100644 index 00000000..ce9191f5 --- /dev/null +++ b/src/sysview/HostWS.cpp @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - FNE System View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +#if !defined(NO_WEBSOCKETS) +#include "Defines.h" +#include "common/Log.h" +#include "common/StopWatch.h" +#include "common/Thread.h" +#include "common/Utils.h" +#include "fne/network/RESTDefines.h" +#include "remote/RESTClient.h" +#include "HostWS.h" +#include "SysViewMain.h" + +#include +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define IDLE_WARMUP_MS 5U + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the HostWS class. */ + +HostWS::HostWS(const std::string& confFile) : + m_confFile(confFile), + m_conf(), + m_websocketPort(8443U), + m_wsServer(), + m_wsConList() +{ + /* stub */ +} + +/* Finalizes a instance of the HostWS class. */ + +HostWS::~HostWS() = default; + +/* Executes the main FNE processing loop. */ + +int HostWS::run() +{ + bool ret = false; + try { + ret = yaml::Parse(m_conf, m_confFile.c_str()); + if (!ret) { + ::fatal("cannot read the configuration file, %s\n", m_confFile.c_str()); + } + } + catch (yaml::OperationException const& e) { + ::fatal("cannot read the configuration file - %s (%s)", m_confFile.c_str(), e.message()); + } + + bool m_daemon = m_conf["daemon"].as(false); + if (m_daemon && g_foreground) + m_daemon = false; + + // re-initialize system logging + yaml::Node logConf = m_conf["log"]; + ret = ::LogInitialise(logConf["filePath"].as(), logConf["fileRoot"].as(), + logConf["fileLevel"].as(0U), logConf["displayLevel"].as(0U)); + if (!ret) { + ::fatal("unable to open the log file\n"); + } + + // handle POSIX process forking + if (m_daemon) { + // create new process + pid_t pid = ::fork(); + if (pid == -1) { + ::fprintf(stderr, "%s: Couldn't fork() , exiting\n", g_progExe.c_str()); + ::LogFinalise(); + return EXIT_FAILURE; + } + else if (pid != 0) { + ::LogFinalise(); + exit(EXIT_SUCCESS); + } + + // create new session and process group + if (::setsid() == -1) { + ::fprintf(stderr, "%s: Couldn't setsid(), exiting\n", g_progExe.c_str()); + ::LogFinalise(); + return EXIT_FAILURE; + } + + // set the working directory to the root directory + if (::chdir("/") == -1) { + ::fprintf(stderr, "%s: Couldn't cd /, exiting\n", g_progExe.c_str()); + ::LogFinalise(); + return EXIT_FAILURE; + } + + ::close(STDIN_FILENO); + ::close(STDOUT_FILENO); + ::close(STDERR_FILENO); + } + + // read base parameters from configuration + ret = readParams(); + if (!ret) + return EXIT_FAILURE; + + // setup peer network connection + ret = createPeerNetwork(); + if (!ret) + return EXIT_FAILURE; + + yaml::Node fne = g_conf["fne"]; + std::string fneRESTAddress = fne["restAddress"].as("127.0.0.1"); + uint16_t fneRESTPort = (uint16_t)fne["restPort"].as(9990U); + std::string fnePassword = fne["restPassword"].as("PASSWORD"); + bool fneSSL = fne["restSsl"].as(false); + + // initialize websockets + m_wsServer.init_asio(); + m_wsServer.set_reuse_addr(true); + m_wsServer.set_open_handler(websocketpp::lib::bind(&HostWS::wsOnConOpen, this, + websocketpp::lib::placeholders::_1)); + m_wsServer.set_close_handler(websocketpp::lib::bind(&HostWS::wsOnConClose, this, + websocketpp::lib::placeholders::_1)); + m_wsServer.set_message_handler(websocketpp::lib::bind(&HostWS::wsOnMessage, this, + websocketpp::lib::placeholders::_1, websocketpp::lib::placeholders::_2)); + + m_wsServer.set_access_channels(websocketpp::log::alevel::all); + m_wsServer.clear_access_channels(websocketpp::log::alevel::frame_payload); + + /** WebSocket Thread */ + if (!Thread::runAsThread(nullptr, threadWebSocket)) + return EXIT_FAILURE; + + ::LogInfoEx(LOG_HOST, "SysView is up and running"); + + StopWatch stopWatch; + stopWatch.start(); + + std::ostringstream logOutput; + __InternalOutputStream(logOutput); + + Timer peerListUpdate(1000U, 10U); + Timer affListUpdate(1000U, 10U); + + // main execution loop + while (!g_killed) { + uint32_t ms = stopWatch.elapsed(); + + ms = stopWatch.elapsed(); + stopWatch.start(); + + if (m_wsConList.size() > 0U) { + if (!peerListUpdate.isRunning()) { + peerListUpdate.start(); + } + + if (!affListUpdate.isRunning()) { + affListUpdate.start(); + } + + // send log messages + if (!logOutput.str().empty()) { + std::string str = logOutput.str(); + + json::object wsObj = json::object(); + std::string type = "log"; + wsObj["type"].set(type); + wsObj["payload"].set(str); + send(wsObj); + } + + // update peer status + std::map peerStatus(getNetwork()->peerStatus.begin(), getNetwork()->peerStatus.end()); + for (auto entry : peerStatus) { + json::object wsObj = json::object(); + std::string type = "peer_status"; + wsObj["type"].set(type); + uint32_t peerId = entry.first; + wsObj["peerId"].set(peerId); + json::object peerStatus = entry.second; + wsObj["payload"].set(peerStatus); + send(wsObj); + } + + // update peer list data + if (peerListUpdate.isRunning() && peerListUpdate.hasExpired()) { + peerListUpdate.start(); + // callback REST API to get status of the channel we represent + json::object req = json::object(); + json::object rsp = json::object(); + + int ret = RESTClient::send(fneRESTAddress, fneRESTPort, fnePassword, + HTTP_GET, FNE_GET_PEER_QUERY, req, rsp, fneSSL, g_debug); + if (ret != network::rest::http::HTTPPayload::StatusType::OK) { + ::LogError(LOG_HOST, "[AFFVIEW] failed to query peers for %s:%u", fneRESTAddress.c_str(), fneRESTPort); + } + else { + try { + json::object wsObj = json::object(); + std::string type = "peer_list"; + wsObj["type"].set(type); + wsObj["payload"].set(rsp); + send(wsObj); + } + catch (std::exception& e) { + ::LogWarning(LOG_HOST, "[AFFVIEW] %s:%u, failed to properly handle peer query request, %s", fneRESTAddress.c_str(), fneRESTPort, e.what()); + } + } + } + + // update affiliation list data + if (affListUpdate.isRunning() && affListUpdate.hasExpired()) { + affListUpdate.start(); + // callback REST API to get status of the channel we represent + json::object req = json::object(); + json::object rsp = json::object(); + + int ret = RESTClient::send(fneRESTAddress, fneRESTPort, fnePassword, + HTTP_GET, FNE_GET_AFF_LIST, req, rsp, fneSSL, g_debug); + if (ret != network::rest::http::HTTPPayload::StatusType::OK) { + ::LogError(LOG_HOST, "[AFFVIEW] failed to query peers for %s:%u", fneRESTAddress.c_str(), fneRESTPort); + } + else { + try { + json::object wsObj = json::object(); + std::string type = "aff_list"; + wsObj["type"].set(type); + wsObj["payload"].set(rsp); + send(wsObj); + } + catch (std::exception& e) { + ::LogWarning(LOG_HOST, "[AFFVIEW] %s:%u, failed to properly handle peer query request, %s", fneRESTAddress.c_str(), fneRESTPort, e.what()); + } + } + } + } else { + peerListUpdate.stop(); + affListUpdate.stop(); + } + + if (ms < 2U) + Thread::sleep(1U); + } + + if (g_killed) + m_wsServer.stop(); + + return EXIT_SUCCESS; +} + +/* */ + +void HostWS::send(json::object obj) +{ + json::value v = json::value(obj); + std::string json = std::string(v.serialize()); + + wsConList::iterator it; + for (it = m_wsConList.begin(); it != m_wsConList.end(); ++it) { + m_wsServer.send(*it, json, websocketpp::frame::opcode::text); + } +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Reads basic configuration parameters from the YAML configuration file. */ + +bool HostWS::readParams() +{ + yaml::Node websocketConf = m_conf["websocket"]; + m_websocketPort = websocketConf["port"].as(8443U); + + LogInfo("General Parameters"); + LogInfo(" Port: %u", m_websocketPort); + + return true; +} + +/* Called when a WebSocket connection is opened. */ + +void HostWS::wsOnConOpen(websocketpp::connection_hdl handle) +{ + m_wsConList.insert(handle); +} + +/* Called when a WebSocket connection is closed. */ + +void HostWS::wsOnConClose(websocketpp::connection_hdl handle) +{ + m_wsConList.erase(handle); +} + +/* Called when a WebSocket message is received. */ + +void HostWS::wsOnMessage(websocketpp::connection_hdl handle, wsServer::message_ptr msg) +{ + /* stub */ +} + +/* Entry point to WebSocket thread. */ + +void* HostWS::threadWebSocket(void* arg) +{ + thread_t* th = (thread_t*)arg; + if (th != nullptr) { +#if defined(_WIN32) + ::CloseHandle(th->thread); +#else + ::pthread_detach(th->thread); +#endif // defined(_WIN32) + + std::string threadName("sysview:ws-thread"); + HostWS* ws = (HostWS*)th->obj; + if (ws == nullptr) { + delete th; + return nullptr; + } + + if (g_killed) { + delete th; + return nullptr; + } + + LogDebug(LOG_HOST, "[ OK ] %s", threadName.c_str()); +#ifdef _GNU_SOURCE + ::pthread_setname_np(th->thread, threadName.c_str()); +#endif // _GNU_SOURCE + + ws->m_wsServer.listen(ws->m_websocketPort); + ws->m_wsServer.start_accept(); + ws->m_wsServer.run(); + + LogDebug(LOG_HOST, "[STOP] %s", threadName.c_str()); + delete th; + } + + return nullptr; +} + +#endif // !defined(NO_WEBSOCKETS) diff --git a/src/sysview/HostWS.h b/src/sysview/HostWS.h new file mode 100644 index 00000000..635906c4 --- /dev/null +++ b/src/sysview/HostWS.h @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - FNE System View + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file HostWS.h + * @ingroup fneSysView + * @file HostWS.cpp + * @ingroup fneSysView + */ +#if !defined(__HOST_WS_H__) +#define __HOST_WS_H__ + +#if !defined(NO_WEBSOCKETS) + +#include "Defines.h" +#include "common/lookups/RadioIdLookup.h" +#include "common/lookups/TalkgroupRulesLookup.h" +#include "common/yaml/Yaml.h" +#include "common/Timer.h" +#include "network/PeerNetwork.h" + +#include +#include + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements the core service logic. + * @ingroup fneSysView + */ +class HOST_SW_API HostWS { +public: + /** + * @brief Initializes a new instance of the HostWS class. + * @param confFile Full-path to the configuration file. + */ + HostWS(const std::string& confFile); + /** + * @brief Finalizes a instance of the HostWS class. + */ + ~HostWS(); + + /** + * @brief Executes the main host processing loop. + * @returns int Zero if successful, otherwise error occurred. + */ + int run(); + + /** + * @brief + * @param obj + */ + void send(json::object obj); + +private: + const std::string& m_confFile; + yaml::Node m_conf; + + uint16_t m_websocketPort; + + typedef websocketpp::server wsServer; + wsServer m_wsServer; + typedef std::set> wsConList; + wsConList m_wsConList; + + /** + * @brief Reads basic configuration parameters from the INI. + * @returns bool True, if configuration was read successfully, otherwise false. + */ + bool readParams(); + + /** + * @brief Called when a WebSocket connection is opened. + * @param handle Connection Handle. + */ + void wsOnConOpen(websocketpp::connection_hdl handle); + /** + * @brief Called when a WebSocket connection is closed. + * @param handle Connection Handle. + */ + void wsOnConClose(websocketpp::connection_hdl handle); + /** + * @brief Called when a WebSocket message is received. + * @param handle Connection Handle. + * @param msg WebSocket Message. + */ + void wsOnMessage(websocketpp::connection_hdl handle, wsServer::message_ptr msg); + + /** + * @brief Entry point to WebSocket thread. + * @param arg Instance of the thread_t structure. + * @returns void* (Ignore) + */ + static void* threadWebSocket(void* arg); +}; + +#endif // !defined(NO_WEBSOCKETS) + +#endif // __HOST_WS_H__ diff --git a/src/sysview/SysViewMain.cpp b/src/sysview/SysViewMain.cpp index 8df3df84..bf432fb7 100644 --- a/src/sysview/SysViewMain.cpp +++ b/src/sysview/SysViewMain.cpp @@ -29,6 +29,9 @@ #include "SysViewMain.h" #include "SysViewApplication.h" #include "SysViewMainWnd.h" +#if !defined(NO_WEBSOCKETS) +#include "HostWS.h" +#endif // !defined(NO_WEBSOCKETS) using namespace system_clock; using namespace network; @@ -49,14 +52,19 @@ using namespace lookups; // Global Variables // --------------------------------------------------------------------------- +int g_signal = 0; + std::string g_progExe = std::string(__EXE_NAME__); std::string g_iniFile = std::string(DEFAULT_CONF_FILE); yaml::Node g_conf; bool g_debug = false; +bool g_foreground = false; bool g_killed = false; bool g_hideLoggingWnd = false; +bool g_webSocketMode = false; + lookups::RadioIdLookup* g_ridLookup = nullptr; lookups::TalkgroupRulesLookup* g_tidLookup = nullptr; lookups::IdenTableLookup* g_idenTable = nullptr; @@ -86,6 +94,14 @@ std::unordered_map g_nxdnStatus; // Global Functions // --------------------------------------------------------------------------- +/* Internal signal handler. */ + +static void sigHandler(int signum) +{ + g_signal = signum; + g_killed = true; +} + /* Helper to print a fatal error message and exit. */ void fatal(const char* msg, ...) @@ -728,6 +744,9 @@ void usage(const char* message, const char* arg) "usage: %s [-dvh]" "[--hide-log]" "[-c ]" +#if !defined(NO_WEBSOCKETS) + "[-f][-ws]" +#endif // !defined(NO_WEBSOCKETS) "\n\n" " -d enable debug\n" " -v show version information\n" @@ -737,6 +756,11 @@ void usage(const char* message, const char* arg) "\n" " -c specifies the system view configuration file to use\n" "\n" +#if !defined(NO_WEBSOCKETS) + " -f foreground mode\n" + " -ws websocket mode\n" + "\n" +#endif // !defined(NO_WEBSOCKETS) " -- stop handling options\n", g_progExe.c_str()); @@ -777,6 +801,16 @@ int checkArgs(int argc, char* argv[]) ++p; g_hideLoggingWnd = true; } +#if !defined(NO_WEBSOCKETS) + else if (IS("-f")) { + ++p; + g_foreground = true; + } + else if (IS("-ws")) { + ++p; + g_webSocketMode = true; + } +#endif // !defined(NO_WEBSOCKETS) else if (IS("-d")) { ++p; g_debug = true; @@ -834,10 +868,18 @@ int main(int argc, char** argv) return 1; } - ::LogInfo(__PROG_NAME__ " " __VER__ " (built " __BUILD__ ")\r\n" \ - "Copyright (c) 2017-2024 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n" \ - "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\r\n" \ - ">> FNE System View\r\n"); + if (!g_webSocketMode) { + ::LogInfo(__PROG_NAME__ " " __VER__ " (built " __BUILD__ ")\r\n" \ + "Copyright (c) 2017-2024 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n" \ + "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\r\n" \ + ">> FNE System View\r\n"); + } else { + ::LogInfo(__BANNER__ "\r\n" __PROG_NAME__ " " __VER__ " (built " __BUILD__ ")\r\n" \ + "Copyright (c) 2024 DVMProject (https://github.com/dvmproject) Authors.\r\n" \ + "This program is non-free software; redistribution is strictly prohibited.\r\n" \ + "RESTRICTED CONFIDENTIAL PROPRIETARY. DO NOT DISTRIBUTE.\r\n" \ + ">> FNE System View\r\n"); + } try { ret = yaml::Parse(g_conf, g_iniFile.c_str()); @@ -854,11 +896,20 @@ int main(int argc, char** argv) return EXIT_FAILURE; // setup the finalcut tui - SysViewApplication app{argc, argv}; - g_app = &app; - - SysViewMainWnd wnd{&app}; - finalcut::FWidget::setMainWidget(&wnd); + SysViewApplication* app = nullptr; + SysViewMainWnd* wnd = nullptr; + if (!g_webSocketMode) { + app = new SysViewApplication(argc, argv); + g_app = app; + + wnd = new SysViewMainWnd(app); + finalcut::FWidget::setMainWidget(wnd); + } else { + // in WebSocket mode install signal handlers + ::signal(SIGINT, sigHandler); + ::signal(SIGTERM, sigHandler); + ::signal(SIGHUP, sigHandler); + } // try to load bandplan identity table std::string idenLookupFile = g_conf["iden_table"]["file"].as(); @@ -869,7 +920,8 @@ int main(int argc, char** argv) return 1; } - g_logDisplayLevel = 0U; + if (!g_webSocketMode) + g_logDisplayLevel = 0U; // try to load radio IDs table std::string ridLookupFile = g_conf["radio_id"]["file"].as(); @@ -903,14 +955,30 @@ int main(int argc, char** argv) g_idenTable = new IdenTableLookup(idenLookupFile, idenReloadTime); g_idenTable->read(); - // show and start the application - wnd.show(); - - finalcut::FApplication::setColorTheme(); - app.resetColors(); - app.redraw(); - - int _errno = app.exec(); + int _errno = 0U; + if (!g_webSocketMode) { + // show and start the application + wnd->show(); + + finalcut::FApplication::setColorTheme(); + app->resetColors(); + app->redraw(); + + _errno = app->exec(); + + if (wnd != nullptr) + delete wnd; + if (app != nullptr) + delete app; + } else { +#if !defined(NO_WEBSOCKETS) + ::LogFinalise(); // HostWS will reinitialize logging after this point... + + HostWS* host = new HostWS(g_iniFile); + _errno = host->run(); + delete host; +#endif // !defined(NO_WEBSOCKETS) + } g_logDisplayLevel = 1U; diff --git a/src/sysview/SysViewMain.h b/src/sysview/SysViewMain.h index 510f2d4d..46b808bd 100644 --- a/src/sysview/SysViewMain.h +++ b/src/sysview/SysViewMain.h @@ -41,6 +41,9 @@ // Externs // --------------------------------------------------------------------------- +/** @brief */ +extern int g_signal; + /** @brief */ extern std::string g_progExe; /** @brief */ @@ -50,6 +53,11 @@ extern yaml::Node g_conf; /** @brief */ extern bool g_debug; +/** @brief (Global) Flag indicating foreground operation. */ +extern bool g_foreground; +/** @brief (Global) Flag indicating the SysView should stop immediately. */ +extern bool g_killed; + /** @brief */ extern bool g_hideLoggingWnd;