Skip to content

Commit 59629d2

Browse files
committed
Prevent overlapping scheduler tasks after immediate restart
1 parent ba94975 commit 59629d2

9 files changed

Lines changed: 159 additions & 6 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ jobs:
3232
- uses: actions/setup-python@v6
3333
with:
3434
python-version: "3.12"
35+
- name: Test scheduler lifecycle without hardware
36+
run: python test/check_lifecycle.py
3537
- run: python -m pip install platformio==6.1.19
3638
- run: pio pkg pack . --output FindMyAdv.tar.gz
3739
- uses: actions/upload-artifact@v7

‎CHANGELOG.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## Unreleased — Lifecycle regression
4+
5+
- Drain the previous scheduler before restarting with `begin()` after `end()`.
6+
Previously an immediate restart could reset the stop request and leave two
7+
scheduler tasks active. The same bounded shutdown path serves `reconfigure()`.
8+
- Add host-side regression tests compiling the real implementation against fake
9+
FreeRTOS/BLE calls, covering repeated restart and switching to manual polling.
10+
- No API, package version, radio payload, key or existing release-tag changes.
11+
312
All notable changes to FindMyAdv are documented here. The format follows
413
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
514
[Semantic Versioning](https://semver.org/).

‎docs/API.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ when neither key is valid, BLE initialization fails, or the task cannot start.
3333
Calling `begin()` while already running is a no-op that returns `true`. Use
3434
`reconfigure()` to replace a live configuration.
3535

36+
After `end()`, an immediate `begin()` waits for the previous private scheduler
37+
to exit before starting a replacement, including when switching to manual
38+
polling. Shutdown waiting is bounded at one second; no second task is created
39+
while the old task is still present. Serialize lifecycle calls in your application.
40+
3641
### `bool reconfigure(const FindMyAdvConfig &config)`
3742

3843
Stops advertising, safely copies the new configuration, and restarts the

‎src/FindMyAdv.cpp‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,15 @@ bool FindMyAdvClass::begin(const FindMyAdvConfig &configuration) {
435435
return false;
436436
}
437437

438+
// end() requests asynchronous task shutdown. Drain that task before
439+
// resetting stopRequested or overwriting its handle on a new begin().
440+
const uint32_t waitStarted = millis();
441+
while (schedulerTask && millis() - waitStarted < 1000) delay(10);
442+
if (schedulerTask) {
443+
vTaskDelete(schedulerTask);
444+
schedulerTask = nullptr;
445+
}
446+
438447
cfg = configuration;
439448
copyKey(appleText, cfg.appleAdvertisementKeyBase64);
440449
copyKey(googleText, cfg.googleAdvertisementEidHex);
@@ -521,12 +530,6 @@ bool FindMyAdvClass::reconfigure(const FindMyAdvConfig &configuration) {
521530
next.googleAdvertisementEidHex = nextGoogle;
522531

523532
end();
524-
const uint32_t waitStarted = millis();
525-
while (schedulerTask && millis() - waitStarted < 1000) delay(10);
526-
if (schedulerTask) {
527-
vTaskDelete(schedulerTask);
528-
schedulerTask = nullptr;
529-
}
530533
return begin(next);
531534
}
532535

‎test/check_lifecycle.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Run real library lifecycle code against a deterministic host scheduler.
2+
3+
This catches overlapping task creation; it does not emulate BLE or prove RF.
4+
Requires a C++11 compiler (g++ by default, or CXX).
5+
"""
6+
import os
7+
from pathlib import Path
8+
import subprocess
9+
import tempfile
10+
11+
ROOT = Path(__file__).resolve().parents[1]
12+
with tempfile.TemporaryDirectory(prefix="findmyadv-lifecycle-") as directory:
13+
executable = Path(directory) / ("lifecycle.exe" if os.name == "nt" else "lifecycle")
14+
subprocess.run([
15+
os.environ.get("CXX", "g++"), "-std=c++11",
16+
"-DFINDMYADV_DISABLE_BUILTIN_ACCELEROMETER=1",
17+
"-I", str(ROOT / "test/host"), str(ROOT / "test/host/lifecycle.cpp"),
18+
"-o", str(executable),
19+
], check=True)
20+
subprocess.run([str(executable)], check=True)
21+
print("Lifecycle: immediate restart, repeated restart, reconfigure, manual mode and bounded shutdown passed")

‎test/host/Arduino.h‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#pragma once
2+
#include <cstddef>
3+
#include <cstdint>
4+
#include <cstdlib>
5+
6+
using TaskHandle_t = void *;
7+
constexpr int pdPASS = 1;
8+
constexpr uint32_t pdMS_TO_TICKS(uint32_t ms) { return ms; }
9+
uint32_t millis();
10+
void delay(uint32_t ms);
11+
void vTaskDelay(uint32_t ticks);
12+
void vTaskDelete(TaskHandle_t task);
13+
int xTaskCreate(void (*entry)(void *), const char *, uint32_t, void *, int,
14+
TaskHandle_t *handle);

‎test/host/NimBLEDevice.h‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#pragma once
2+
#include <cstdint>
3+
4+
enum class NimBLETxPowerType { Advertise };
5+
struct NimBLEDevice {
6+
static bool isInitialized() { return true; }
7+
static bool init(const char *) { return true; }
8+
static void setPower(int8_t, NimBLETxPowerType) {}
9+
static bool setOwnAddr(uint8_t *) { return true; }
10+
static bool setOwnAddrType(int) { return true; }
11+
};
12+
constexpr int BLE_OWN_ADDR_RANDOM = 1;
13+
constexpr int BLE_GAP_CONN_MODE_NON = 0;
14+
constexpr int BLE_GAP_DISC_MODE_NON = 0;
15+
constexpr int BLE_HS_FOREVER = -1;
16+
struct ble_gap_adv_params {
17+
int conn_mode, disc_mode;
18+
uint16_t itvl_min, itvl_max;
19+
};
20+
inline int ble_gap_adv_stop() { return 0; }
21+
inline int ble_gap_adv_set_data(uint8_t *, uint8_t) { return 0; }
22+
inline int ble_gap_adv_start(int, void *, int, ble_gap_adv_params *, void *, void *) {
23+
return 0;
24+
}

‎test/host/lifecycle.cpp‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Compile the actual implementation; only hardware/FreeRTOS calls are faked.
2+
#include <cstdio>
3+
#include "../../src/FindMyAdv.cpp"
4+
5+
// Exit normally on failure, avoiding Windows assertion-dialog side effects.
6+
#define CHECK(condition) do { if (!(condition)) { \
7+
std::fprintf(stderr, "Lifecycle check failed at line %d: %s\n", __LINE__, #condition); \
8+
std::exit(1); \
9+
} } while (false)
10+
11+
static uint32_t clockMs = 0;
12+
static unsigned liveTasks = 0, createdTasks = 0, overlappingTasks = 0;
13+
static unsigned forcedDeletes = 0;
14+
static bool stalledTask = false;
15+
16+
uint32_t millis() { return clockMs; }
17+
void delay(uint32_t ms) {
18+
clockMs += ms;
19+
// Model the old task seeing end()'s stop request on its next wakeup.
20+
if (!stalledTask && stopRequested && schedulerTask) {
21+
--liveTasks;
22+
schedulerTask = nullptr;
23+
}
24+
}
25+
void vTaskDelay(uint32_t ticks) { delay(ticks); }
26+
void vTaskDelete(TaskHandle_t task) {
27+
CHECK(task != nullptr); // The test never actually enters a scheduler loop.
28+
CHECK(liveTasks > 0);
29+
--liveTasks;
30+
++forcedDeletes;
31+
}
32+
int xTaskCreate(void (*)(void *), const char *, uint32_t, void *, int,
33+
TaskHandle_t *handle) {
34+
if (liveTasks) ++overlappingTasks;
35+
++liveTasks;
36+
*handle = reinterpret_cast<void *>(static_cast<uintptr_t>(++createdTasks));
37+
return pdPASS;
38+
}
39+
40+
int main() {
41+
FindMyAdvConfig config;
42+
config.googleAdvertisementEidHex = "0000000000000000000000000000000000000000";
43+
CHECK(FindMyAdv.begin(config));
44+
CHECK(liveTasks == 1);
45+
const unsigned initialCreated = createdTasks;
46+
CHECK(FindMyAdv.begin(config)); // Already running is still a no-op.
47+
CHECK(createdTasks == initialCreated);
48+
for (int i = 0; i < 5; ++i) {
49+
FindMyAdv.end();
50+
CHECK(FindMyAdv.begin(config));
51+
CHECK(overlappingTasks == 0);
52+
CHECK(liveTasks == 1);
53+
}
54+
CHECK(FindMyAdv.reconfigure(config));
55+
CHECK(liveTasks == 1 && overlappingTasks == 0);
56+
FindMyAdv.end();
57+
config.backgroundTask = false;
58+
CHECK(FindMyAdv.begin(config));
59+
CHECK(liveTasks == 0); // Background -> manual must drain the old task.
60+
FindMyAdv.poll();
61+
FindMyAdv.end();
62+
config.backgroundTask = true;
63+
CHECK(FindMyAdv.begin(config));
64+
FindMyAdv.end();
65+
stalledTask = true;
66+
CHECK(FindMyAdv.begin(config));
67+
CHECK(forcedDeletes == 1); // Preserve the bounded shutdown fallback.
68+
CHECK(liveTasks == 1 && overlappingTasks == 0);
69+
stalledTask = false;
70+
FindMyAdv.end();
71+
delay(100);
72+
CHECK(liveTasks == 0);
73+
}

‎test/host/sdkconfig.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#pragma once
2+
// Host tests use the default NimBLE branch with the sensor compiled out.

0 commit comments

Comments
 (0)