1use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};
8use std::thread::{self, JoinHandle};
9use std::time::{Duration, Instant};
10
11use crate::{DataSource, MIN_POLL_INTERVAL, ProcessSignal, Snapshot};
12
13const EVENT_QUEUE: usize = 8;
16
17#[derive(Debug)]
19pub enum Command {
20 Signal { pid: u32, signal: ProcessSignal },
22 SetInterval(Duration),
24 SetProcessLimit(Option<usize>),
26}
27
28#[derive(Debug)]
30pub enum Event {
31 Snapshot(Box<Snapshot>),
33 SignalResult {
35 pid: u32,
36 signal: ProcessSignal,
37 result: Result<(), String>,
38 },
39 Error(String),
41}
42
43pub struct Worker {
45 commands: Option<Sender<Command>>,
46 events: Receiver<Event>,
47 handle: Option<JoinHandle<()>>,
48}
49
50impl Worker {
51 pub fn spawn<S, F>(make_source: F, interval: Duration) -> Self
55 where
56 S: DataSource + 'static,
57 F: FnOnce() -> S + Send + 'static,
58 {
59 Self::spawn_with_notify(make_source, interval, || {})
60 }
61
62 pub fn spawn_with_notify<S, F, N>(make_source: F, interval: Duration, notify: N) -> Self
66 where
67 S: DataSource + 'static,
68 F: FnOnce() -> S + Send + 'static,
69 N: Fn() + Send + 'static,
70 {
71 let (command_tx, command_rx) = mpsc::channel();
72 let (event_tx, event_rx) = mpsc::sync_channel(EVENT_QUEUE);
75 let handle = thread::Builder::new()
76 .name("rigtop-collector".to_owned())
77 .spawn(move || {
78 let events = Notifying {
79 sender: event_tx,
80 notify,
81 };
82 run(make_source(), interval, &command_rx, &events)
83 })
84 .expect("failed to spawn the collector thread");
85 Self {
86 commands: Some(command_tx),
87 events: event_rx,
88 handle: Some(handle),
89 }
90 }
91
92 pub fn send(&self, command: Command) {
94 if let Some(tx) = &self.commands {
95 let _ = tx.send(command);
96 }
97 }
98
99 pub fn try_recv(&self) -> Option<Event> {
101 self.events.try_recv().ok()
102 }
103}
104
105impl Drop for Worker {
106 fn drop(&mut self) {
107 self.commands.take();
109 if let Some(handle) = self.handle.take() {
110 let _ = handle.join();
111 }
112 }
113}
114
115struct Notifying<N: Fn()> {
118 sender: SyncSender<Event>,
119 notify: N,
120}
121
122impl<N: Fn()> Notifying<N> {
123 fn send(&self, event: Event) -> Result<(), TrySendError<Event>> {
127 match self.sender.try_send(event) {
128 Ok(()) | Err(TrySendError::Full(_)) => {
129 (self.notify)();
131 Ok(())
132 }
133 Err(err @ TrySendError::Disconnected(_)) => Err(err),
134 }
135 }
136}
137
138fn run<S: DataSource, N: Fn()>(
139 mut source: S,
140 mut interval: Duration,
141 commands: &Receiver<Command>,
142 events: &Notifying<N>,
143) {
144 source.set_interval(interval);
147 let mut last_poll = Instant::now();
148 let mut next_poll = last_poll + MIN_POLL_INTERVAL;
149 loop {
150 let timeout = next_poll.saturating_duration_since(Instant::now());
151 match commands.recv_timeout(timeout) {
152 Ok(Command::Signal { pid, signal }) => {
153 let result = source
154 .signal_process(pid, signal)
155 .map_err(|e| e.to_string());
156 if events
157 .send(Event::SignalResult {
158 pid,
159 signal,
160 result,
161 })
162 .is_err()
163 {
164 return;
165 }
166 }
167 Ok(Command::SetInterval(new_interval)) => {
168 interval = new_interval.max(MIN_POLL_INTERVAL);
169 source.set_interval(interval);
170 next_poll = last_poll + interval;
171 }
172 Ok(Command::SetProcessLimit(limit)) => source.set_process_limit(limit),
173 Err(RecvTimeoutError::Timeout) => {
174 let event = match source.poll() {
175 Ok(snapshot) => Event::Snapshot(Box::new(snapshot)),
176 Err(err) => Event::Error(err.to_string()),
177 };
178 last_poll = Instant::now();
179 next_poll = (next_poll + interval).max(last_poll + MIN_POLL_INTERVAL);
182 if events.send(event).is_err() {
183 return;
184 }
185 }
186 Err(RecvTimeoutError::Disconnected) => return,
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::SourceError;
195
196 struct FakeSource {
198 polls: u64,
199 signals: Sender<(u32, ProcessSignal)>,
200 }
201
202 impl DataSource for FakeSource {
203 fn poll(&mut self) -> Result<Snapshot, SourceError> {
204 self.polls += 1;
205 Ok(Snapshot {
206 timestamp_ms: self.polls,
207 ..Default::default()
208 })
209 }
210
211 fn signal_process(&mut self, pid: u32, signal: ProcessSignal) -> Result<(), SourceError> {
212 self.signals.send((pid, signal)).unwrap();
213 if pid == 1 {
214 Err(SourceError::Disconnected("denied".to_owned()))
215 } else {
216 Ok(())
217 }
218 }
219 }
220
221 fn recv(worker: &Worker) -> Event {
222 let deadline = Instant::now() + Duration::from_secs(5);
223 loop {
224 if let Some(event) = worker.try_recv() {
225 return event;
226 }
227 assert!(Instant::now() < deadline, "no event from worker");
228 thread::sleep(Duration::from_millis(5));
229 }
230 }
231
232 #[test]
233 fn notify_runs_after_each_event() {
234 use std::sync::Arc;
235 use std::sync::atomic::{AtomicUsize, Ordering};
236
237 let (signal_tx, _signal_rx) = mpsc::channel();
238 let count = Arc::new(AtomicUsize::new(0));
239 let counter = Arc::clone(&count);
240 let worker = Worker::spawn_with_notify(
241 move || FakeSource {
242 polls: 0,
243 signals: signal_tx,
244 },
245 MIN_POLL_INTERVAL,
246 move || {
247 counter.fetch_add(1, Ordering::SeqCst);
248 },
249 );
250 recv(&worker);
251 let deadline = Instant::now() + Duration::from_secs(5);
253 while count.load(Ordering::SeqCst) == 0 {
254 assert!(Instant::now() < deadline, "notify was not called");
255 thread::sleep(Duration::from_millis(5));
256 }
257 }
258
259 #[test]
260 fn undrained_events_stay_bounded() {
261 let (signal_tx, _signal_rx) = mpsc::channel();
262 let worker = Worker::spawn(
263 move || FakeSource {
264 polls: 0,
265 signals: signal_tx,
266 },
267 MIN_POLL_INTERVAL,
268 );
269 thread::sleep(MIN_POLL_INTERVAL * 15);
271 let mut queued = 0;
272 while worker.try_recv().is_some() {
273 queued += 1;
274 }
275 assert!(
276 (1..=EVENT_QUEUE).contains(&queued),
277 "{queued} events queued"
278 );
279 let next = recv(&worker);
281 assert!(matches!(next, Event::Snapshot(s) if s.timestamp_ms > EVENT_QUEUE as u64));
282 }
283
284 #[test]
285 fn delivers_snapshots_and_signal_results() {
286 let (signal_tx, signal_rx) = mpsc::channel();
287 let worker = Worker::spawn(
288 move || FakeSource {
289 polls: 0,
290 signals: signal_tx,
291 },
292 MIN_POLL_INTERVAL,
293 );
294
295 match recv(&worker) {
296 Event::Snapshot(s) => assert_eq!(s.timestamp_ms, 1),
297 other => panic!("unexpected {other:?}"),
298 }
299
300 worker.send(Command::Signal {
301 pid: 1,
302 signal: ProcessSignal::Kill,
303 });
304 let result = loop {
306 if let Event::SignalResult { pid, result, .. } = recv(&worker) {
307 assert_eq!(pid, 1);
308 break result;
309 }
310 };
311 assert!(result.unwrap_err().contains("denied"));
312 assert_eq!(signal_rx.recv().unwrap(), (1, ProcessSignal::Kill));
313 drop(worker);
315 }
316}