diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/README.md b/exercises/306_microReticulum_ble_file_transfer_oled/README.md index 72f8b6a..bac6e10 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/README.md +++ b/exercises/306_microReticulum_ble_file_transfer_oled/README.md @@ -309,6 +309,19 @@ Run one on each Pi. There should be no required start order for the dual-role te ./microreticulum_306_rpi_arm64_dual_children ``` +Dual builds default to `--ble-dual-policy=first-path-wins`. Both BlueZ roles start, but once either central or peripheral establishes the first peer path, the opposite role is stopped. This avoids exposing two simultaneous Reticulum interfaces to the same peer. + +Temporary policy overrides are available for debugging: + +```bash +./microreticulum_306_rpi_arm64_dual_little_boy_blue --ble-dual-policy=both +./microreticulum_306_rpi_arm64_dual_little_boy_blue --ble-dual-policy=first-path-wins +./microreticulum_306_rpi_arm64_dual_little_boy_blue --ble-dual-policy=central-only +./microreticulum_306_rpi_arm64_dual_little_boy_blue --ble-dual-policy=peripheral-only +``` + +`both` preserves the earlier Linux dual behavior and may produce Reticulum `Link-associated packet received on unexpected interface` errors if two BLE paths form at the same time. The `central-only` and `peripheral-only` policies are useful when isolating BlueZ adapter behavior without rebuilding split-role binaries. + You can copy those binaries to another Pi Zero 2W and rename them for clarity if both Pis use the same CPU architecture, OS bitness, and compatible runtime libraries. Check with: ```bash diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/raspberrypi_downloads.html b/exercises/306_microReticulum_ble_file_transfer_oled/raspberrypi_downloads.html index 332992d..41bec06 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/raspberrypi_downloads.html +++ b/exercises/306_microReticulum_ble_file_transfer_oled/raspberrypi_downloads.html @@ -60,6 +60,8 @@

Use two different payload binaries for a simple exchange test. Start one on each Pi; no fixed client/server start order is intended for the dual builds. + The default BLE policy is first-path-wins, which stops the opposite BlueZ + role after the first peer path is established.

Downloads

diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/raspberrypi_install.md b/exercises/306_microReticulum_ble_file_transfer_oled/raspberrypi_install.md index 09a895b..fc6c226 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/raspberrypi_install.md +++ b/exercises/306_microReticulum_ble_file_transfer_oled/raspberrypi_install.md @@ -106,6 +106,8 @@ microreticulum_306_rpi_arm64_dual_little_boy_blue microreticulum_306_rpi_arm64_dual_children ``` +Dual binaries default to `--ble-dual-policy=first-path-wins`: both BlueZ roles start, but the first working peer path stops the opposite role. To reproduce the older experimental behavior, run with `--ble-dual-policy=both`. + Example scp command to a new server named "trixie1": ```bash scp microreticulum_306_rpi_arm64_dual_children trixie1:~ @@ -162,6 +164,7 @@ Expected healthy startup: jlpoole@trixie1:~ $ ./microreticulum_306_rpi_arm64_dual_children Exercise 306 native BLE file transfer console Node=Node-PIZERO2-DUAL +BLE dual policy=first-path-wins Selected file=children.txt bytes=1422 chunk=32 interval_ms=500 repeat_rest_ms=10000 [ustore] Initializing PosixFileSystem [ustore] WARNING: FlashFSFileSystem check failed, reformatting! @@ -175,6 +178,20 @@ BLE linux-dual waiting for peer BLE linux-dual waiting for peer ``` + +When one role wins, expect a line like: + +```text +BLE dual policy first-path-wins: peripheral path won peer=:1.42; stopping central role +``` + +or: + +```text +BLE dual policy first-path-wins: central path won peer=/org/bluez/hci0/dev_D8_3A_DD_1D_CF_B5; stopping peripheral role +``` + +The native BLE interfaces also emit temporary `BLE-FRAME` lines for Reticulum frame tracing. Those lines include timestamp, node label, BLE role, peer path/address, BlueZ object path, interface pointer, packet length, and the first packet byte. When you have a second unit running, your console will show something like: ```bash [Startup stuff] diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.cpp b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.cpp index 1e65f27..975f68a 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.cpp +++ b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.cpp @@ -48,6 +48,34 @@ static void log_error(const char* what, GError* error) { } } +static std::string peer_address_from_path(const std::string& path) { + size_t pos = path.find("/dev_"); + if (pos == std::string::npos) { + return path.empty() ? std::string("unknown") : path; + } + std::string addr = path.substr(pos + 5); + std::replace(addr.begin(), addr.end(), '_', ':'); + return addr; +} + +static void log_reticulum_frame(const char* direction, + const std::string& node_label, + const std::string& peer_path, + const std::string& object_path, + const void* iface, + const RNS::Bytes& packet) { + unsigned first = packet.size() > 0 ? packet[0] : 0; + std::printf("BLE-FRAME t=%llu node=%s role=central dir=%s peer=%s object=%s if=%p len=%zu packet0=0x%02X link_hint=unknown\n", + (unsigned long long)now_ms(), + node_label.c_str(), + direction, + peer_address_from_path(peer_path).c_str(), + object_path.empty() ? "unknown" : object_path.c_str(), + iface, + packet.size(), + first); +} + HostBluezBleInterface::HostBluezBleInterface(const std::string& node_label, const char* name) : InterfaceImpl(name), node_label_(node_label) { _IN = true; @@ -281,7 +309,6 @@ bool HostBluezBleInterface::connect_device(const std::string& device_path) { if (!result) { log_error("BLE linux-central: Device1.Connect failed", error); next_scan_ms_ = now_ms() + SCAN_RETRY_MS; - start_discovery(); return false; } g_variant_unref(result); @@ -457,6 +484,8 @@ void HostBluezBleInterface::send_outgoing(const RNS::Bytes& data) { return; } + log_reticulum_frame("tx", node_label_, device_path_, rx_char_path_, this, data); + size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE; if (total == 0 || total > 65535) { std::fprintf(stderr, "BLE linux-central: cannot fragment packet len=%zu fragments=%zu\n", data.size(), total); @@ -617,6 +646,7 @@ void HostBluezBleInterface::handle_fragment(const uint8_t* data, size_t len) { } void HostBluezBleInterface::enqueue_packet(const RNS::Bytes& packet) { + log_reticulum_frame("rx", node_label_, device_path_, tx_char_path_, this, packet); incoming_packets_.push_back(packet); } diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.h b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.h index d5263a6..2df13f3 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.h +++ b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezBleInterface.h @@ -21,6 +21,7 @@ public: bool connected() const { return connected_; } const char* role_name() const { return "linux-central"; } + const std::string& peer_path() const { return device_path_; } private: void send_outgoing(const RNS::Bytes& data) override; diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.cpp b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.cpp index 1ff7f6c..0a97ac6 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.cpp +++ b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.cpp @@ -62,6 +62,34 @@ static GVariant* flags_variant(std::initializer_list flags) { return g_variant_builder_end(&builder); } +static std::string peer_address_from_path(const std::string& path) { + size_t pos = path.find("/dev_"); + if (pos == std::string::npos) { + return path.empty() ? std::string("unknown") : path; + } + std::string addr = path.substr(pos + 5); + std::replace(addr.begin(), addr.end(), '_', ':'); + return addr; +} + +static void log_reticulum_frame(const char* direction, + const std::string& node_label, + const std::string& peer_path, + const char* object_path, + const void* iface, + const RNS::Bytes& packet) { + unsigned first = packet.size() > 0 ? packet[0] : 0; + std::printf("BLE-FRAME t=%llu node=%s role=peripheral dir=%s peer=%s object=%s if=%p len=%zu packet0=0x%02X link_hint=unknown\n", + (unsigned long long)now_ms(), + node_label.c_str(), + direction, + peer_address_from_path(peer_path).c_str(), + object_path ? object_path : "unknown", + iface, + packet.size(), + first); +} + static const char* OBJECT_MANAGER_XML = "" " " @@ -167,6 +195,7 @@ void HostBluezPeripheralInterface::stop() { online_ = false; connected_ = false; notifying_ = false; + peer_path_.clear(); _online = false; } @@ -381,6 +410,8 @@ void HostBluezPeripheralInterface::send_outgoing(const RNS::Bytes& data) { return; } + log_reticulum_frame("tx", node_label_, peer_path_, TX_PATH, this, data); + size_t total = (data.size() + BLE_PAYLOAD_SIZE - 1) / BLE_PAYLOAD_SIZE; if (total == 0 || total > 65535) { std::fprintf(stderr, "BLE linux-peripheral: cannot fragment packet len=%zu fragments=%zu\n", data.size(), total); @@ -496,6 +527,7 @@ void HostBluezPeripheralInterface::handle_fragment(const uint8_t* data, size_t l } void HostBluezPeripheralInterface::enqueue_packet(const RNS::Bytes& packet) { + log_reticulum_frame("rx", node_label_, peer_path_, RX_PATH, this, packet); incoming_packets_.push_back(packet); } @@ -615,6 +647,7 @@ GVariant* HostBluezPeripheralInterface::get_property(const char* object_path, void HostBluezPeripheralInterface::handle_method_call(const char* object_path, const char* interface_name, const char* method_name, + const char* sender, GVariant* parameters, GDBusMethodInvocation* invocation) { if (std::strcmp(interface_name, "org.freedesktop.DBus.ObjectManager") == 0 && @@ -661,6 +694,12 @@ void HostBluezPeripheralInterface::handle_method_call(const char* object_path, GVariant* value = nullptr; GVariant* options = nullptr; g_variant_get(parameters, "(@ay@a{sv})", &value, &options); + const gchar* device = nullptr; + if (options && g_variant_lookup(options, "device", "&o", &device) && device) { + peer_path_ = device; + } else if (sender && peer_path_.empty()) { + peer_path_ = sender; + } gsize len = 0; const uint8_t* data = static_cast(g_variant_get_fixed_array(value, &len, sizeof(uint8_t))); if (data && len > 0) { @@ -683,6 +722,9 @@ void HostBluezPeripheralInterface::handle_method_call(const char* object_path, } notifying_ = true; connected_ = true; + if (sender && peer_path_.empty()) { + peer_path_ = sender; + } std::printf("BLE linux-peripheral: central subscribed to TX notifications\n"); g_dbus_method_invocation_return_value(invocation, nullptr); return; @@ -691,6 +733,7 @@ void HostBluezPeripheralInterface::handle_method_call(const char* object_path, if (std::strcmp(method_name, "StopNotify") == 0) { notifying_ = false; connected_ = false; + peer_path_.clear(); reset_reassembly(); std::printf("BLE linux-peripheral: central unsubscribed from TX notifications\n"); g_dbus_method_invocation_return_value(invocation, nullptr); @@ -715,7 +758,7 @@ void HostBluezPeripheralInterface::method_call(GDBusConnection* connection, g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "No peripheral instance"); return; } - self->handle_method_call(object_path, interface_name, method_name, parameters, invocation); + self->handle_method_call(object_path, interface_name, method_name, sender, parameters, invocation); } GVariant* HostBluezPeripheralInterface::property_get(GDBusConnection* connection, diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.h b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.h index 4935972..ca123c5 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.h +++ b/exercises/306_microReticulum_ble_file_transfer_oled/src/HostBluezPeripheralInterface.h @@ -21,6 +21,7 @@ public: bool connected() const { return connected_; } const char* role_name() const { return "linux-peripheral"; } + const std::string& peer_path() const { return peer_path_; } private: void send_outgoing(const RNS::Bytes& data) override; @@ -44,6 +45,7 @@ private: void handle_method_call(const char* object_path, const char* interface_name, const char* method_name, + const char* sender, GVariant* parameters, GDBusMethodInvocation* invocation); @@ -102,6 +104,7 @@ private: bool advertisement_registered_ = false; bool has_gatt_manager_ = false; bool has_advertising_manager_ = false; + std::string peer_path_; uint32_t tx_message_id_ = 0; RNS::Bytes tx_value_; RNS::Bytes identity_value_; diff --git a/exercises/306_microReticulum_ble_file_transfer_oled/src/host_jp_main.cpp b/exercises/306_microReticulum_ble_file_transfer_oled/src/host_jp_main.cpp index 7c9b129..d4ec181 100644 --- a/exercises/306_microReticulum_ble_file_transfer_oled/src/host_jp_main.cpp +++ b/exercises/306_microReticulum_ble_file_transfer_oled/src/host_jp_main.cpp @@ -59,9 +59,18 @@ static constexpr uint32_t FNV1A_PRIME = 16777619UL; static RNS::Reticulum reticulum({RNS::Type::NONE}); #if defined(HOST_BLE_DUAL) +enum class BleDualPolicy { + Both, + FirstPathWins, + CentralOnly, + PeripheralOnly, +}; + static std::vector ble_interfaces; static HostBluezBleInterface* ble_central_impl = nullptr; static HostBluezPeripheralInterface* ble_peripheral_impl = nullptr; +static BleDualPolicy ble_dual_policy = BleDualPolicy::FirstPathWins; +static const char* ble_dual_winner = nullptr; #else static RNS::Interface ble_interface({RNS::Type::NONE}); static HostBleInterface* ble_impl = nullptr; @@ -79,6 +88,42 @@ static bool link_attempted = false; static std::string node_label = HOST_NODE_LABEL; static bool running = true; +#if defined(HOST_BLE_DUAL) +static const char* ble_dual_policy_name(BleDualPolicy policy) { + switch (policy) { + case BleDualPolicy::Both: + return "both"; + case BleDualPolicy::FirstPathWins: + return "first-path-wins"; + case BleDualPolicy::CentralOnly: + return "central-only"; + case BleDualPolicy::PeripheralOnly: + return "peripheral-only"; + } + return "unknown"; +} + +static bool set_ble_dual_policy(const char* value) { + if (std::strcmp(value, "both") == 0) { + ble_dual_policy = BleDualPolicy::Both; + return true; + } + if (std::strcmp(value, "first-path-wins") == 0) { + ble_dual_policy = BleDualPolicy::FirstPathWins; + return true; + } + if (std::strcmp(value, "central-only") == 0) { + ble_dual_policy = BleDualPolicy::CentralOnly; + return true; + } + if (std::strcmp(value, "peripheral-only") == 0) { + ble_dual_policy = BleDualPolicy::PeripheralOnly; + return true; + } + return false; +} +#endif + struct RxTransferState { bool active = false; std::string sender; @@ -433,21 +478,25 @@ static void setup_reticulum() { RNS::Utilities::OS::register_filesystem(filesystem); #if defined(HOST_BLE_DUAL) - auto central = std::shared_ptr(new HostBluezBleInterface(node_label)); - ble_central_impl = static_cast(central.get()); - RNS::Interface central_interface(central); - central_interface.mode(RNS::Type::Interface::MODE_GATEWAY); - RNS::Transport::register_interface(central_interface); - ble_interfaces.push_back(central_interface); - central_interface.start(); + if (ble_dual_policy != BleDualPolicy::PeripheralOnly) { + auto central = std::shared_ptr(new HostBluezBleInterface(node_label)); + ble_central_impl = static_cast(central.get()); + RNS::Interface central_interface(central); + central_interface.mode(RNS::Type::Interface::MODE_GATEWAY); + RNS::Transport::register_interface(central_interface); + ble_interfaces.push_back(central_interface); + central_interface.start(); + } - auto peripheral = std::shared_ptr(new HostBluezPeripheralInterface(node_label)); - ble_peripheral_impl = static_cast(peripheral.get()); - RNS::Interface peripheral_interface(peripheral); - peripheral_interface.mode(RNS::Type::Interface::MODE_GATEWAY); - RNS::Transport::register_interface(peripheral_interface); - ble_interfaces.push_back(peripheral_interface); - peripheral_interface.start(); + if (ble_dual_policy != BleDualPolicy::CentralOnly) { + auto peripheral = std::shared_ptr(new HostBluezPeripheralInterface(node_label)); + ble_peripheral_impl = static_cast(peripheral.get()); + RNS::Interface peripheral_interface(peripheral); + peripheral_interface.mode(RNS::Type::Interface::MODE_GATEWAY); + RNS::Transport::register_interface(peripheral_interface); + ble_interfaces.push_back(peripheral_interface); + peripheral_interface.start(); + } #else auto impl = std::shared_ptr(new HostBleInterface(node_label)); ble_impl = static_cast(impl.get()); @@ -478,12 +527,12 @@ static void setup_reticulum() { static void loop_ble_interfaces() { #if defined(HOST_BLE_DUAL) - if (ble_central_impl) { - ble_central_impl->loop(); - } if (ble_peripheral_impl) { ble_peripheral_impl->loop(); } + if (ble_central_impl) { + ble_central_impl->loop(); + } #else if (ble_impl) { ble_impl->loop(); @@ -491,6 +540,33 @@ static void loop_ble_interfaces() { #endif } +#if defined(HOST_BLE_DUAL) +static void enforce_ble_dual_policy() { + if (ble_dual_policy != BleDualPolicy::FirstPathWins || ble_dual_winner) { + return; + } + + if (ble_peripheral_impl && ble_peripheral_impl->connected()) { + ble_dual_winner = "peripheral"; + std::printf("BLE dual policy first-path-wins: peripheral path won peer=%s; stopping central role\n", + ble_peripheral_impl->peer_path().empty() ? "unknown" : ble_peripheral_impl->peer_path().c_str()); + if (ble_central_impl) { + ble_central_impl->stop(); + } + return; + } + + if (ble_central_impl && ble_central_impl->connected()) { + ble_dual_winner = "central"; + std::printf("BLE dual policy first-path-wins: central path won peer=%s; stopping peripheral role\n", + ble_central_impl->peer_path().empty() ? "unknown" : ble_central_impl->peer_path().c_str()); + if (ble_peripheral_impl) { + ble_peripheral_impl->stop(); + } + } +} +#endif + static bool ble_connected() { #if defined(HOST_BLE_DUAL) return (ble_central_impl && ble_central_impl->connected()) || @@ -502,6 +578,9 @@ static bool ble_connected() { static const char* ble_role_status() { #if defined(HOST_BLE_DUAL) + if (ble_dual_winner) { + return ble_dual_winner; + } if (ble_central_impl && ble_central_impl->connected()) { return ble_central_impl->role_name(); } @@ -534,13 +613,37 @@ static void handle_signal(int signal) { running = false; } -int main() { +int main(int argc, char** argv) { std::signal(SIGINT, handle_signal); std::signal(SIGTERM, handle_signal); +#if defined(HOST_BLE_DUAL) + static constexpr const char* policy_prefix = "--ble-dual-policy="; + for (int i = 1; i < argc; ++i) { + if (std::strncmp(argv[i], policy_prefix, std::strlen(policy_prefix)) == 0) { + const char* value = argv[i] + std::strlen(policy_prefix); + if (!set_ble_dual_policy(value)) { + std::fprintf(stderr, + "Unknown --ble-dual-policy=%s; valid values: both, first-path-wins, central-only, peripheral-only\n", + value); + return 2; + } + } else { + std::fprintf(stderr, "Unknown option: %s\n", argv[i]); + return 2; + } + } +#else + (void)argc; + (void)argv; +#endif + RNS::loglevel(RNS::LOG_NOTICE); std::printf("Exercise 306 native BLE file transfer console\n"); std::printf("Node=%s\n", node_label.c_str()); +#if defined(HOST_BLE_DUAL) + std::printf("BLE dual policy=%s\n", ble_dual_policy_name(ble_dual_policy)); +#endif std::printf("Selected file=%s bytes=%u chunk=%u interval_ms=%lu repeat_rest_ms=%lu\n", SELECTED_TEXT_NAME, (unsigned)SELECTED_TEXT_SIZE, (unsigned)TRANSFER_CHUNK_SIZE, @@ -557,6 +660,9 @@ int main() { while (running) { reticulum.loop(); loop_ble_interfaces(); +#if defined(HOST_BLE_DUAL) + enforce_ble_dual_policy(); +#endif uint64_t now = millis64(); if (!ble_connected()) { diff --git a/tools/run_sync.sh b/tools/run_sync.sh new file mode 100755 index 0000000..2e46e53 --- /dev/null +++ b/tools/run_sync.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# 20260522 ChatGPT +# $Header$ +# +# Example: +# ./run_sync.sh little_boy_blue 14:40 +# ./run_sync.sh childrens_hour 14:40 +# ./run_sync.sh ./little_boy_blue 14:40 +# +# Optional with timeout: +# ./run_sync.sh little_boy_blue 14:40 300 +# + +set -euo pipefail + +if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then + echo "Usage: $0 binary_name HH:MM [run_seconds]" + echo "Example: $0 little_boy_blue 14:40" + echo "Example: $0 little_boy_blue 14:40 300" + exit 1 +fi + +BINARY="$1" +START_TIME="$2" +RUN_SECONDS="${3:-}" + +# If the binary name does not contain a slash, assume it is in the current directory. +case "$BINARY" in + */*) CMD="$BINARY" ;; + *) CMD="./$BINARY" ;; +esac + +if [ ! -x "$CMD" ]; then + echo "ERROR: command is not executable: $CMD" + echo + echo "Check with:" + echo " ls -l $CMD" + echo " chmod 755 $CMD" + exit 1 +fi + +TARGET_EPOCH=$(date -d "today ${START_TIME}" +%s) +NOW_EPOCH=$(date +%s) + +# If requested time already passed today, assume tomorrow. +if [ "$TARGET_EPOCH" -le "$NOW_EPOCH" ]; then + TARGET_EPOCH=$(date -d "tomorrow ${START_TIME}" +%s) +fi + +SLEEP_SECONDS=$(( TARGET_EPOCH - NOW_EPOCH )) + +mkdir -p "$HOME/logs" + +echo "Host: $(hostname)" +echo "Binary: $CMD" +echo "Now: $(date '+%Y%m%d_%H%M%S %Z')" +echo "Target time: $(date -d "@${TARGET_EPOCH}" '+%Y%m%d_%H%M%S %Z')" +echo "Sleeping for: ${SLEEP_SECONDS} seconds" + +sleep "$SLEEP_SECONDS" + +STAMP=$(date +%Y%m%d_%H%M%S) +SAFE_BINARY=$(basename "$BINARY") +LOG="$HOME/logs/$(hostname)_${SAFE_BINARY}_${STAMP}.log" + +{ + echo "SYNC_START_HOST=$(hostname)" + echo "SYNC_START_BINARY=$CMD" + echo "SYNC_START_EPOCH=$(date +%s.%N)" + echo "SYNC_START_LOCAL=$(date '+%Y%m%d_%H%M%S.%N %Z')" + echo "SYNC_LOG_FILE=$LOG" + echo "========================================" +} | tee "$LOG" + +if [ -n "$RUN_SECONDS" ]; then + exec timeout "$RUN_SECONDS"s "$CMD" 2>&1 | tee -a "$LOG" +else + exec "$CMD" 2>&1 | tee -a "$LOG" +fi