implement backroundable WebSocket mode for SysView; add WebSockets++ dependency (disableable with CMake option: -DDISABLE_WEBSOCKETS=1);
parent
68d98817a9
commit
813a594b84
@ -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__
|
||||||
Loading…
Reference in new issue