cognitive_qualia/
env.rs

1// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of
2// the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/
3
4//! This module contains logic of setting up and tearing down application environment.
5
6// -------------------------------------------------------------------------------------------------
7
8use std;
9use std::fs;
10use std::time::{Duration, SystemTime};
11use std::ops::BitAnd;
12use std::error::Error;
13use std::path::{Path, PathBuf};
14
15use libc;
16use time;
17use nix::sys::signal;
18
19use timber;
20
21use settings::Directories;
22use errors::Illusion;
23use log;
24
25// -------------------------------------------------------------------------------------------------
26
27const RUNTIME_DIR_VAR: &'static str = "XDG_RUNTIME_DIR";
28const DATA_DIR_VAR: &'static str = "XDG_DATA_HOME";
29const CACHE_DIR_VAR: &'static str = "XDG_CACHE_HOME";
30const CONFIG_DIR_VAR: &'static str = "XDG_CONFIG_HOME";
31
32const DEFAULT_RUNTIME_DIR: &'static str = "/tmp";
33const DEFAULT_SYSTEM_CONFIG_DIR_BASE: &'static str = "/etc/";
34
35const DEFAULT_DATA_DIR_FRAGMENT: &'static str = ".local/share";
36const DEFAULT_CACHE_DIR_FRAGMENT: &'static str = ".cache";
37const DEFAULT_CONFIG_DIR_FRAGMENT: &'static str = ".config";
38
39// -------------------------------------------------------------------------------------------------
40
41pub enum LogDestination {
42    StdOut,
43    LogFile,
44    Disabled,
45}
46
47// -------------------------------------------------------------------------------------------------
48
49pub enum Directory {
50    Data,
51    Cache,
52    Runtime,
53}
54
55// -------------------------------------------------------------------------------------------------
56
57pub struct Env {
58    dirs: Directories,
59    progname: String,
60}
61
62// -------------------------------------------------------------------------------------------------
63
64/// This class represents runtime environment. It cares for creating directories or initializing
65/// logger.
66impl Env {
67    /// Prepares environment:
68    ///  - register signal handler
69    ///  - create needed directories
70    ///  - initialize logger
71    ///  - clean old files
72    pub fn create(log_destination: LogDestination, progname: &str) -> Self {
73        // Register signals
74        Self::register_signal_handler();
75
76        // Create cache directory and initialize logger
77        let cache_dir = Self::create_cache_dir(progname).unwrap();
78        if let Err(err) = Self::initialize_logger(log_destination, &cache_dir, progname) {
79            log_warn1!("{}", err);
80        }
81
82        // Create runtime directory
83        let data_dir = Self::create_data_dir(progname).unwrap();
84
85        // Create runtime directory
86        let runtime_dir = Self::create_runtime_dir(progname).unwrap();
87
88        // Check if configuration directories exist and remember them if so.
89        let (system_config_dir, user_config_dir) = Self::check_config_dirs(progname);
90
91        // Construct `Env`
92        let mine = Env {
93            dirs: Directories {
94                runtime: runtime_dir,
95                data: data_dir,
96                cache: cache_dir,
97                user_config: user_config_dir,
98                system_config: system_config_dir,
99            },
100            progname: progname.to_string(),
101        };
102
103        // Remove unneeded files
104        mine.remove_old_logs();
105        mine
106    }
107
108    /// Opens file in predefined directory.
109    pub fn open_file(&self, name: String, dir: Directory) -> Result<fs::File, Illusion> {
110        let mut dir = {
111            match dir {
112                Directory::Data => self.dirs.data.clone(),
113                Directory::Cache => self.dirs.cache.clone(),
114                Directory::Runtime => self.dirs.runtime.clone(),
115            }
116        };
117
118        dir.set_file_name(name);
119        match fs::OpenOptions::new().read(true).write(true).create(true).open(dir.as_path()) {
120            Ok(file) => Ok(file),
121            Err(err) => Err(Illusion::IO(err.description().to_string())),
122        }
123    }
124
125    /// Returns directory paths.
126    pub fn get_directories(&self) -> &Directories {
127        &self.dirs
128    }
129}
130
131// -------------------------------------------------------------------------------------------------
132
133// Initializing logger
134impl Env {
135    /// Initializes logger to write log to given destination.
136    fn initialize_logger(destination: LogDestination,
137                         dir: &Path,
138                         progname: &str)
139                         -> Result<(), Illusion> {
140        match destination {
141            LogDestination::LogFile => Self::initialize_logger_for_log_file(dir, progname),
142            LogDestination::StdOut => Self::initialize_logger_for_stdout(),
143            LogDestination::Disabled => Self::disable_logger(),
144        }
145    }
146
147    /// Chose log file path and sets logger up to use it.
148    fn initialize_logger_for_log_file(dir: &Path, progname: &str) -> Result<(), Illusion> {
149        let path = dir.join(format!("log-{}.log", Self::get_time_representation()));
150        match timber::init(&path) {
151            Ok(ok) => {
152                println!("Welcome to {}", progname);
153                println!("Log file in {:?}", path);
154                Ok(ok)
155            }
156            Err(err) => Err(Illusion::General(err.description().to_owned())),
157        }
158    }
159
160    /// Sets logger to write logs to `/dev/null`.
161    fn disable_logger() -> Result<(), Illusion> {
162        if let Err(err) = timber::init(Path::new("/dev/null")) {
163            Err(Illusion::General(format!("Failed to disable logger: {}", err)))
164        } else {
165            Ok(())
166        }
167    }
168
169    /// Sets logger to write logs to `stdout`.
170    fn initialize_logger_for_stdout() -> Result<(), Illusion> {
171        // Nothing to do. `timber` prints to `stdout` by default.
172        Ok(())
173    }
174}
175
176// -------------------------------------------------------------------------------------------------
177
178// Cleaning up
179impl Env {
180    /// Cleans up environment: remove runtime directory.
181    fn cleanup(&mut self) {
182        if let Err(err) = fs::remove_dir_all(&self.dirs.runtime) {
183            log_warn1!("Failed to remove runtime directory: {:?}", err);
184        }
185    }
186
187    /// Removes logs older than two days.
188    fn remove_old_logs(&self) {
189        let transition_time = SystemTime::now() - Duration::new(2 * 24 * 60 * 60, 0);
190        if let Ok(entries) = fs::read_dir(&self.dirs.cache) {
191            for entry in entries {
192                if let Ok(entry) = entry {
193                    let path = entry.path();
194                    if let Some(extension) = path.extension() {
195                        if extension == "log" {
196                            // Check if file is old enough to be removed. In case of any error
197                            // remove the file.
198                            let meta = path.metadata();
199                            let good_to_remove = {
200                                if let Ok(meta) = meta {
201                                    if let Ok(access_time) = meta.accessed() {
202                                        access_time < transition_time
203                                    } else {
204                                        true
205                                    }
206                                } else {
207                                    true
208                                }
209                            };
210
211                            if good_to_remove {
212                                if let Err(err) = fs::remove_file(&path) {
213                                    log_warn1!("Failed to remove old log file {:?}: {}", path, err);
214                                }
215                            }
216                        }
217                    }
218                }
219            }
220        }
221    }
222}
223
224// -------------------------------------------------------------------------------------------------
225
226// Creating directories
227impl Env {
228    /// Create runtime directory.
229    fn create_runtime_dir(progname: &str) -> Result<PathBuf, Illusion> {
230        let mut path = Self::read_path(RUNTIME_DIR_VAR, DEFAULT_RUNTIME_DIR);
231        path.push(format!("{}-{}", progname, Self::get_time_representation()));
232        Self::mkdir(&path).and(Ok(path))
233    }
234
235    /// Create data directory.
236    fn create_data_dir(progname: &str) -> Result<PathBuf, Illusion> {
237        let mut default_path = std::env::home_dir().unwrap();
238        default_path.push(DEFAULT_DATA_DIR_FRAGMENT);
239        let mut path = Self::read_path(DATA_DIR_VAR, default_path.to_str().unwrap());
240        path.push(progname);
241        Self::mkdir(&path).and(Ok(path))
242    }
243
244    /// Create cache directory.
245    fn create_cache_dir(progname: &str) -> Result<PathBuf, Illusion> {
246        let mut default_path = std::env::home_dir().unwrap();
247        default_path.push(DEFAULT_CACHE_DIR_FRAGMENT);
248        let mut path = Self::read_path(CACHE_DIR_VAR, default_path.to_str().unwrap());
249        path.push(progname);
250        Self::mkdir(&path).and(Ok(path))
251    }
252
253    /// Checks if config directories exist.
254    ///
255    /// Global config directory is `/etc/<program name>/`.
256    ///
257    /// Local config directory is `$XDG_CONFIG_HOME/<program name>` if the variable exists, else
258    /// `~/.config/<program name>`.
259    fn check_config_dirs(progname: &str) -> (Option<PathBuf>, Option<PathBuf>) {
260        // Check if local config directory exists
261        let user_config_dir = {
262            let mut default_path = std::env::home_dir().unwrap();
263            default_path.push(DEFAULT_CONFIG_DIR_FRAGMENT);
264            let mut user = Self::read_path(CONFIG_DIR_VAR, default_path.to_str().unwrap());
265            user.push(progname);
266            if user.exists() && user.is_dir() {
267                Some(user)
268            } else {
269                None
270            }
271        };
272
273        // Check if global config directory exists
274        let system_config_dir = {
275            let system = PathBuf::from(format!("{}{}", DEFAULT_SYSTEM_CONFIG_DIR_BASE, progname));
276            if system.exists() && system.is_dir() {
277                Some(system)
278            } else {
279                None
280            }
281        };
282
283        // Return results
284        (system_config_dir, user_config_dir)
285    }
286
287    /// Reads given environment variable and if exists returns its value or default value otherwise.
288    fn read_path(var: &str, default_path: &str) -> PathBuf {
289        let mut path = PathBuf::new();
290        path.push(std::env::var(var).unwrap_or(default_path.to_string()));
291        path
292    }
293
294    /// Helper function for creating directory.
295    fn mkdir(path: &PathBuf) -> Result<(), Illusion> {
296        if path.exists() {
297            if path.is_dir() {
298                Ok(())
299            } else {
300                Err(Illusion::InvalidArgument(format!("Path '{:?}' is not directory!", path)))
301            }
302        } else if let Err(err) = fs::create_dir(path) {
303            Err(Illusion::General(format!("Could not create directory '{:?}': {}", path, err)))
304        } else {
305            Ok(())
306        }
307    }
308
309    /// Helper function for generating temporary director and file names. Returns string in format
310    /// `ddd-hh-mm-ss`, where
311    ///
312    /// - `ddd` is zero padded number of current day in year
313    /// - `hh` is zero padded hour
314    /// - `mm` is zero padded minute
315    /// - `ss` is zero padded second
316    fn get_time_representation() -> String {
317        let tm = time::now().to_local();
318        format!("{:03}-{:02}-{:02}-{:02}", tm.tm_yday, tm.tm_hour, tm.tm_min, tm.tm_sec)
319    }
320}
321
322// -------------------------------------------------------------------------------------------------
323
324// Handling signals.
325impl Env {
326    /// Registers handler for signals `SIGINT`, `SIGTERM`, `SIGSEGV` and `SIGABRT`. Panics if
327    /// something goes wrong.
328    fn register_signal_handler() {
329        let flags = signal::SaFlags::empty().bitand(signal::SA_SIGINFO);
330        let handler = signal::SigHandler::Handler(Self::signal_handler);
331        let sa = signal::SigAction::new(handler, flags, signal::SigSet::empty());
332
333        unsafe {
334            signal::sigaction(signal::SIGINT, &sa).unwrap();
335            signal::sigaction(signal::SIGTERM, &sa).unwrap();
336            signal::sigaction(signal::SIGSEGV, &sa).unwrap();
337            signal::sigaction(signal::SIGABRT, &sa).unwrap();
338        }
339    }
340
341    /// System signal handler.
342    ///
343    /// Normally `SIGINT` and `SIGTERM` signals should be blocked and be handled by `Dispatcher` and
344    /// this function should be only able to catch these signals after `Dispatcher` exited.
345    ///
346    /// `SIGSEGV` and `SIGABRT` are handler by exiting.
347    #[cfg_attr(rustfmt, rustfmt_skip)]
348    extern fn signal_handler(signum: libc::c_int) {
349        if (signum == signal::SIGSEGV as libc::c_int)
350        || (signum == signal::SIGABRT as libc::c_int) {
351            log_info1!("Signal {} received asynchronously", signum);
352            log::backtrace();
353            std::process::exit(1);
354        } else if (signum == signal::SIGINT as libc::c_int)
355        || (signum == signal::SIGTERM as libc::c_int) {
356            log_info1!("Signal {} received asynchronously", signum);
357            log::backtrace();
358        } else {
359            log_info2!("Signal {} received asynchronously: ignore", signum);
360        }
361    }
362}
363
364// -------------------------------------------------------------------------------------------------
365
366impl Drop for Env {
367    fn drop(&mut self) {
368        self.cleanup();
369        log_info1!("Thank you for running {}! Bye!", self.progname);
370    }
371}
372
373// -------------------------------------------------------------------------------------------------