Skip to content

Commit 074b7a5

Browse files
Merge pull request #8 from bitcoin3us/docs/task-supervision-and-boot-ctrlc
Document TaskManager.create_supervised_task() and boot-time Ctrl-C handling
2 parents 680a96a + 0ff7077 commit 074b7a5

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

docs/architecture/boot-sequence.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ MicroPythonOS consists of several core components that initialize and manage the
77
- **internal_filesystem/main.py**: Hands off execution to /lib/mpos/main.py by importing it
88

99
- **/lib/mpos/main.py**:
10+
- Disables the Ctrl-C interrupt character for the duration of the boot
1011
- Detects the hardware board
1112
- Initializes the filesystem driver
1213
- Mounts the freezefs into /builtin/
@@ -25,3 +26,15 @@ MicroPythonOS consists of several core components that initialize and manage the
2526
See [Filesystem Layout](filesystem.md) for where apps and data are stored.
2627

2728
See [Service](../frameworks/service.md) for details on writing boot services.
29+
30+
## Connecting over serial during boot
31+
32+
Tools that open the serial port send Ctrl-C to interrupt whatever is running — `mpremote` does this whenever it enters the raw REPL. Arriving mid-boot, that would abort the boot scripts and leave the device at a bare REPL shell with the OS only half-started, which is easily mistaken for a broken build: the prompt looks normal, but apps are missing and files placed in `/lib` appear to have no effect.
33+
34+
To prevent this, `/lib/mpos/main.py` disables the interrupt character (`micropython.kbd_intr(-1)`) as its first action, and the asyncio REPL manages it from then on. It is restored on the paths that fall back to the REPL shell, so a failed boot still gives you an interruptible prompt.
35+
36+
Practical consequences:
37+
38+
- Connecting while the device boots is safe; the command waits for the boot to finish instead of corrupting it.
39+
- Wait for `Starting asyncio REPL...` on the serial console to know the boot completed.
40+
- Ctrl-C can no longer break into a boot that hangs — use the board's reset button in that case.

docs/frameworks/task-manager.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,35 @@ Create and schedule a background task.
191191
TaskManager.create_task(self.background_work())
192192
```
193193

194+
#### `TaskManager.create_supervised_task(coroutine_factory, restart_delay_ms=200)`
195+
196+
Create a background task that is automatically restarted if it dies unexpectedly. Used by the OS for long-lived services that must never stay down, such as the asyncio REPL console.
197+
198+
**Parameters:**
199+
200+
- `coroutine_factory` - A **callable returning a fresh coroutine** (not a coroutine object), because a new one is needed for every restart
201+
- `restart_delay_ms` (int) - Delay before each restart, in milliseconds
202+
203+
**Returns:**
204+
205+
- Task object for the supervisor (cancel it to stop supervision)
206+
207+
**Restart behavior:**
208+
209+
- Task raises `Exception` or `KeyboardInterrupt` → logged and restarted after `restart_delay_ms`
210+
- Task returns normally → supervision ends (a clean exit is treated as intentional)
211+
- Supervisor task is cancelled → supervision ends, no restart
212+
213+
**Example:**
214+
```python
215+
def make_listener():
216+
return my_service.listen_forever()
217+
218+
TaskManager.create_supervised_task(make_listener)
219+
```
220+
221+
Note the factory: passing `create_supervised_task(my_service.listen_forever())` would hand over a single coroutine object that cannot be re-run after it dies.
222+
194223
### Sleep Operations
195224

196225
#### `TaskManager.sleep(seconds)`
@@ -473,10 +502,15 @@ async def safe_task(self):
473502

474503
**Key features:**
475504
- `create_task()` - Wraps `asyncio.create_task()`
505+
- `create_supervised_task()` - Restarts a task that dies from an exception or KeyboardInterrupt
476506
- `sleep()` / `sleep_ms()` - Wrap `asyncio.sleep()`
477507
- `wait_for()` - Wraps `asyncio.wait_for()` with timeout handling
478508
- `notify_event()` - Creates `asyncio.Event()` objects
479509

510+
**KeyboardInterrupt resilience:**
511+
512+
MicroPython's `run_until_complete()` only catches `CancelledError` and `Exception`, so a `KeyboardInterrupt` raised inside any task (for example from a stray Ctrl-C sent by a host connecting over serial) escapes the event loop and would otherwise terminate every running task while the LVGL UI keeps running on its hardware timer. `TaskManager.start()` catches this and re-enters `asyncio.run()`; the task queue survives the unwind, so the remaining tasks resume.
513+
480514
**Thread model:**
481515
- All async tasks run on main asyncio event loop
482516
- No separate threads created (unless using `_thread` module separately)

0 commit comments

Comments
 (0)