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