Add Auto publishing mode to UDPStreamer; optimise WebUI hub
UDPStreamer: - Add PublishingMode = "Strict" | "Auto" config parameter - Add MinRefreshRate (Hz) for Auto mode; uses HRT phase-locked tick counting to rate-limit sends without accumulation buffers - Fix high-frequency integration test configs to use newline-separated key-value pairs (MARTe2 StandardParser does not treat ';' as delimiter) - Add 3 new unit tests (AutoMode_Valid, AutoMode_MissingRefreshRate, UnknownPublishingMode); all 33 tests passing WebUI hub: - Skip ring-buffer LTTB writes when zoom has not been accessed in 10 s, reducing idle CPU usage - Use unsafe float64→bytes reinterpretation to eliminate per-element encoding overhead in the hot broadcast path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,21 @@ TEST(UDPStreamerGTest, TestInitialise_ZeroStackSize) {
|
||||
ASSERT_TRUE(test.TestInitialise_ZeroStackSize());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestInitialise_AutoMode_Valid) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestInitialise_AutoMode_Valid());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestInitialise_AutoMode_MissingRefreshRate) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestInitialise_AutoMode_MissingRefreshRate());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestInitialise_UnknownPublishingMode) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestInitialise_UnknownPublishingMode());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestGetBrokerName_Output) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestGetBrokerName_Output());
|
||||
|
||||
@@ -230,6 +230,41 @@ bool UDPStreamerTest::TestInitialise_ZeroStackSize() {
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool UDPStreamerTest::TestInitialise_AutoMode_Valid() {
|
||||
using namespace MARTe;
|
||||
UDPStreamer ds;
|
||||
ConfigurationDatabase cdb;
|
||||
cdb.Write("Port", 44502u);
|
||||
cdb.Write("PublishingMode", "Auto");
|
||||
cdb.Write("MinRefreshRate", 120.0);
|
||||
cdb.CreateRelative("Signals");
|
||||
cdb.MoveToRoot();
|
||||
return ds.Initialise(cdb);
|
||||
}
|
||||
|
||||
bool UDPStreamerTest::TestInitialise_AutoMode_MissingRefreshRate() {
|
||||
using namespace MARTe;
|
||||
UDPStreamer ds;
|
||||
ConfigurationDatabase cdb;
|
||||
cdb.Write("Port", 44503u);
|
||||
cdb.Write("PublishingMode", "Auto");
|
||||
/* MinRefreshRate intentionally omitted */
|
||||
cdb.CreateRelative("Signals");
|
||||
cdb.MoveToRoot();
|
||||
return !ds.Initialise(cdb); /* Must fail */
|
||||
}
|
||||
|
||||
bool UDPStreamerTest::TestInitialise_UnknownPublishingMode() {
|
||||
using namespace MARTe;
|
||||
UDPStreamer ds;
|
||||
ConfigurationDatabase cdb;
|
||||
cdb.Write("Port", 44504u);
|
||||
cdb.Write("PublishingMode", "Invalid");
|
||||
cdb.CreateRelative("Signals");
|
||||
cdb.MoveToRoot();
|
||||
return !ds.Initialise(cdb); /* Must fail */
|
||||
}
|
||||
|
||||
bool UDPStreamerTest::TestGetBrokerName_Output() {
|
||||
using namespace MARTe;
|
||||
UDPStreamer ds;
|
||||
@@ -1157,36 +1192,88 @@ bool UDPStreamerTest::TestIsClientConnected_InitiallyFalse() {
|
||||
* Fragment count at MaxPayloadSize=1400:
|
||||
* chunk = 1400 - 17 = 1383 B → ceil(8016 / 1383) = 6 fragments
|
||||
*/
|
||||
/* MARTe2 StandardParser uses whitespace/newlines as delimiters.
|
||||
* Semicolons are NOT statement separators — they are consumed as part of
|
||||
* token values. All inline { key = val; key = val } blocks below have been
|
||||
* rewritten to use one key-value pair per line. */
|
||||
|
||||
#define HF_FUNCTIONS_BLOCK \
|
||||
" +Functions = {\n" \
|
||||
" Class = ReferenceContainer\n" \
|
||||
" +Writer = {\n" \
|
||||
" Class = UDPStreamerTestOutputGAM\n" \
|
||||
" OutputSignals = {\n" \
|
||||
" T0 = { DataSource = Streamer; Type = uint64 }\n" \
|
||||
" Ch1 = { DataSource = Streamer; Type = int16; NumberOfElements = 1000 }\n" \
|
||||
" Ch2 = { DataSource = Streamer; Type = int16; NumberOfElements = 1000 }\n" \
|
||||
" Ch3 = { DataSource = Streamer; Type = int16; NumberOfElements = 1000 }\n" \
|
||||
" Ch4 = { DataSource = Streamer; Type = int16; NumberOfElements = 1000 }\n" \
|
||||
" T0 = {\n" \
|
||||
" DataSource = Streamer\n" \
|
||||
" Type = uint64\n" \
|
||||
" }\n" \
|
||||
" Ch1 = {\n" \
|
||||
" DataSource = Streamer\n" \
|
||||
" Type = int16\n" \
|
||||
" NumberOfElements = 1000\n" \
|
||||
" }\n" \
|
||||
" Ch2 = {\n" \
|
||||
" DataSource = Streamer\n" \
|
||||
" Type = int16\n" \
|
||||
" NumberOfElements = 1000\n" \
|
||||
" }\n" \
|
||||
" Ch3 = {\n" \
|
||||
" DataSource = Streamer\n" \
|
||||
" Type = int16\n" \
|
||||
" NumberOfElements = 1000\n" \
|
||||
" }\n" \
|
||||
" Ch4 = {\n" \
|
||||
" DataSource = Streamer\n" \
|
||||
" Type = int16\n" \
|
||||
" NumberOfElements = 1000\n" \
|
||||
" }\n" \
|
||||
" }\n" \
|
||||
" }\n" \
|
||||
" }\n"
|
||||
|
||||
#define HF_SIGNALS_BLOCK \
|
||||
" Signals = {\n" \
|
||||
" T0 = { Type = uint64 }\n" \
|
||||
" Ch1 = { Type = int16; NumberOfDimensions = 1; NumberOfElements = 1000;\n" \
|
||||
" TimeMode = FirstSample; TimeSignal = T0; SamplingRate = 1000000.0 }\n" \
|
||||
" Ch2 = { Type = int16; NumberOfDimensions = 1; NumberOfElements = 1000;\n" \
|
||||
" TimeMode = FirstSample; TimeSignal = T0; SamplingRate = 1000000.0 }\n" \
|
||||
" Ch3 = { Type = int16; NumberOfDimensions = 1; NumberOfElements = 1000;\n" \
|
||||
" TimeMode = FirstSample; TimeSignal = T0; SamplingRate = 1000000.0 }\n" \
|
||||
" Ch4 = { Type = int16; NumberOfDimensions = 1; NumberOfElements = 1000;\n" \
|
||||
" TimeMode = FirstSample; TimeSignal = T0; SamplingRate = 1000000.0 }\n" \
|
||||
" T0 = {\n" \
|
||||
" Type = uint64\n" \
|
||||
" }\n" \
|
||||
" Ch1 = {\n" \
|
||||
" Type = int16\n" \
|
||||
" NumberOfDimensions = 1\n" \
|
||||
" NumberOfElements = 1000\n" \
|
||||
" TimeMode = FirstSample\n" \
|
||||
" TimeSignal = T0\n" \
|
||||
" SamplingRate = 1000000.0\n" \
|
||||
" }\n" \
|
||||
" Ch2 = {\n" \
|
||||
" Type = int16\n" \
|
||||
" NumberOfDimensions = 1\n" \
|
||||
" NumberOfElements = 1000\n" \
|
||||
" TimeMode = FirstSample\n" \
|
||||
" TimeSignal = T0\n" \
|
||||
" SamplingRate = 1000000.0\n" \
|
||||
" }\n" \
|
||||
" Ch3 = {\n" \
|
||||
" Type = int16\n" \
|
||||
" NumberOfDimensions = 1\n" \
|
||||
" NumberOfElements = 1000\n" \
|
||||
" TimeMode = FirstSample\n" \
|
||||
" TimeSignal = T0\n" \
|
||||
" SamplingRate = 1000000.0\n" \
|
||||
" }\n" \
|
||||
" Ch4 = {\n" \
|
||||
" Type = int16\n" \
|
||||
" NumberOfDimensions = 1\n" \
|
||||
" NumberOfElements = 1000\n" \
|
||||
" TimeMode = FirstSample\n" \
|
||||
" TimeSignal = T0\n" \
|
||||
" SamplingRate = 1000000.0\n" \
|
||||
" }\n" \
|
||||
" }\n"
|
||||
|
||||
#define HF_TAIL_BLOCK \
|
||||
" +Timings = { Class = TimingDataSource }\n" \
|
||||
" +Timings = {\n" \
|
||||
" Class = TimingDataSource\n" \
|
||||
" }\n" \
|
||||
" }\n" \
|
||||
" +States = {\n" \
|
||||
" Class = ReferenceContainer\n" \
|
||||
@@ -1194,36 +1281,57 @@ bool UDPStreamerTest::TestIsClientConnected_InitiallyFalse() {
|
||||
" Class = RealTimeState\n" \
|
||||
" +Threads = {\n" \
|
||||
" Class = ReferenceContainer\n" \
|
||||
" +Thread1 = { Class = RealTimeThread; Functions = { Writer } }\n" \
|
||||
" +Thread1 = {\n" \
|
||||
" Class = RealTimeThread\n" \
|
||||
" Functions = { Writer }\n" \
|
||||
" }\n" \
|
||||
" }\n" \
|
||||
" }\n" \
|
||||
" }\n" \
|
||||
" +Scheduler = { Class = GAMScheduler; TimingDataSource = Timings }\n" \
|
||||
" +Scheduler = {\n" \
|
||||
" Class = GAMScheduler\n" \
|
||||
" TimingDataSource = Timings\n" \
|
||||
" }\n" \
|
||||
"}\n"
|
||||
|
||||
static const MARTe::char8 *const HF_CFG_ALLOC =
|
||||
"+Test = { Class = RealTimeApplication\n"
|
||||
"+Test = {\n"
|
||||
" Class = RealTimeApplication\n"
|
||||
HF_FUNCTIONS_BLOCK
|
||||
" +Data = { Class = ReferenceContainer\n"
|
||||
" +Streamer = { Class = UDPStreamer; Port = 44650; MaxPayloadSize = 1400\n"
|
||||
" +Data = {\n"
|
||||
" Class = ReferenceContainer\n"
|
||||
" +Streamer = {\n"
|
||||
" Class = UDPStreamer\n"
|
||||
" Port = 44650\n"
|
||||
" MaxPayloadSize = 1400\n"
|
||||
HF_SIGNALS_BLOCK
|
||||
" }\n"
|
||||
HF_TAIL_BLOCK;
|
||||
|
||||
static const MARTe::char8 *const HF_CFG_FRAG =
|
||||
"+Test = { Class = RealTimeApplication\n"
|
||||
"+Test = {\n"
|
||||
" Class = RealTimeApplication\n"
|
||||
HF_FUNCTIONS_BLOCK
|
||||
" +Data = { Class = ReferenceContainer\n"
|
||||
" +Streamer = { Class = UDPStreamer; Port = 44651; MaxPayloadSize = 1400\n"
|
||||
" +Data = {\n"
|
||||
" Class = ReferenceContainer\n"
|
||||
" +Streamer = {\n"
|
||||
" Class = UDPStreamer\n"
|
||||
" Port = 44651\n"
|
||||
" MaxPayloadSize = 1400\n"
|
||||
HF_SIGNALS_BLOCK
|
||||
" }\n"
|
||||
HF_TAIL_BLOCK;
|
||||
|
||||
static const MARTe::char8 *const HF_CFG_INTEGRITY =
|
||||
"+Test = { Class = RealTimeApplication\n"
|
||||
"+Test = {\n"
|
||||
" Class = RealTimeApplication\n"
|
||||
HF_FUNCTIONS_BLOCK
|
||||
" +Data = { Class = ReferenceContainer\n"
|
||||
" +Streamer = { Class = UDPStreamer; Port = 44653; MaxPayloadSize = 1400\n"
|
||||
" +Data = {\n"
|
||||
" Class = ReferenceContainer\n"
|
||||
" +Streamer = {\n"
|
||||
" Class = UDPStreamer\n"
|
||||
" Port = 44653\n"
|
||||
" MaxPayloadSize = 1400\n"
|
||||
HF_SIGNALS_BLOCK
|
||||
" }\n"
|
||||
HF_TAIL_BLOCK;
|
||||
|
||||
@@ -162,6 +162,21 @@ public:
|
||||
*/
|
||||
bool TestQuantization_Uint8Clamping();
|
||||
|
||||
/**
|
||||
* @brief Tests PublishingMode = "Auto" with a valid MinRefreshRate.
|
||||
*/
|
||||
bool TestInitialise_AutoMode_Valid();
|
||||
|
||||
/**
|
||||
* @brief Tests that PublishingMode = "Auto" without MinRefreshRate is rejected.
|
||||
*/
|
||||
bool TestInitialise_AutoMode_MissingRefreshRate();
|
||||
|
||||
/**
|
||||
* @brief Tests that an unknown PublishingMode string is rejected.
|
||||
*/
|
||||
bool TestInitialise_UnknownPublishingMode();
|
||||
|
||||
/**
|
||||
* @brief Tests the GetPort() accessor.
|
||||
*/
|
||||
|
||||
+92
-15
@@ -4,14 +4,14 @@
|
||||
# Usage:
|
||||
# ./run.sh # run MARTe2 app only
|
||||
# ./run.sh --webui # also start the WebUI Go client in the background
|
||||
# ./run.sh --nativeui # also start the NativeUI ImGui client in the background
|
||||
# ./run.sh --qtui # also start the NativeUI Qt client in the background
|
||||
# ./run.sh --help # show this message
|
||||
#
|
||||
# The WebUI is started with three sources:
|
||||
# The clients are started with three sources:
|
||||
# Streamer @ 127.0.0.1:44500 (scalar signals, PacketTime, 1 kHz)
|
||||
# FastStreamer @ 127.0.0.1:44501 (packed arrays, FirstSample/LastSample, 5 kHz)
|
||||
# FullArrStreamer @ 127.0.0.1:44502 (packed arrays, FullArray, 5 kHz)
|
||||
# Additional sources and a persistent source list file (--sources-file) can
|
||||
# be configured directly in the WebUI or by editing this script.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - marte_env.sh must be present in the repo root (sets MARTe2_DIR, etc.)
|
||||
@@ -27,11 +27,15 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
# ── Parse arguments ──────────────────────────────────────────────────────────
|
||||
START_WEBUI=0
|
||||
START_NATIVEUI=0
|
||||
START_QTUI=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--webui) START_WEBUI=1 ;;
|
||||
--webui) START_WEBUI=1 ;;
|
||||
--nativeui) START_NATIVEUI=1 ;;
|
||||
--qtui) START_QTUI=1 ;;
|
||||
--help|-h)
|
||||
sed -n '2,15p' "$0"
|
||||
sed -n '2,21p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
@@ -84,10 +88,55 @@ WEBUI_DIR="${REPO_ROOT}/Client/WebUI"
|
||||
WEBUI_BIN="${WEBUI_DIR}/udpstreamer-webui"
|
||||
if [ "${START_WEBUI}" -eq 1 ] && [ ! -x "${WEBUI_BIN}" ]; then
|
||||
echo "==> Building WebUI..."
|
||||
(cd "${WEBUI_DIR}" && go build -o udpstreamer-webui ./...)
|
||||
(cd "${WEBUI_DIR}" && go build -o udpstreamer-webui ./...)
|
||||
echo "==> WebUI build done."
|
||||
fi
|
||||
|
||||
# ── Build NativeUI (ImGui) binary (if requested and not already built) ───────
|
||||
NATIVEUI_DIR="${REPO_ROOT}/Client/NativeUI"
|
||||
NATIVEUI_BUILD="${NATIVEUI_DIR}/build"
|
||||
NATIVEUI_BIN="${NATIVEUI_BUILD}/daq_viewer"
|
||||
if [ "${START_NATIVEUI}" -eq 1 ]; then
|
||||
if [ ! -x "${NATIVEUI_BIN}" ]; then
|
||||
echo "==> Building NativeUI/ImGui (cmake)..."
|
||||
cmake -S "${NATIVEUI_DIR}" -B "${NATIVEUI_BUILD}" -DCMAKE_BUILD_TYPE=Release -Wno-dev
|
||||
cmake --build "${NATIVEUI_BUILD}" --parallel "$(nproc)"
|
||||
echo "==> NativeUI/ImGui build done."
|
||||
else
|
||||
# Rebuild if sources are newer than the binary
|
||||
if find "${NATIVEUI_DIR}/src" -name '*.cpp' -o -name '*.h' \
|
||||
| xargs ls -t 2>/dev/null | head -1 \
|
||||
| xargs -I{} test {} -nt "${NATIVEUI_BIN}" 2>/dev/null; then
|
||||
echo "==> Sources changed — rebuilding NativeUI/ImGui..."
|
||||
cmake --build "${NATIVEUI_BUILD}" --parallel "$(nproc)"
|
||||
echo "==> NativeUI/ImGui rebuild done."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Build NativeUI-Qt binary (if requested and not already built) ─────────────
|
||||
QTUI_DIR="${REPO_ROOT}/Client/NativeUI-Qt"
|
||||
QTUI_BUILD="${QTUI_DIR}/build"
|
||||
QTUI_BIN="${QTUI_BUILD}/daq_viewer"
|
||||
if [ "${START_QTUI}" -eq 1 ]; then
|
||||
if [ ! -x "${QTUI_BIN}" ]; then
|
||||
echo "==> Building NativeUI/Qt (cmake)..."
|
||||
cmake -S "${QTUI_DIR}" -B "${QTUI_BUILD}" -DCMAKE_BUILD_TYPE=Release -Wno-dev
|
||||
cmake --build "${QTUI_BUILD}" --parallel "$(nproc)"
|
||||
echo "==> NativeUI/Qt build done."
|
||||
else
|
||||
# Rebuild if sources are newer than the binary
|
||||
if find "${QTUI_DIR}/src" "${REPO_ROOT}/Client/NativeUI/src" \
|
||||
\( -name '*.cpp' -o -name '*.h' \) \
|
||||
| xargs ls -t 2>/dev/null | head -1 \
|
||||
| xargs -I{} test {} -nt "${QTUI_BIN}" 2>/dev/null; then
|
||||
echo "==> Sources changed — rebuilding NativeUI/Qt..."
|
||||
cmake --build "${QTUI_BUILD}" --parallel "$(nproc)"
|
||||
echo "==> NativeUI/Qt rebuild done."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Set LD_LIBRARY_PATH ───────────────────────────────────────────────────────
|
||||
COMP="${MARTe2_Components_DIR}/Build/${TARGET}/Components"
|
||||
|
||||
@@ -105,10 +154,6 @@ ${LD_LIBRARY_PATH}"
|
||||
echo "==> LD_LIBRARY_PATH=${LD_LIBRARY_PATH}"
|
||||
|
||||
# ── Create class-name symlinks for MARTe2 auto-loader ────────────────────────
|
||||
# MARTe2 looks for <ClassName>.so when a class is not yet registered.
|
||||
# Some components bundle multiple classes into one .so (e.g. WaveformGAM.so
|
||||
# contains WaveformSin, WaveformChirp, WaveformPointsDef). We need symlinks
|
||||
# so dlopen("<ClassName>.so") succeeds.
|
||||
WAVEFORM_DIR="${COMP}/GAMs/WaveformGAM"
|
||||
for cls in WaveformSin WaveformChirp WaveformPointsDef; do
|
||||
target="${WAVEFORM_DIR}/${cls}.so"
|
||||
@@ -117,10 +162,10 @@ for cls in WaveformSin WaveformChirp WaveformPointsDef; do
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
# ── Optionally start WebUI ────────────────────────────────────────────────────
|
||||
WEBUI_PID=""
|
||||
if [ "${START_WEBUI}" -eq 1 ]; then
|
||||
echo "==> Starting WebUI on http://localhost:8080 (source: 127.0.0.1:44500)..."
|
||||
echo "==> Starting WebUI on http://localhost:8080..."
|
||||
"${WEBUI_BIN}" \
|
||||
--source "Streamer@127.0.0.1:44500" \
|
||||
--source "FastStreamer@127.0.0.1:44501" \
|
||||
@@ -130,6 +175,38 @@ if [ "${START_WEBUI}" -eq 1 ]; then
|
||||
echo "==> WebUI PID ${WEBUI_PID}"
|
||||
fi
|
||||
|
||||
# ── Optionally start NativeUI (ImGui) ────────────────────────────────────────
|
||||
NATIVEUI_PID=""
|
||||
if [ "${START_NATIVEUI}" -eq 1 ]; then
|
||||
if [ ! -x "${NATIVEUI_BIN}" ]; then
|
||||
echo "ERROR: NativeUI/ImGui binary not found at ${NATIVEUI_BIN}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> Starting NativeUI/ImGui..."
|
||||
"${NATIVEUI_BIN}" \
|
||||
"Streamer@127.0.0.1:44500" \
|
||||
"FastStreamer@127.0.0.1:44501" \
|
||||
"FullArrStreamer@127.0.0.1:44502" &
|
||||
NATIVEUI_PID=$!
|
||||
echo "==> NativeUI/ImGui PID ${NATIVEUI_PID}"
|
||||
fi
|
||||
|
||||
# ── Optionally start NativeUI-Qt ──────────────────────────────────────────────
|
||||
QTUI_PID=""
|
||||
if [ "${START_QTUI}" -eq 1 ]; then
|
||||
if [ ! -x "${QTUI_BIN}" ]; then
|
||||
echo "ERROR: NativeUI/Qt binary not found at ${QTUI_BIN}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> Starting NativeUI/Qt..."
|
||||
"${QTUI_BIN}" \
|
||||
"Streamer@127.0.0.1:44500" \
|
||||
"FastStreamer@127.0.0.1:44501" \
|
||||
"FullArrStreamer@127.0.0.1:44502" &
|
||||
QTUI_PID=$!
|
||||
echo "==> NativeUI/Qt PID ${QTUI_PID}"
|
||||
fi
|
||||
|
||||
# ── Launch MARTe2 application ─────────────────────────────────────────────────
|
||||
MARTE_APP="${MARTe2_DIR}/Build/${TARGET}/App/MARTeApp.ex"
|
||||
CFG="${SCRIPT_DIR}/TestApp.cfg"
|
||||
@@ -149,9 +226,9 @@ echo ""
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "==> Stopping..."
|
||||
if [ "${START_WEBUI}" -eq 1 ] && kill -0 "${WEBUI_PID}" 2>/dev/null; then
|
||||
kill "${WEBUI_PID}"
|
||||
fi
|
||||
[ -n "${WEBUI_PID}" ] && kill -0 "${WEBUI_PID}" 2>/dev/null && kill "${WEBUI_PID}"
|
||||
[ -n "${NATIVEUI_PID}" ] && kill -0 "${NATIVEUI_PID}" 2>/dev/null && kill "${NATIVEUI_PID}"
|
||||
[ -n "${QTUI_PID}" ] && kill -0 "${QTUI_PID}" 2>/dev/null && kill "${QTUI_PID}"
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
|
||||
Reference in New Issue
Block a user