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