diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index a80619e90..b528f80b2 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -20,5 +20,7 @@ jobs: uses: actions/stale@v10.1.0 with: days-before-stale: 45 + stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days. + close-issue-message: This issue has not had any comment since the last notice. It has been closed automatically. If this is incorrect, or the issue becomes relevant again, please request that it is reopened. exempt-issue-labels: pinned,3.0 exempt-pr-labels: pinned,3.0 diff --git a/bin/kill-github-actions.sh b/bin/kill-github-actions.sh new file mode 100755 index 000000000..f71047c5e --- /dev/null +++ b/bin/kill-github-actions.sh @@ -0,0 +1,116 @@ +#!/bin/bash + +# Script to cancel all running GitHub Actions workflows +# Requires GitHub CLI (gh) to be installed and authenticated + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if gh CLI is installed +if ! command -v gh &> /dev/null; then + print_error "GitHub CLI (gh) is not installed. Please install it first:" + echo " brew install gh" + echo " Or visit: https://cli.github.com/" + exit 1 +fi + +# Check if authenticated +if ! gh auth status &> /dev/null; then + print_error "GitHub CLI is not authenticated. Please run:" + echo " gh auth login" + exit 1 +fi + +# Get repository info +REPO=$(gh repo view --json owner,name -q '.owner.login + "/" + .name') +if [[ -z "$REPO" ]]; then + print_error "Could not determine repository. Make sure you're in a GitHub repository." + exit 1 +fi + +print_status "Working with repository: $REPO" + +# Get all active workflows (both queued and in-progress) +print_status "Fetching active workflows (queued and in-progress)..." +QUEUED_WORKFLOWS=$(gh run list --status queued --json databaseId,displayTitle,headBranch,status --limit 100) +IN_PROGRESS_WORKFLOWS=$(gh run list --status in_progress --json databaseId,displayTitle,headBranch,status --limit 100) + +# Combine both lists +ALL_WORKFLOWS=$(echo "$QUEUED_WORKFLOWS $IN_PROGRESS_WORKFLOWS" | jq -s 'add | unique_by(.databaseId)') + +if [[ "$ALL_WORKFLOWS" == "[]" ]]; then + print_status "No active workflows found." + exit 0 +fi + +# Parse and display active workflows +echo +print_warning "Found active workflows:" +echo "$ALL_WORKFLOWS" | jq -r '.[] | " - \(.displayTitle) (Branch: \(.headBranch), Status: \(.status), ID: \(.databaseId))"' + +echo +read -p "Do you want to cancel ALL these workflows? (y/N): " -n 1 -r +echo + +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + print_status "Cancelled by user." + exit 0 +fi + +# Cancel each workflow +print_status "Cancelling workflows..." +CANCELLED_COUNT=0 +FAILED_COUNT=0 + +while IFS= read -r WORKFLOW_ID; do + if [[ -n "$WORKFLOW_ID" ]]; then + print_status "Cancelling workflow ID: $WORKFLOW_ID" + if gh run cancel "$WORKFLOW_ID" 2>/dev/null; then + ((CANCELLED_COUNT++)) + else + print_error "Failed to cancel workflow ID: $WORKFLOW_ID" + ((FAILED_COUNT++)) + fi + fi +done < <(echo "$ALL_WORKFLOWS" | jq -r '.[].databaseId') + +echo +print_status "Summary:" +echo " - Cancelled: $CANCELLED_COUNT workflows" +if [[ $FAILED_COUNT -gt 0 ]]; then + echo " - Failed: $FAILED_COUNT workflows" +fi + +print_status "Done!" + +# Optional: Show remaining active workflows +echo +print_status "Checking for any remaining active workflows..." +REMAINING_QUEUED=$(gh run list --status queued --json databaseId --limit 10) +REMAINING_IN_PROGRESS=$(gh run list --status in_progress --json databaseId --limit 10) +REMAINING_ALL=$(echo "$REMAINING_QUEUED $REMAINING_IN_PROGRESS" | jq -s 'add | unique_by(.databaseId)') + +if [[ "$REMAINING_ALL" == "[]" ]]; then + print_status "All workflows successfully cancelled." +else + REMAINING_COUNT=$(echo "$REMAINING_ALL" | jq '. | length') + print_warning "Still $REMAINING_COUNT workflows active (may take a moment to update status)" +fi \ No newline at end of file diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 9eeadf5a2..36bd577e1 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -15,6 +15,7 @@ #include "Router.h" #include "SPILock.h" #include "TypeConversions.h" +#include "concurrency/LockGuard.h" #include "main.h" #include "xmodem.h" @@ -71,8 +72,12 @@ void PhoneAPI::handleStartConfig() LOG_DEBUG("Got %d files in manifest", filesManifest.size()); LOG_INFO("Start API client config"); - nodeInfoForPhone.num = 0; // Don't keep returning old nodeinfos - nodeInfoQueue.clear(); + // Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue. + { + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoForPhone = {}; + nodeInfoQueue.clear(); + } resetReadIndex(); } @@ -94,8 +99,12 @@ void PhoneAPI::close() onConnectionChanged(false); fromRadioScratch = {}; toRadioScratch = {}; - nodeInfoForPhone = {}; - nodeInfoQueue.clear(); + // Clear cached node info under lock because NimBLE callbacks can still be draining it. + { + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoForPhone = {}; + nodeInfoQueue.clear(); + } packetForPhone = NULL; filesManifest.clear(); fromRadioNum = 0; @@ -241,13 +250,20 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) LOG_DEBUG("Send My NodeInfo"); auto us = nodeDB->readNextMeshNode(readIndex); if (us) { - nodeInfoForPhone = TypeConversions::ConvertToNodeInfo(us); - nodeInfoForPhone.has_hops_away = false; - nodeInfoForPhone.is_favorite = true; + auto info = TypeConversions::ConvertToNodeInfo(us); + info.has_hops_away = false; + info.is_favorite = true; + { + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoForPhone = info; + } fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; - fromRadioScratch.node_info = nodeInfoForPhone; + fromRadioScratch.node_info = info; // Should allow us to resume sending NodeInfo in STATE_SEND_OTHER_NODEINFOS - nodeInfoForPhone.num = 0; + { + concurrency::LockGuard guard(&nodeInfoMutex); + nodeInfoForPhone.num = 0; + } } if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { // If client only wants node info, jump directly to sending nodes @@ -434,23 +450,30 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) case STATE_SEND_OTHER_NODEINFOS: { LOG_DEBUG("Send known nodes"); - if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) { - // Serve the next cached node without re-reading from the DB iterator. - nodeInfoForPhone = nodeInfoQueue.front(); - nodeInfoQueue.pop_front(); + meshtastic_NodeInfo infoToSend = {}; + { + concurrency::LockGuard guard(&nodeInfoMutex); + if (nodeInfoForPhone.num == 0 && !nodeInfoQueue.empty()) { + // Serve the next cached node without re-reading from the DB iterator. + nodeInfoForPhone = nodeInfoQueue.front(); + nodeInfoQueue.pop_front(); + } + infoToSend = nodeInfoForPhone; + if (infoToSend.num != 0) + nodeInfoForPhone = {}; } - if (nodeInfoForPhone.num != 0) { + if (infoToSend.num != 0) { // Just in case we stored a different user.id in the past, but should never happen going forward - sprintf(nodeInfoForPhone.user.id, "!%08x", nodeInfoForPhone.num); - LOG_DEBUG("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard, - nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name); + sprintf(infoToSend.user.id, "!%08x", infoToSend.num); + LOG_DEBUG("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", infoToSend.num, infoToSend.last_heard, + infoToSend.user.id, infoToSend.user.long_name); fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; - fromRadioScratch.node_info = nodeInfoForPhone; - nodeInfoForPhone = {}; + fromRadioScratch.node_info = infoToSend; prefetchNodeInfos(); } else { LOG_DEBUG("Done sending nodeinfo"); + concurrency::LockGuard guard(&nodeInfoMutex); nodeInfoQueue.clear(); state = STATE_SEND_FILEMANIFEST; // Go ahead and send that ID right now @@ -559,20 +582,23 @@ void PhoneAPI::prefetchNodeInfos() { bool added = false; // Keep the queue topped up so BLE reads stay responsive even if DB fetches take a moment. - while (nodeInfoQueue.size() < kNodePrefetchDepth) { - auto nextNode = nodeDB->readNextMeshNode(readIndex); - if (!nextNode) - break; + { + concurrency::LockGuard guard(&nodeInfoMutex); + while (nodeInfoQueue.size() < kNodePrefetchDepth) { + auto nextNode = nodeDB->readNextMeshNode(readIndex); + if (!nextNode) + break; - auto info = TypeConversions::ConvertToNodeInfo(nextNode); - bool isUs = info.num == nodeDB->getNodeNum(); - info.hops_away = isUs ? 0 : info.hops_away; - info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard; - info.snr = isUs ? 0 : info.snr; - info.via_mqtt = isUs ? false : info.via_mqtt; - info.is_favorite = info.is_favorite || isUs; - nodeInfoQueue.push_back(info); - added = true; + auto info = TypeConversions::ConvertToNodeInfo(nextNode); + bool isUs = info.num == nodeDB->getNodeNum(); + info.hops_away = isUs ? 0 : info.hops_away; + info.last_heard = isUs ? getValidTime(RTCQualityFromNet) : info.last_heard; + info.snr = isUs ? 0 : info.snr; + info.via_mqtt = isUs ? false : info.via_mqtt; + info.is_favorite = info.is_favorite || isUs; + nodeInfoQueue.push_back(info); + added = true; + } } if (added) @@ -614,10 +640,17 @@ bool PhoneAPI::available() case STATE_SEND_COMPLETE_ID: return true; - case STATE_SEND_OTHER_NODEINFOS: - if (nodeInfoQueue.empty()) - prefetchNodeInfos(); + case STATE_SEND_OTHER_NODEINFOS: { + concurrency::LockGuard guard(&nodeInfoMutex); + if (nodeInfoQueue.empty()) { + // Drop the lock before prefetching; prefetchNodeInfos() will re-acquire it. + goto PREFETCH_NODEINFO; + } + } return true; // Always say we have something, because we might need to advance our state machine + PREFETCH_NODEINFO: + prefetchNodeInfos(); + return true; case STATE_SEND_PACKETS: { if (!queueStatusPacketForPhone) queueStatusPacketForPhone = service->getQueueStatusForPhone(); diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index 692fdd0b9..a8d0faa28 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -1,6 +1,7 @@ #pragma once #include "Observer.h" +#include "concurrency/Lock.h" #include "mesh-pb-constants.h" #include "meshtastic/portnums.pb.h" #include @@ -84,6 +85,8 @@ class PhoneAPI std::deque nodeInfoQueue; // Tunable size of the node info cache so we can keep BLE reads non-blocking. static constexpr size_t kNodePrefetchDepth = 4; + // Protect nodeInfoForPhone + nodeInfoQueue because NimBLE callbacks run in a separate FreeRTOS task. + concurrency::Lock nodeInfoMutex; meshtastic_ToRadio toRadioScratch = { 0}; // this is a static scratch object, any data must be copied elsewhere before returning diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 4b0c33609..eb1d909f1 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -139,7 +139,7 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks { bluetoothPhoneAPI->phoneWants = true; bluetoothPhoneAPI->setIntervalFromNow(0); - std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); + std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); // BLE callbacks run in NimBLE task if (!bluetoothPhoneAPI->hasChecked) { // Fetch payload on demand; prefetch keeps this fast for the first read. diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini index 4e6a592de..49a8a71c7 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -17,6 +17,7 @@ extends = native_base build_flags = ${native_base.build_flags} !pkg-config --libs libulfius --silence-errors || : !pkg-config --libs openssl --silence-errors || : + !pkg-config --cflags --libs sdl2 --silence-errors || : [env:native-tft] extends = native_base