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