implement backroundable WebSocket mode for SysView; add WebSockets++ dependency (disableable with CMake option: -DDISABLE_WEBSOCKETS=1);

pull/72/head
Bryan Biedenkapp 1 year ago
parent 68d98817a9
commit 813a594b84

@ -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)

@ -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
#
@ -59,3 +84,10 @@ fne:
restSsl: false
# REST API authentication password.
restPassword: "PASSWORD"
#
# WebSocket Configuration (only used in WebSocket mode)
#
websocket:
# Port number of the WebSocket should listen on.
port: 8443

@ -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)
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))
#

@ -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 <unistd.h>
#include <pwd.h>
// ---------------------------------------------------------------------------
// 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<bool>(false);
if (m_daemon && g_foreground)
m_daemon = false;
// re-initialize system logging
yaml::Node logConf = m_conf["log"];
ret = ::LogInitialise(logConf["filePath"].as<std::string>(), logConf["fileRoot"].as<std::string>(),
logConf["fileLevel"].as<uint32_t>(0U), logConf["displayLevel"].as<uint32_t>(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<std::string>("127.0.0.1");
uint16_t fneRESTPort = (uint16_t)fne["restPort"].as<uint32_t>(9990U);
std::string fnePassword = fne["restPassword"].as<std::string>("PASSWORD");
bool fneSSL = fne["restSsl"].as<bool>(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<std::string>(type);
wsObj["payload"].set<std::string>(str);
send(wsObj);
}
// update peer status
std::map<uint32_t, json::object> peerStatus(getNetwork()->peerStatus.begin(), getNetwork()->peerStatus.end());
for (auto entry : peerStatus) {
json::object wsObj = json::object();
std::string type = "peer_status";
wsObj["type"].set<std::string>(type);
uint32_t peerId = entry.first;
wsObj["peerId"].set<uint32_t>(peerId);
json::object peerStatus = entry.second;
wsObj["payload"].set<json::object>(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<std::string>(type);
wsObj["payload"].set<json::object>(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<std::string>(type);
wsObj["payload"].set<json::object>(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<uint16_t>(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)

@ -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 <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
#include <string>
#include <set>
#include <unordered_map>
#include <vector>
// ---------------------------------------------------------------------------
// 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<websocketpp::config::asio> wsServer;
wsServer m_wsServer;
typedef std::set<websocketpp::connection_hdl, std::owner_less<websocketpp::connection_hdl>> 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__

@ -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<uint32_t, RxStatus> 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 <configuration file>]"
#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 <file> 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;
}
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<std::string>();
@ -869,6 +920,7 @@ int main(int argc, char** argv)
return 1;
}
if (!g_webSocketMode)
g_logDisplayLevel = 0U;
// try to load radio IDs table
@ -903,14 +955,30 @@ int main(int argc, char** argv)
g_idenTable = new IdenTableLookup(idenLookupFile, idenReloadTime);
g_idenTable->read();
int _errno = 0U;
if (!g_webSocketMode) {
// show and start the application
wnd.show();
wnd->show();
finalcut::FApplication::setColorTheme<dvmColorTheme>();
app.resetColors();
app.redraw();
app->resetColors();
app->redraw();
_errno = app->exec();
int _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;

@ -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;

Loading…
Cancel
Save

Powered by TurnKey Linux.