extern crate procfs;
use std::collections::HashMap;
use std::thread;
use std::time::Duration;
#[macro_use]
extern crate serde_derive;
extern crate serde;
use std::fs::File;
use std::io::Write;
use std::io::{BufRead, BufReader};
extern crate clap;
extern crate hostname;
use clap::{App, Arg, SubCommand};
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct PidStatus {
pub ppid: i32,
pub euid: i32,
pub cmd_long: Vec<String>,
pub name: String,
pub cmd_short: String,
pub tracerpid: i32,
pub fdsize: u32,
pub state: String,
pub vmpeak: Option<u64>,
pub vmsize: Option<u64>,
pub rss_pages: i64,
pub rss_bytes: i64,
pub rsslim_bytes: u64,
pub processor_last_executed: Option<i32>,
pub utime: u64,
pub stime: u64,
pub user_cpu_usage: f64,
pub sys_cpu_usage: f64,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct EncoDecode {
pub hostname: String,
pub pid_map_list: HashMap<i32, PidStatus>,
pub time_epoch: u64,
pub delay: u64,
pub total_cpu_time: u64,
}
pub fn scan_proc(delay: u64, host: String, datadir: &'static str) {
print!("Starting procshot server with delay set as {}", delay);
let mut previous_stats: Option<HashMap<i32, PidStatus>> = None;
let mut previous_cpu_time: u64 = 0;
loop {
let mut pid_map_hash: HashMap<i32, PidStatus> = HashMap::new(); let time_epoch = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let total_cpu_time = match read_proc_stat() {
Ok(t) => t,
Err(e) => {
eprintln!("Cannot read from /proc/stat, error is:: {:?}", e);
continue;
}
};
for prc in procfs::all_processes() {
let status = prc.status().unwrap_or_else(|_| dummy_pid_status());
if status.vmpeak == None || prc.stat.rss == 0 || status.pid < 0 {
continue;
}
let s = PidStatus {
ppid: status.ppid,
euid: status.euid,
cmd_long: prc
.cmdline()
.unwrap_or_else(|_| vec!["No cmd_long found".to_string()]),
name: status.name,
cmd_short: prc.stat.comm.clone(),
tracerpid: status.tracerpid,
fdsize: status.fdsize,
state: status.state,
vmpeak: status.vmpeak,
vmsize: status.vmsize,
rss_pages: prc.stat.rss,
rss_bytes: prc.stat.rss_bytes(),
rsslim_bytes: prc.stat.rsslim,
processor_last_executed: prc.stat.processor,
utime: prc.stat.utime,
stime: prc.stat.stime,
user_cpu_usage: get_cpu_usage(
"user".to_string(),
status.pid,
&previous_stats,
prc.stat.utime,
total_cpu_time,
previous_cpu_time,
),
sys_cpu_usage: get_cpu_usage(
"system".to_string(),
status.pid,
&previous_stats,
prc.stat.stime,
total_cpu_time,
previous_cpu_time,
),
};
pid_map_hash.insert(status.pid, s);
}
previous_stats = Some(pid_map_hash.clone());
previous_cpu_time = total_cpu_time;
let encodecode: EncoDecode = EncoDecode {
hostname: host.clone(),
pid_map_list: pid_map_hash,
delay: delay,
time_epoch: time_epoch,
total_cpu_time: total_cpu_time,
};
let encoded: Vec<u8> = bincode::serialize(&encodecode).unwrap();
let file = File::create(format! {"{}/{}.procshot", datadir, time_epoch});
match file {
Err(e) => eprintln!("Cannot create file!, err: {}", e),
Ok(mut f) => {
f.write_all(&encoded).unwrap();
}
}
thread::sleep(Duration::from_secs(delay));
}
}
fn get_cpu_usage(
type_of: String,
pid: i32,
previous: &Option<HashMap<i32, PidStatus>>,
current_type_time: u64,
current_cpu_time: u64,
previous_cpu_time: u64,
) -> f64 {
match type_of.as_ref() {
"user" => match previous {
Some(x) => match x.get(&pid) {
Some(p) => {
100 as f64 * (current_type_time as f64 - p.utime as f64) / (current_cpu_time as f64 - previous_cpu_time as f64)
}
None => {
0.0
}
},
None => {
0.0
}
},
"system" => match previous {
Some(x) => match x.get(&pid) {
Some(p) => {
100 as f64 * (current_type_time as f64 - p.stime as f64)
/ (current_cpu_time as f64 - previous_cpu_time as f64)
}
None => 0.0,
},
None => 0.0,
},
_ => {
println!("Keyword not supported!");
0.0
}
}
}
fn read_proc_stat() -> Result<u64, std::io::Error> {
let f = match File::open("/proc/stat") {
Ok(somefile) => somefile,
Err(e) => return Err(e),
};
let mut reader_itr = BufReader::new(f).lines();
let first_line = match reader_itr.next() {
Some(total_string) => match total_string {
Ok(s) => s,
Err(e) => return Err(e),
},
None => {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Cannot read the first line from /proc/stat.",
))
}
};
let total_vector = first_line
.split("cpu") .collect::<Vec<&str>>()[1] .split(" ") .filter(|&x| x != "") .collect::<Vec<&str>>(); let mut total: u64 = 0;
for i in total_vector {
total += i.parse::<u64>().unwrap();
}
Ok(total)
}
fn dummy_pid_status() -> procfs::Status {
let ds = "Dummy because unwrap failed".to_string();
procfs::Status {
name: ds.clone(),
umask: Some(std::u32::MAX),
state: ds.clone(),
tgid: -1,
ngid: Some(-1),
pid: -1,
ppid: -1,
tracerpid: -1,
ruid: -1,
euid: -1,
suid: -1,
fuid: -1,
rgid: -1,
egid: -1,
sgid: -1,
fgid: -1,
fdsize: std::u32::MAX,
groups: vec![-1],
nstgid: Some(vec![-1]),
nspid: Some(vec![-1]),
nspgid: Some(vec![-1]),
nssid: Some(vec![-1]),
vmpeak: Some(std::u64::MAX),
vmsize: Some(std::u64::MAX),
vmlck: Some(std::u64::MAX),
vmpin: Some(std::u64::MAX),
vmhwm: Some(std::u64::MAX),
vmrss: Some(std::u64::MAX),
rssanon: Some(std::u64::MAX),
rssfile: Some(std::u64::MAX),
rssshmem: Some(std::u64::MAX),
vmdata: Some(std::u64::MAX),
vmstk: Some(std::u64::MAX),
vmexe: Some(std::u64::MAX),
vmlib: Some(std::u64::MAX),
vmpte: Some(std::u64::MAX),
vmswap: Some(std::u64::MAX),
hugetblpages: Some(std::u64::MAX),
threads: std::u64::MAX,
sigq: (std::u64::MAX, std::u64::MAX),
sigpnd: std::u64::MAX,
shdpnd: std::u64::MAX,
sigblk: std::u64::MAX,
sigign: std::u64::MAX,
sigcgt: std::u64::MAX,
capinh: std::u64::MAX,
capprm: std::u64::MAX,
capeff: std::u64::MAX,
capbnd: Some(std::u64::MAX),
capamb: Some(std::u64::MAX),
nonewprivs: Some(std::u64::MAX),
seccomp: Some(std::u32::MAX),
speculation_store_bypass: Some(ds.clone()),
cpus_allowed: Some(vec![std::u32::MAX]),
cpus_allowed_list: Some(vec![(std::u32::MAX, std::u32::MAX)]),
mems_allowed: Some(vec![std::u32::MAX]),
mems_allowed_list: Some(vec![(std::u32::MAX, std::u32::MAX)]),
voluntary_ctxt_switches: Some(std::u64::MAX),
nonvoluntary_ctxt_switches: Some(std::u64::MAX),
}
}
#[derive(Debug)]
pub struct Config {
pub hostname: String,
pub delay: u64,
pub server: bool,
pub client_time_from: String,
pub client_sort_by: String,
}
impl Config {
pub fn new() -> Self {
let matches = App::new("procshot")
.version("1.0")
.author("nohupped@gmail.com")
.about("Snapshots proc periodically. All the options except delay works when 'server' option is not used.")
.arg(Arg::with_name("delay")
.short("d")
.long("delay")
.default_value("60")
.help("Sets delay in seconds before it scans /proc every time."))
.subcommand(SubCommand::with_name("server")
.about("Runs as server and records stats."))
.arg(Arg::with_name("time_from")
.short("t")
.help("Read stats from a specific time. Accepted format: 2015-09-05 23:56:04")
)
.arg(Arg::with_name("order_by")
.short("o")
.help("Sort result by Memory or CPU. Accepted values are...") )
.get_matches();
Config {
hostname: hostname::get_hostname().unwrap().to_string(),
delay: matches
.value_of("delay")
.unwrap_or("60")
.parse()
.unwrap_or(60),
server: match matches.subcommand_matches("server") {
Some(_) => true,
None => false,
},
client_time_from: matches.value_of("time_from").unwrap_or("").to_string(),
client_sort_by: matches.value_of("order_by").unwrap_or("m").to_string(),
}
}
}
pub fn check_sudo(uid: u32) -> Result<(), &'static str> {
match uid == 0 {
true => Ok(()),
false => Err("Error: Run as root."),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_sudo_privileged() {
match check_sudo(0) {
Ok(()) => (),
Err(e) => panic!("Test failed, {:?}", e),
}
}
#[test]
#[should_panic]
fn test_check_sudo_non_privileged() {
match check_sudo(10) {
Ok(()) => (),
Err(e) => panic!("Test failed, {:?}", e),
}
}
}