1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! Slack bot process lifecycle: PID-file recording and precise `stop`.
//!
//! Why: `tm slack stop` must terminate exactly the bot `tm slack start` launched —
//! not any process whose argv happens to contain "slack start" (the old
//! `pkill -f "slack start"` could kill an editor, a grep, or an unrelated tool).
//! A PID file records the one true process so `stop` signals precisely it. This
//! concern is self-contained (no Socket-Mode, executor, or proxy coupling), so it
//! lives in its own module to keep `slack/mod.rs` under the 500-SLOC cap (#2549).
//! What: [`pid_file_path`] locates the file, [`write_pid_file`] records the PID at
//! startup, [`PidFileGuard`] removes it on every exit path of `run`, and
//! [`stop_via_pid_file`] signals the recorded process, returning a typed
//! [`StopOutcome`].
//! Test: `slack::tests` covers `pid_file_path`, the guard's drop behavior, and the
//! stop paths (missing / stale / garbage PID file).
use Path;
/// The PID-file name (under `~/.trusty-mpm`) for the foreground Slack bot.
///
/// Why: `tm slack stop` must terminate exactly the bot `tm slack start` launched —
/// not any process whose argv happens to contain "slack start" (the old
/// `pkill -f "slack start"` could kill an editor, a grep, or an unrelated tool).
/// A PID file records the one true process so `stop` signals precisely it.
const SLACK_PID_FILE: &str = "slack.pid";
/// Absolute path to the Slack bot PID file (`~/.trusty-mpm/slack.pid`).
///
/// Why: `start` (writer) and `stop` (reader) must agree on one location; deriving
/// it from the shared [`FRAMEWORK_DIR_NAME`](crate::core::paths::FRAMEWORK_DIR_NAME)
/// home root keeps it consistent with the rest of the framework's state files.
/// What: `~/.trusty-mpm/slack.pid`, falling back to `./.trusty-mpm/slack.pid` when
/// the home directory cannot be resolved (mirrors `FrameworkPaths::default`).
/// Test: `pid_file_path_is_under_framework_root`.
/// Record the current process id in the Slack PID file.
///
/// Why: so `tm slack stop` can signal exactly this foreground bot instead of
/// pattern-matching process argv. Written once at startup, removed on stop.
/// What: creates `~/.trusty-mpm` if needed and writes the current PID as text.
/// A write failure is logged (stderr) and swallowed — an unwritable PID file must
/// not prevent the bot from running; `stop` simply falls back to a manual kill.
/// Test: `pid_file_guard_removes_on_drop` exercises the sibling guard; the write
/// itself is side-effect-only (creates a real file) and covered manually.
pub
/// RAII guard that best-effort removes the Slack PID file when dropped.
///
/// Why: [`run`](super::run) writes the PID file at startup so `tm slack stop` can
/// signal exactly this process, but a clean exit (e.g. a permanent `disconnect`),
/// an early `?` return, or a panic would otherwise leave a STALE PID file behind —
/// a later `tm slack stop` could then SIGTERM a recycled, unrelated PID. Holding
/// the path in a `Drop` guard removes the file on EVERY exit path of `run`.
/// What: on drop, removes the file at `path`. Best-effort: a `NotFound` is
/// expected (e.g. `tm slack stop` already removed it) and silent; any other
/// removal error is logged to stderr and swallowed — cleanup must never panic.
/// Test: `pid_file_guard_removes_on_drop`, `pid_file_guard_drop_missing_is_silent`.
pub
/// The result of a `tm slack stop` attempt, for a precise operator message.
///
/// Why: `stop` must report honestly — it should NOT print "no process found" when
/// it actually hit an error (an unreadable PID file or a failed signal). The old
/// `pkill` path conflated "no match" with "pkill itself errored"; a typed outcome
/// keeps the three cases distinct.
/// What: `Stopped(pid)` signalled a live bot; `NotRunning` found no PID file or a
/// stale one (process already gone); `Failed(msg)` is a real error.
/// Test: `stop_via_pid_file_*` cover the missing-file and stale-pid paths.
/// Stop the foreground Slack bot recorded in the PID file at `path`.
///
/// Why: this is the precise replacement for `pkill -f "slack start"` — it signals
/// exactly the process `tm slack start` recorded, so it can never kill an
/// unrelated process, and it distinguishes "not running" from a genuine failure.
/// Taking `path` as a parameter keeps it unit-testable against a temp file.
/// What: reads the PID, sends `SIGTERM` to it (Unix), removes the PID file, and
/// returns a [`StopOutcome`]. A missing PID file → `NotRunning`; a recorded PID
/// that no longer exists → `NotRunning` (stale file, still cleaned up); an
/// unreadable/garbage PID file or a signal error other than "no such process" →
/// `Failed`.
/// Test: `stop_via_pid_file_missing_is_not_running`,
/// `stop_via_pid_file_stale_pid_is_not_running`.
/// Send `SIGTERM` to `pid`, classifying the result.
///
/// Why: `stop_via_pid_file` must distinguish a successful signal from "the process
/// is already gone" (a stale PID file, not an error) and from a real failure.
/// Isolating the raw `kill(2)` call keeps that classification in one place.
/// What: on Unix, calls `libc::kill(pid, SIGTERM)`; `ESRCH` (no such process) maps
/// to [`StopOutcome::NotRunning`], any other errno to [`StopOutcome::Failed`], and
/// success to [`StopOutcome::Stopped`]. On non-Unix it reports unsupported.
/// Test: covered indirectly via `stop_via_pid_file_stale_pid_is_not_running`.