docs(01-safety-foundation): create phase plan
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
---
|
||||
phase: 01-safety-foundation
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/terminal.rs
|
||||
- src/signals.rs
|
||||
autonomous: true
|
||||
requirements:
|
||||
- LIFE-01
|
||||
- LIFE-02
|
||||
- LIFE-03
|
||||
- LIFE-04
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "When the app panics, raw mode is disabled and a friendly BBS message is printed before exiting"
|
||||
- "Panic details go to stderr for sysop; user sees only the friendly message"
|
||||
- "SIGHUP sets a terminate flag that the event loop can poll"
|
||||
- "SIGTERM sets a terminate flag that the event loop can poll"
|
||||
- "Writing to stdout after SSH disconnect does not panic (broken pipe handled)"
|
||||
- "Terminal init uses main screen buffer (no alternate screen) with raw mode and clear"
|
||||
- "Terminal restore disables raw mode and shows cursor without calling LeaveAlternateScreen"
|
||||
artifacts:
|
||||
- path: "src/terminal.rs"
|
||||
provides: "init_terminal(), restore_terminal(), install_panic_hook()"
|
||||
contains: "disable_raw_mode"
|
||||
- path: "src/signals.rs"
|
||||
provides: "SignalFlags, register_signals()"
|
||||
contains: "AtomicBool"
|
||||
key_links:
|
||||
- from: "src/terminal.rs"
|
||||
to: "crossterm"
|
||||
via: "enable_raw_mode, disable_raw_mode, Clear, cursor::Show"
|
||||
pattern: "enable_raw_mode"
|
||||
- from: "src/terminal.rs"
|
||||
to: "ratatui"
|
||||
via: "Terminal::with_options(Viewport::Fullscreen)"
|
||||
pattern: "Viewport::Fullscreen"
|
||||
- from: "src/signals.rs"
|
||||
to: "signal-hook"
|
||||
via: "flag::register for SIGHUP and SIGTERM"
|
||||
pattern: "flag::register"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement terminal initialization (main screen, no alternate buffer), terminal restoration, panic hook with friendly BBS messaging, UNIX signal handling for SIGHUP/SIGTERM, and broken pipe safety. These are the safety primitives that all other code depends on.
|
||||
|
||||
Purpose: The login-shell deployment context means any terminal corruption locks out SSH users. This plan establishes the safety envelope: every exit path (panic, signal, normal quit) restores the terminal correctly.
|
||||
|
||||
Output: `src/terminal.rs` with init/restore/panic-hook, `src/signals.rs` with signal flag registration.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/ruohki/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/ruohki/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-safety-foundation/01-RESEARCH.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Implement terminal init/restore and panic hook</name>
|
||||
<files>src/terminal.rs</files>
|
||||
<action>
|
||||
Create `src/terminal.rs` with:
|
||||
|
||||
1. **`init_terminal() -> std::io::Result<Terminal<CrosstermBackend<std::io::Stdout>>>`**
|
||||
- Call `crossterm::terminal::enable_raw_mode()?`
|
||||
- Call `crossterm::execute!(stdout(), Clear(ClearType::All), MoveTo(0, 0))?` — clears main screen buffer for immersive BBS feel
|
||||
- Create `CrosstermBackend::new(stdout())`
|
||||
- Return `Terminal::with_options(backend, TerminalOptions { viewport: Viewport::Fullscreen })`
|
||||
- IMPORTANT: Do NOT enter alternate screen. Do NOT use `ratatui::init()`.
|
||||
|
||||
2. **`restore_terminal()`** (no return value — must never panic)
|
||||
- `let _ = crossterm::terminal::disable_raw_mode();`
|
||||
- `let _ = crossterm::execute!(std::io::stdout(), crossterm::cursor::Show);`
|
||||
- Do NOT call `ratatui::restore()` (it calls LeaveAlternateScreen which we never entered)
|
||||
- Every operation uses `let _ =` to suppress errors — this runs in cleanup paths including panic hook
|
||||
|
||||
3. **`install_panic_hook()`**
|
||||
- Call `std::panic::take_hook()` to capture the original hook
|
||||
- Call `std::panic::set_hook(Box::new(move |panic_info| { ... }))` with a closure that:
|
||||
a. Calls `restore_terminal()` to clean up the terminal
|
||||
b. Prints friendly BBS-themed message to stderr with `eprintln!()`:
|
||||
```
|
||||
\r\n+----------------------------------------------+
|
||||
| SYSTEM ERROR: An unexpected fault occurred. |
|
||||
| The BBS has exited safely. |
|
||||
| SysOp has been notified. |
|
||||
+----------------------------------------------+\r
|
||||
```
|
||||
(Use `\r\n` and trailing `\r` for proper display after raw mode restoration)
|
||||
c. Calls the original hook with `panic_info` — this outputs the technical backtrace to stderr (captured by systemd journal / SSH log)
|
||||
- All I/O in the hook uses `let _ =` or `eprintln!()` (which silently ignores errors). Never `unwrap()` inside the hook.
|
||||
|
||||
4. **Broken pipe safety (LIFE-04):** At the module level, add a doc comment noting that all write operations in the TUI must use `let _ =` or handle `ErrorKind::BrokenPipe`. The actual enforcement happens in the event loop (Plan 03), but restore_terminal() already follows this pattern with `let _ =` on every call.
|
||||
|
||||
Use these imports:
|
||||
```rust
|
||||
use std::io::stdout;
|
||||
use crossterm::{
|
||||
execute,
|
||||
terminal::{enable_raw_mode, disable_raw_mode, Clear, ClearType},
|
||||
cursor::{MoveTo, Show},
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal, TerminalOptions, Viewport};
|
||||
```
|
||||
|
||||
Export the Terminal type alias for use in app.rs:
|
||||
```rust
|
||||
pub type Term = Terminal<CrosstermBackend<std::io::Stdout>>;
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
`cargo build` succeeds (after adding `mod terminal;` temporarily to main.rs or just checking compilation).
|
||||
|
||||
Review: `init_terminal` does NOT contain `EnterAlternateScreen`. `restore_terminal` does NOT contain `LeaveAlternateScreen`. Panic hook does NOT contain any `unwrap()` or `?` operator.
|
||||
</verify>
|
||||
<done>
|
||||
Terminal init enters raw mode and clears main screen without alternate screen buffer. Restore disables raw mode and shows cursor with `let _ =` on every call. Panic hook restores terminal, prints friendly BBS message, then delegates to original hook for technical details.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Implement signal handling and file logging stub</name>
|
||||
<files>src/signals.rs</files>
|
||||
<action>
|
||||
Create `src/signals.rs` with:
|
||||
|
||||
1. **`SignalFlags` struct:**
|
||||
```rust
|
||||
pub struct SignalFlags {
|
||||
pub terminate: Arc<AtomicBool>,
|
||||
}
|
||||
```
|
||||
|
||||
2. **`register_signals() -> std::io::Result<SignalFlags>`:**
|
||||
- Create `terminate = Arc::new(AtomicBool::new(false))`
|
||||
- Register SIGTERM: `signal_hook::flag::register(signal_hook::consts::SIGTERM, Arc::clone(&terminate))?`
|
||||
- Register SIGHUP: `signal_hook::flag::register(signal_hook::consts::SIGHUP, Arc::clone(&terminate))?`
|
||||
- Do NOT register SIGINT — Ctrl+C is handled as a crossterm key event in the event loop (avoids double-handling per research findings)
|
||||
- Return `Ok(SignalFlags { terminate })`
|
||||
|
||||
3. **`SignalFlags::should_terminate(&self) -> bool`:**
|
||||
- Returns `self.terminate.load(Ordering::Relaxed)`
|
||||
- Convenience method for polling in the event loop
|
||||
|
||||
4. **LIFE-03 (logging):** Add a simple `init_logging()` function stub. For Phase 1, logging is minimal — the requirement is that after TUI init, nothing writes to stdout/stderr directly. The panic hook is the exception (it restores terminal first). For now, `init_logging()` is a no-op that returns `()`. A doc comment should note: "Phase 1: no file logging needed yet. When LIFE-03 becomes relevant in Phase 2+, replace this with file-based logging. For now, the rule is simply: do not write to stdout/stderr after terminal init except through ratatui's draw cycle or after calling restore_terminal()."
|
||||
|
||||
Use these imports:
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use signal_hook::consts::signal::{SIGHUP, SIGTERM};
|
||||
use signal_hook::flag as signal_flag;
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
`cargo build` succeeds (after adding `mod signals;` temporarily to main.rs).
|
||||
|
||||
Review: SIGINT is NOT registered (only SIGHUP and SIGTERM). `should_terminate()` uses `Ordering::Relaxed`.
|
||||
</verify>
|
||||
<done>
|
||||
SIGHUP and SIGTERM set a shared AtomicBool flag. SignalFlags provides a `should_terminate()` method for polling. SIGINT is deliberately not registered (Ctrl+C handled via crossterm key events). Logging stub documents the stdout/stderr rule for LIFE-03.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `cargo build` compiles with both new modules
|
||||
2. `init_terminal()` uses `Viewport::Fullscreen` and never enters alternate screen
|
||||
3. `restore_terminal()` uses `let _ =` for every operation
|
||||
4. Panic hook contains no `unwrap()` or `?` — only `let _ =` and `eprintln!()`
|
||||
5. Signal registration covers SIGHUP and SIGTERM but NOT SIGINT
|
||||
6. `should_terminate()` polls the AtomicBool
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Terminal initializes on main screen buffer with raw mode
|
||||
- Panic hook restores terminal and prints BBS-friendly message
|
||||
- Signals set a pollable flag for clean shutdown
|
||||
- No alternate screen buffer is used anywhere
|
||||
- All cleanup paths are panic-safe (no unwrap in error paths)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-safety-foundation/01-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user