bubbles 4.2.0

Bubble integration server for powder diffraction
use crate::logger::Logger;
use crate::utils::py_stamp;
#[cfg(not(target_os = "windows"))]
use daemonize::Daemonize;
#[cfg(not(target_os = "windows"))]
use std::fs;
#[cfg(not(target_os = "windows"))]
use std::fs::File;
#[cfg(not(target_os = "windows"))]
use std::path::Path;

const PORT: &'static str = "8587";
#[cfg(not(target_os = "linux"))]
const RESIZE: &'static str = "4";
#[cfg(target_os = "linux")]
const RESIZE: &'static str = "1";
const VERBOSE_ALL: &'static str = "3";
const VERBOSE_INFO: &'static str = "2";
const VERBOSE_ERRORS: &'static str = "1";
const VERBOSE_SILENT: &'static str = "0";

pub struct Params {
    pub started: f64,
    pub port: u16,
    pub resize: usize,
    pub(crate) threads: usize,
    #[cfg(not(target_os = "windows"))]
    stdout: String,
    #[cfg(not(target_os = "windows"))]
    stderr: String,
    #[cfg(not(target_os = "windows"))]
    pid: String,
    #[cfg(not(target_os = "windows"))]
    user: String,
    #[cfg(not(target_os = "windows"))]
    group: String,
    #[cfg(not(target_os = "windows"))]
    workdir: String,
    pub(crate) log_level: log::LevelFilter,
    pub(crate) daemon: bool,
}

impl Params {
    fn remove_pid_file(&self) {
        #[cfg(not(target_os = "windows"))]
        let _ = fs::remove_file(&self.pid);
    }

    pub(crate) fn from_command_line() -> Params {
        #[cfg(not(target_os = "windows"))]
        let user = whoami::username();
        let about = about();
        let threads = num_cpus::get().to_string();
        let matches = clap::App::new(env!("CARGO_PKG_NAME"))
            .about(<std::string::String as AsRef<str>>::as_ref(&about))
            .arg(
                clap::Arg::with_name("port")
                    .value_name("PORT")
                    .help("TCP/IP port to listen for client connections")
                    .default_value(PORT)
                    .takes_value(true),
            )
            .arg(
                clap::Arg::with_name("resize")
                    .short("r")
                    .long("resize")
                    .value_name("PIXELS")
                    .help("Resize and interpolate image by PIXELS before sending to client")
                    .default_value(RESIZE)
                    .takes_value(true),
            )
            .arg(
                clap::Arg::with_name("verbose")
                    .short("v")
                    .long("verbose")
                    .value_name("VERBOSITY LEVEL")
                    .help("Verbosity level for messages: 0 - off; 1 - errors; 2 - info; 3 - all")
                    .default_value(VERBOSE_INFO)
                    .takes_value(true),
            )
            .arg(
                clap::Arg::with_name("threads")
                    .short("t")
                    .long("threads")
                    .value_name("THREAD NUMBER")
                    .help("Number of threads which are used for integration")
                    .default_value(&threads)
                    .takes_value(true),
            );
        #[cfg(not(target_os = "windows"))]
        let matches = matches
            .arg(
                clap::Arg::with_name("stdout")
                    .short("o")
                    .long("out")
                    .value_name("LOGFILE")
                    .help("Log file")
                    .takes_value(true),
            )
            .arg(
                clap::Arg::with_name("stderr")
                    .short("e")
                    .long("err")
                    .value_name("LOGFILE")
                    .help("Log file for errors")
                    .takes_value(true),
            )
            .arg(
                clap::Arg::with_name("pid")
                    .short("i")
                    .long("pid")
                    .value_name("PIDFILE")
                    .help("pidfile path")
                    .takes_value(true),
            )
            .arg(
                clap::Arg::with_name("user")
                    .short("u")
                    .long("user")
                    .value_name("USER")
                    .help("A username to run daemon")
                    .default_value(&user)
                    .takes_value(true),
            )
            .arg(
                clap::Arg::with_name("group")
                    .short("g")
                    .long("group")
                    .value_name("GROUP")
                    .help("A group to run daemon")
                    .default_value(&user)
                    .takes_value(true),
            )
            .arg(
                clap::Arg::with_name("workdir")
                    .short("w")
                    .long("workdir")
                    .value_name("WORKING DIRECTORY")
                    .help("Working directory for the daemon")
                    .takes_value(true),
            )
            .arg(
                clap::Arg::with_name("daemon")
                    .short("d")
                    .long("daemon")
                    .help("Run process in the background"),
            );
        let matches = matches.get_matches();
        let p = Params {
            port: value_t!(matches.value_of("port"), u16).unwrap_or_else(|e| e.exit()),
            resize: value_t!(matches.value_of("resize"), usize).unwrap_or_else(|e| e.exit()),
            threads: value_t!(matches.value_of("threads"), usize).unwrap_or_else(|e| e.exit()),
            #[cfg(not(target_os = "windows"))]
            stdout: matches.value_of("stdout").or(Some("")).unwrap().to_string(),
            #[cfg(not(target_os = "windows"))]
            stderr: matches.value_of("stderr").or(Some("")).unwrap().to_string(),
            #[cfg(not(target_os = "windows"))]
            pid: matches.value_of("pid").or(Some("")).unwrap().to_string(),
            #[cfg(not(target_os = "windows"))]
            user: matches
                .value_of("user")
                .or(Some(&user))
                .unwrap()
                .to_string(),
            #[cfg(not(target_os = "windows"))]
            group: matches.value_of("group").or(Some("")).unwrap().to_string(),
            #[cfg(not(target_os = "windows"))]
            workdir: matches
                .value_of("workdir")
                .or(Some(""))
                .unwrap()
                .to_string(),
            started: py_stamp(),
            log_level: match matches
                .value_of("verbose")
                .unwrap_or_else(|| std::process::exit(1))
            {
                VERBOSE_ERRORS => log::LevelFilter::Error,
                VERBOSE_ALL => log::LevelFilter::Debug,
                VERBOSE_SILENT => log::LevelFilter::Off,
                VERBOSE_INFO => log::LevelFilter::Info,
                _ => log::LevelFilter::Debug,
            },
            #[cfg(not(target_os = "windows"))]
            daemon: matches.is_present("daemon"),
            #[cfg(target_os = "windows")]
            daemon: false,
        };
        if let Err(e) = Logger::init(&p) {
            die!("Could not set logger: {}", e);
        }
        p.daemonize();
        println!("{} {}", env!("CARGO_PKG_NAME"), about);
        p
    }

    #[cfg(target_os = "windows")]
    fn daemonize(&self) {}

    #[cfg(not(target_os = "windows"))]
    fn daemonize(&self) {
        if !self.daemon {
            return;
        }
        let mut daemon = Daemonize::new();
        if let Some(out) = self.stdout.create() {
            daemon = daemon.stdout(out);
        }
        if self.stderr.is_empty() {
            if !self.stdout.is_empty() {
                if let Some(out) = self.stdout.create() {
                    daemon = daemon.stderr(out);
                }
            }
        } else {
            if let Some(err) = self.stderr.create() {
                daemon = daemon.stderr(err);
            }
        }
        if !self.pid.is_empty() {
            daemon = daemon.pid_file(&self.pid);
        }
        if !self.workdir.is_empty() {
            daemon = daemon.working_directory(&self.workdir);
        }
        if !self.user.is_empty() {
            daemon = daemon.user(self.user.as_ref());
        }
        if !self.group.is_empty() {
            daemon = daemon.group(self.group.as_ref());
        }
        match daemon.start() {
            Ok(_) => info!("The main process has forked successfully"),
            Err(e) => die!("Could not daemonize: {}", e),
        }
    }

    pub(crate) fn address(&self) -> String {
        format!("0.0.0.0:{}", self.port)
    }
}

#[cfg(not(target_os = "windows"))]
trait FileCreator: AsRef<Path> {
    fn create(&self) -> Option<fs::File>;
}

#[cfg(not(target_os = "windows"))]
impl FileCreator for str {
    fn create(&self) -> Option<File> {
        if !self.is_empty() {
            match fs::OpenOptions::new().append(true).create(true).open(self) {
                Ok(out) => Some(out),
                Err(e) => {
                    error!("Failed to open file {}: {}", self, e);
                    None
                }
            }
        } else {
            None
        }
    }
}

impl Drop for Params {
    fn drop(&mut self) {
        self.remove_pid_file();
    }
}

fn about() -> String {
    format!(
        "is {}, version {} built on {} {}\n\
         (c) {} 2014-{}, SNBL@ESRF, inspired by Giuseppe Portale\n\
         If this program proves to be useful, please cite this paper:\n\
         http://dx.doi.org/10.1107/S1600577516002411\n\
         Official web page: {}\n\
         Git repository: {}\n\
         Git hash: {}\n\
         Git date: {} {}",
        env!("CARGO_PKG_DESCRIPTION"),
        env!("CARGO_PKG_VERSION"),
        env!("VERGEN_BUILD_DATE"),
        env!("VERGEN_BUILD_TIME"),
        env!("CARGO_PKG_AUTHORS"),
        &env!("VERGEN_BUILD_DATE")[..4],
        env!("CARGO_PKG_HOMEPAGE"),
        env!("CARGO_PKG_REPOSITORY"),
        env!("VERGEN_GIT_SHA"),
        env!("VERGEN_GIT_COMMIT_DATE"),
        env!("VERGEN_GIT_COMMIT_TIME"),
    )
}