procshot_server/
lib.rs

1//! This crate can be used to continuously scan over `/proc` filesystem and store it in the struct `EncoDecode. This struct is serialized and is written to the `datadir`.
2//! This is a wrapper over the [procfs](https://docs.rs/procfs/0.5.3/procfs/) crate, so the compatibility of this crate depends on the compatibility of the [procfs](https://docs.rs/procfs/0.5.3/procfs/) crate.
3//!
4//! The stored data is of type `EncoDecode` and can be read as:
5//!
6//! # Examples
7//!
8//! ```rust
9//! use std::fs::File;
10//! use std::io::Read;
11//! use procshot_server::EncoDecode;
12//! pub fn read_test_data() {
13//!         let mut file = File::open("./test_data.procshot").unwrap();
14//!         let mut data = Vec::new();
15//!         file.read_to_end(&mut data).unwrap();
16//!         let decoded: EncoDecode = bincode::deserialize(&data[..]).unwrap_or_else(|x| panic!("Error reading saved data. This was either created with an older version of procshot, or the file is corrupt. Error is {}", x));
17//!         println!("Decoded test file data: {:#?}", decoded);
18//! }
19//! ```
20
21extern crate procfs;
22use std::collections::HashMap;
23use std::thread;
24use std::time::Duration;
25#[macro_use]
26extern crate serde_derive;
27extern crate serde;
28use std::fs::File;
29use std::io::Write;
30use std::io::{BufRead, BufReader};
31
32// Tmp imports
33
34extern crate clap;
35extern crate hostname;
36use clap::{App, Arg, SubCommand};
37
38/// PidStatus is the struct that holds the data that we store for each process' status. In this crate, we create a
39/// ` Vec<HashMap<i32, PidStatus>>` which is a mapping of pid to its status.
40#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
41pub struct PidStatus {
42    /// Parent pid
43    pub ppid: i32,
44    /// Effective uid
45    pub euid: i32,
46    /// The complete path to cmd_long if available.
47    pub cmd_long: Vec<String>,
48    /// Command run by this process.
49    pub name: String,
50    /// The filename of the executable, in parentheses.
51    ///
52    /// This is visible whether or not the executable is swapped out.
53    pub cmd_short: String,
54    /// PID of process tracing this process (0 if not being traced).
55    pub tracerpid: i32,
56    /// Number of file descriptor slots currently allocated.
57    pub fdsize: u32,
58    /// Current state of the process.
59    pub state: String,
60    /// Peak virtual memory size by kB.
61    pub vmpeak: Option<u64>,
62    /// Virtual memory size by kB.
63    pub vmsize: Option<u64>,
64    /// Resident Set Size: number of pages the process has in real memory.
65    ///
66    /// This is just the pages which count toward text,  data,  or stack space.
67    /// This does not include pages which have not been demand-loaded in, or which are swapped out.
68    pub rss_pages: i64,
69    /// Gets the Resident Set Size (in bytes)
70    pub rss_bytes: i64,
71    /// Current soft limit in bytes on the rss of the process; see the description of RLIMIT_RSS in
72    /// getrlimit(2).
73    pub rsslim_bytes: u64,
74    /// CPU number last executed on.
75    ///
76    /// (since Linux 2.2.8)
77    pub processor_last_executed: Option<i32>,
78    // Amount of time that this process has been scheduled in user mode, measured in clock ticks
79    /// (divide by [`ticks_per_second()`].
80    ///
81    /// This includes guest time, guest_time (time spent running a virtual CPU, see below), so that
82    /// applications that are not aware of the guest time field  do not lose that time from their
83    /// calculations.
84    pub utime: u64,
85    /// Amount of time that this process has been scheduled in kernel mode, measured in clock ticks
86    /// (divide by [`ticks_per_second()`]).
87    pub stime: u64,
88    /// Holds the user CPU usage by that process.
89    pub user_cpu_usage: f64,
90    /// Holds the sys CPU usage by that process.    
91    pub sys_cpu_usage: f64,
92}
93
94/// EncodDecode is the struct that we use to hold additional metadata and write to disk as
95/// serialized data of the form `let enc encoded: Vec<u8> = bincode::serialize(&encodecode).unwrap();`.
96#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
97pub struct EncoDecode {
98    pub hostname: String,
99    /// Vector of hashmap of pid to the pidstats.
100    pub pid_map_list: HashMap<i32, PidStatus>,
101    /// The epoch time at which the stats were recorded
102    pub time_epoch: u64,
103    /// Can be used for sampling
104    pub delay: u64,
105    /// The cumilative CPU time in jiffies.
106    pub total_cpu_time: u64,
107}
108
109/// scan_proc continuously scans /proc and records all the processes.
110/// scan_proc omits the pids if status.vmpeak == None || prc.stat.rss == 0 || status.pid < 0.
111/// One file is created for each iteration and sleeps for `delay` seconds after each iteration.
112/// The example in the description can be used as a reference to read the stored struct.
113pub fn scan_proc(delay: u64, host: String, datadir: &'static str) {
114    print!("Starting procshot server with delay set as {}", delay);
115
116    let mut previous_stats: Option<HashMap<i32, PidStatus>> = None;
117    let mut previous_cpu_time: u64 = 0;
118    // Starts the continuous iteration over /proc
119    loop {
120        let mut pid_map_hash: HashMap<i32, PidStatus> = HashMap::new(); //Vec::new();
121        let time_epoch = std::time::SystemTime::now()
122            .duration_since(std::time::SystemTime::UNIX_EPOCH)
123            .unwrap()
124            .as_secs();
125        let total_cpu_time = match read_proc_stat() {
126            Ok(t) => t,
127            Err(e) => {
128                eprintln!("Cannot read from /proc/stat, error is:: {:?}", e);
129                continue;
130            }
131        };
132
133        // Iterate over all processess
134        for prc in procfs::all_processes() {
135            let status = prc.status().unwrap_or_else(|_| dummy_pid_status());
136            if status.vmpeak == None || prc.stat.rss == 0 || status.pid < 0 {
137                continue;
138            }
139            let s = PidStatus {
140                ppid: status.ppid,
141                euid: status.euid,
142                cmd_long: prc
143                    .cmdline()
144                    .unwrap_or_else(|_| vec!["No cmd_long found".to_string()]),
145                name: status.name,
146                cmd_short: prc.stat.comm.clone(),
147                tracerpid: status.tracerpid,
148                fdsize: status.fdsize,
149                state: status.state,
150                vmpeak: status.vmpeak,
151                vmsize: status.vmsize,
152                rss_pages: prc.stat.rss,
153                rss_bytes: prc.stat.rss_bytes(),
154                rsslim_bytes: prc.stat.rsslim,
155                processor_last_executed: prc.stat.processor,
156                utime: prc.stat.utime,
157                stime: prc.stat.stime,
158                user_cpu_usage: get_cpu_usage(
159                    "user".to_string(),
160                    status.pid,
161                    &previous_stats,
162                    prc.stat.utime,
163                    total_cpu_time,
164                    previous_cpu_time,
165                ),
166                sys_cpu_usage: get_cpu_usage(
167                    "system".to_string(),
168                    status.pid,
169                    &previous_stats,
170                    prc.stat.stime,
171                    total_cpu_time,
172                    previous_cpu_time,
173                ),
174            };
175
176            // let mut pidmap: HashMap<i32, PidStatus> = HashMap::new();
177            pid_map_hash.insert(status.pid, s);
178        }
179        previous_stats = Some(pid_map_hash.clone());
180        previous_cpu_time = total_cpu_time;
181
182        let encodecode: EncoDecode = EncoDecode {
183            hostname: host.clone(),
184            pid_map_list: pid_map_hash,
185            delay: delay,
186            time_epoch: time_epoch,
187            total_cpu_time: total_cpu_time,
188        };
189        let encoded: Vec<u8> = bincode::serialize(&encodecode).unwrap();
190        // println!("DECODED VALUES:: {:#?}", decoded);
191        //assert_eq!(pids, decoded);
192        let file = File::create(format! {"{}/{}.procshot", datadir, time_epoch});
193        match file {
194            Err(e) => eprintln!("Cannot create file!, err: {}", e),
195            Ok(mut f) => {
196                f.write_all(&encoded).unwrap();
197            }
198        }
199        thread::sleep(Duration::from_secs(delay));
200    }
201}
202
203/// get_cpu_usage calculates cpu usage for user/system.
204/// user_util = 100 * (utime_after - utime_before) / (time_total_after - time_total_before);
205/// sys_util = 100 * (stime_after - stime_before) / (time_total_after - time_total_before);
206fn get_cpu_usage(
207    type_of: String,
208    pid: i32,
209    previous: &Option<HashMap<i32, PidStatus>>,
210    current_type_time: u64,
211    current_cpu_time: u64,
212    previous_cpu_time: u64,
213) -> f64 {
214    match type_of.as_ref() {
215        "user" => match previous {
216            Some(x) => match x.get(&pid) {
217                Some(p) => {
218                    100 as f64 * (current_type_time as f64 - p.utime as f64) / (current_cpu_time as f64 - previous_cpu_time as f64)
219                }
220                None => {
221                    0.0
222                }
223            },
224            None => {
225                0.0
226            }
227        },
228        "system" => match previous {
229            Some(x) => match x.get(&pid) {
230                Some(p) => {
231                    100 as f64 * (current_type_time as f64 - p.stime as f64)
232                        / (current_cpu_time as f64 - previous_cpu_time as f64)
233                }
234                None => 0.0,
235            },
236            None => 0.0,
237        },
238        _ => {
239            println!("Keyword not supported!");
240            0.0
241        }
242    }
243}
244
245/// Reads and parses /proc/stat's first line for calculating cpu percentage
246fn read_proc_stat() -> Result<u64, std::io::Error> {
247    let f = match File::open("/proc/stat") {
248        Ok(somefile) => somefile,
249        Err(e) => return Err(e),
250    };
251
252    let mut reader_itr = BufReader::new(f).lines();
253    let first_line = match reader_itr.next() {
254        // next returns an Option<Result<>> type, and hence the nested some(ok())
255        Some(total_string) => match total_string {
256            Ok(s) => s,
257            Err(e) => return Err(e),
258        },
259        None => {
260            return Err(std::io::Error::new(
261                std::io::ErrorKind::Other,
262                "Cannot read the first line from /proc/stat.",
263            ))
264        }
265    };
266    let total_vector = first_line
267        .split("cpu") // Split at "cpu"
268        .collect::<Vec<&str>>()[1] // Skip 0th element
269        .split(" ") // Split at " "
270        .filter(|&x| x != "") // filter empty lines
271        .collect::<Vec<&str>>(); // collect
272    let mut total: u64 = 0;
273    for i in total_vector {
274        total += i.parse::<u64>().unwrap();
275    }
276    Ok(total)
277}
278
279///dummy_status is used to return a dummy procfs::Status struct
280fn dummy_pid_status() -> procfs::Status {
281    let ds = "Dummy because unwrap failed".to_string();
282    procfs::Status {
283        name: ds.clone(),
284        umask: Some(std::u32::MAX),
285        state: ds.clone(),
286        tgid: -1,
287        ngid: Some(-1),
288        pid: -1,
289        ppid: -1,
290        tracerpid: -1,
291        ruid: -1,
292        euid: -1,
293        suid: -1,
294        fuid: -1,
295        rgid: -1,
296        egid: -1,
297        sgid: -1,
298        fgid: -1,
299        fdsize: std::u32::MAX,
300        groups: vec![-1],
301        nstgid: Some(vec![-1]),
302        nspid: Some(vec![-1]),
303        nspgid: Some(vec![-1]),
304        nssid: Some(vec![-1]),
305        vmpeak: Some(std::u64::MAX),
306        vmsize: Some(std::u64::MAX),
307        vmlck: Some(std::u64::MAX),
308        vmpin: Some(std::u64::MAX),
309        vmhwm: Some(std::u64::MAX),
310        vmrss: Some(std::u64::MAX),
311        rssanon: Some(std::u64::MAX),
312        rssfile: Some(std::u64::MAX),
313        rssshmem: Some(std::u64::MAX),
314        vmdata: Some(std::u64::MAX),
315        vmstk: Some(std::u64::MAX),
316        vmexe: Some(std::u64::MAX),
317        vmlib: Some(std::u64::MAX),
318        vmpte: Some(std::u64::MAX),
319        vmswap: Some(std::u64::MAX),
320        hugetblpages: Some(std::u64::MAX),
321        threads: std::u64::MAX,
322        sigq: (std::u64::MAX, std::u64::MAX),
323        sigpnd: std::u64::MAX,
324        shdpnd: std::u64::MAX,
325        sigblk: std::u64::MAX,
326        sigign: std::u64::MAX,
327        sigcgt: std::u64::MAX,
328        capinh: std::u64::MAX,
329        capprm: std::u64::MAX,
330        capeff: std::u64::MAX,
331        capbnd: Some(std::u64::MAX),
332        capamb: Some(std::u64::MAX),
333        nonewprivs: Some(std::u64::MAX),
334        seccomp: Some(std::u32::MAX),
335        speculation_store_bypass: Some(ds.clone()),
336        cpus_allowed: Some(vec![std::u32::MAX]),
337        cpus_allowed_list: Some(vec![(std::u32::MAX, std::u32::MAX)]),
338        mems_allowed: Some(vec![std::u32::MAX]),
339        mems_allowed_list: Some(vec![(std::u32::MAX, std::u32::MAX)]),
340        voluntary_ctxt_switches: Some(std::u64::MAX),
341        nonvoluntary_ctxt_switches: Some(std::u64::MAX),
342    }
343}
344
345/// Config struct holds the user input when running the server. It is a bad design to hold the client's option as well in the same struct, but
346/// as of now, it is here.
347#[derive(Debug)]
348pub struct Config {
349    /// hostname of the server. This is derived by this crate from the [hostname](https://docs.rs/hostname/0.1.5/hostname/) crate.
350    pub hostname: String,
351    /// Delay decides how many seconds to sleep after each iteration of scanning /proc
352    pub delay: u64,
353    /// If true, runs as server. Defaults to false. Pass the subcommand `server` to set it to true.
354    pub server: bool,
355    /// The time from which the client can fetch data to process.
356    pub client_time_from: String,
357    /// Sort the processed data by whatever the user wants.
358    pub client_sort_by: String,
359}
360
361/// Returns a new config object. This also gives the following command line argument options.
362/// # Examples
363/// Here are the cli options used to populate the struct.
364/// > sudo target/debug/procshot --help
365///
366/// ```bash
367/// procshot 1.0
368/// nohupped@gmail.com
369/// Snapshots proc periodically. All the options except delay works when 'server' option is not used.
370///
371/// USAGE:
372///     procshot [FLAGS] [OPTIONS] [SUBCOMMAND]
373
374/// FLAGS:
375///     -h, --help       Prints help information
376///     -o               Sort result by Memory or CPU. Accepted values are...
377///     -t               Read stats from a specific time. Accepted format: 2015-09-05 23:56:04
378///     -V, --version    Prints version information
379///
380/// OPTIONS:
381///     -d, --delay <delay>      Sets delay in seconds before it scans /proc every time. [default: 60]
382///
383/// SUBCOMMANDS:
384///     help      Prints this message or the help of the given subcommand(s)
385///     server    Decides whether to run as server or client
386impl Config {
387    pub fn new() -> Self {
388        let matches = App::new("procshot")
389                        .version("1.0")
390                        .author("nohupped@gmail.com")
391                        .about("Snapshots proc periodically. All the options except delay works when 'server' option is not used.")
392                        .arg(Arg::with_name("delay")
393                            .short("d")
394                            .long("delay")
395                            .default_value("60")
396                            .help("Sets delay in seconds before it scans /proc every time."))
397                        .subcommand(SubCommand::with_name("server")
398                            .about("Runs as server and records stats."))
399                        .arg(Arg::with_name("time_from")
400                            .short("t")
401                            .help("Read stats from a specific time. Accepted format: 2015-09-05 23:56:04")
402                            )
403                        .arg(Arg::with_name("order_by")
404                            .short("o")
405                            .help("Sort result by Memory or CPU. Accepted values are...") // Todo here
406                            )
407                        .get_matches();
408
409        Config {
410            hostname: hostname::get_hostname().unwrap().to_string(),
411            delay: matches
412                .value_of("delay")
413                .unwrap_or("60")
414                .parse()
415                .unwrap_or(60),
416            server: match matches.subcommand_matches("server") {
417                Some(_) => true,
418                None => false,
419            },
420            client_time_from: matches.value_of("time_from").unwrap_or("").to_string(),
421            client_sort_by: matches.value_of("order_by").unwrap_or("m").to_string(),
422        }
423    }
424}
425/// Checks if the program is run as sudo (root) user. This doesn't check if the user has the privilege to read over all of /proc or write to the datadir
426/// but just checks if the uid passed to this is 0, and returns a `Result`
427///
428/// # Examples
429///
430///```rust
431///
432/// use procshot_server::check_sudo;
433/// use std::process;
434///
435/// fn main() {
436///     match check_sudo(0) { // Can also use get_current_uid() from the `users` crate
437///         Err(e) => {
438///             eprintln!("Error encountered checking privileges, {:?}", e);
439///             process::exit(1);
440///         },
441///         _ => (),
442///     }
443/// }
444///```
445pub fn check_sudo(uid: u32) -> Result<(), &'static str> {
446    match uid == 0 {
447        true => Ok(()),
448        false => Err("Error: Run as root."),
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn test_check_sudo_privileged() {
458        match check_sudo(0) {
459            Ok(()) => (),
460            Err(e) => panic!("Test failed, {:?}", e),
461        }
462    }
463
464    #[test]
465    #[should_panic]
466    fn test_check_sudo_non_privileged() {
467        match check_sudo(10) {
468            Ok(()) => (),
469            Err(e) => panic!("Test failed, {:?}", e),
470        }
471    }
472}