runtime.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. //
  2. // imag - the personal information management suite for the commandline
  3. // Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
  4. //
  5. // This library is free software; you can redistribute it and/or
  6. // modify it under the terms of the GNU Lesser General Public
  7. // License as published by the Free Software Foundation; version
  8. // 2.1 of the License.
  9. //
  10. // This library is distributed in the hope that it will be useful,
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. // Lesser General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU Lesser General Public
  16. // License along with this library; if not, write to the Free Software
  17. // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  18. //
  19. use std::path::PathBuf;
  20. use std::process::Command;
  21. use std::env;
  22. use std::io::stderr;
  23. use std::io::Write;
  24. pub use clap::App;
  25. use clap::{Arg, ArgMatches};
  26. use log;
  27. use log::LogLevelFilter;
  28. use configuration::Configuration;
  29. use error::RuntimeError;
  30. use error::RuntimeErrorKind;
  31. use error::MapErrInto;
  32. use logger::ImagLogger;
  33. use libimagstore::store::Store;
  34. #[derive(Debug)]
  35. pub struct Runtime<'a> {
  36. rtp: PathBuf,
  37. configuration: Option<Configuration>,
  38. cli_matches: ArgMatches<'a>,
  39. store: Store,
  40. }
  41. impl<'a> Runtime<'a> {
  42. /**
  43. * Gets the CLI spec for the program and retreives the config file path (or uses the default on
  44. * in $HOME/.imag/config, $XDG_CONFIG_DIR/imag/config or from env("$IMAG_CONFIG")
  45. * and builds the Runtime object with it.
  46. *
  47. * The cli_spec object should be initially build with the ::get_default_cli_builder() function.
  48. *
  49. */
  50. pub fn new(cli_spec: App<'a, 'a>) -> Result<Runtime<'a>, RuntimeError> {
  51. use std::env;
  52. use libimagstore::hook::position::HookPosition as HP;
  53. use libimagstore::hook::Hook;
  54. use libimagstore::error::StoreErrorKind;
  55. use libimagstorestdhook::debug::DebugHook;
  56. use libimagstorestdhook::vcs::git::delete::DeleteHook as GitDeleteHook;
  57. use libimagstorestdhook::vcs::git::update::UpdateHook as GitUpdateHook;
  58. use libimagstorestdhook::vcs::git::store_unload::StoreUnloadHook as GitStoreUnloadHook;
  59. use libimagerror::trace::trace_error;
  60. use libimagerror::trace::trace_error_dbg;
  61. use libimagerror::into::IntoError;
  62. use configuration::error::ConfigErrorKind;
  63. let matches = cli_spec.get_matches();
  64. let is_debugging = matches.is_present("debugging");
  65. let is_verbose = matches.is_present("verbosity");
  66. let colored = !matches.is_present("no-color-output");
  67. Runtime::init_logger(is_debugging, is_verbose, colored);
  68. let rtp : PathBuf = matches.value_of("runtimepath")
  69. .map_or_else(|| {
  70. env::var("HOME")
  71. .map(PathBuf::from)
  72. .map(|mut p| { p.push(".imag"); p})
  73. .unwrap_or_else(|_| {
  74. panic!("You seem to be $HOME-less. Please get a $HOME before using this software. We are sorry for you and hope you have some accommodation anyways.");
  75. })
  76. }, PathBuf::from);
  77. let storepath = matches.value_of("storepath")
  78. .map_or_else(|| {
  79. let mut spath = rtp.clone();
  80. spath.push("store");
  81. spath
  82. }, PathBuf::from);
  83. let configpath = matches.value_of("config")
  84. .map_or_else(|| rtp.clone(), PathBuf::from);
  85. let cfg = match Configuration::new(&configpath) {
  86. Err(e) => if e.err_type() != ConfigErrorKind::NoConfigFileFound {
  87. return Err(RuntimeErrorKind::Instantiate.into_error_with_cause(Box::new(e)));
  88. } else {
  89. warn!("No config file found.");
  90. warn!("Continuing without configuration file");
  91. None
  92. },
  93. Ok(mut cfg) => {
  94. if let Err(e) = cfg.override_config(get_override_specs(&matches)) {
  95. error!("Could not apply config overrides");
  96. trace_error(&e);
  97. // TODO: continue question (interactive)
  98. }
  99. Some(cfg)
  100. }
  101. };
  102. let store_config = match cfg {
  103. Some(ref c) => c.store_config().cloned(),
  104. None => None,
  105. };
  106. if is_debugging {
  107. write!(stderr(), "Config: {:?}\n", cfg).ok();
  108. write!(stderr(), "Store-config: {:?}\n", store_config).ok();
  109. }
  110. Store::new(storepath.clone(), store_config).map(|mut store| {
  111. // If we are debugging, generate hooks for all positions
  112. if is_debugging {
  113. let hooks : Vec<(Box<Hook>, &str, HP)> = vec![
  114. (Box::new(DebugHook::new(HP::PreCreate)) , "debug", HP::PreCreate),
  115. (Box::new(DebugHook::new(HP::PostCreate)) , "debug", HP::PostCreate),
  116. (Box::new(DebugHook::new(HP::PreRetrieve)) , "debug", HP::PreRetrieve),
  117. (Box::new(DebugHook::new(HP::PostRetrieve)) , "debug", HP::PostRetrieve),
  118. (Box::new(DebugHook::new(HP::PreUpdate)) , "debug", HP::PreUpdate),
  119. (Box::new(DebugHook::new(HP::PostUpdate)) , "debug", HP::PostUpdate),
  120. (Box::new(DebugHook::new(HP::PreDelete)) , "debug", HP::PreDelete),
  121. (Box::new(DebugHook::new(HP::PostDelete)) , "debug", HP::PostDelete),
  122. ];
  123. // If hook registration fails, trace the error and warn, but continue.
  124. for (hook, aspectname, position) in hooks {
  125. if let Err(e) = store.register_hook(position, &String::from(aspectname), hook) {
  126. if e.err_type() == StoreErrorKind::HookRegisterError {
  127. trace_error_dbg(&e);
  128. warn!("Registering debug hook with store failed");
  129. } else {
  130. trace_error(&e);
  131. };
  132. }
  133. }
  134. }
  135. let sp = storepath;
  136. let hooks : Vec<(Box<Hook>, &str, HP)> = vec![
  137. (Box::new(GitDeleteHook::new(sp.clone(), HP::PostDelete)), "vcs", HP::PostDelete),
  138. (Box::new(GitUpdateHook::new(sp.clone(), HP::PostUpdate)), "vcs", HP::PostUpdate),
  139. (Box::new(GitStoreUnloadHook::new(sp)), "vcs", HP::StoreUnload),
  140. ];
  141. for (hook, aspectname, position) in hooks {
  142. if let Err(e) = store.register_hook(position, &String::from(aspectname), hook) {
  143. if e.err_type() == StoreErrorKind::HookRegisterError {
  144. trace_error_dbg(&e);
  145. warn!("Registering git hook with store failed");
  146. } else {
  147. trace_error(&e);
  148. };
  149. }
  150. }
  151. Runtime {
  152. cli_matches: matches,
  153. configuration: cfg,
  154. rtp: rtp,
  155. store: store,
  156. }
  157. })
  158. .map_err_into(RuntimeErrorKind::Instantiate)
  159. }
  160. /**
  161. * Get a commandline-interface builder object from `clap`
  162. *
  163. * This commandline interface builder object already contains some predefined interface flags:
  164. * * -v | --verbose for verbosity
  165. * * --debug for debugging
  166. * * -c <file> | --config <file> for alternative configuration file
  167. * * -r <path> | --rtp <path> for alternative runtimepath
  168. * * --store <path> for alternative store path
  169. * Each has the appropriate help text included.
  170. *
  171. * The `appname` shall be "imag-<command>".
  172. */
  173. pub fn get_default_cli_builder(appname: &'a str,
  174. version: &'a str,
  175. about: &'a str)
  176. -> App<'a, 'a>
  177. {
  178. App::new(appname)
  179. .version(version)
  180. .author("Matthias Beyer <mail@beyermatthias.de>")
  181. .about(about)
  182. .arg(Arg::with_name(Runtime::arg_verbosity_name())
  183. .short("v")
  184. .long("verbose")
  185. .help("Enables verbosity")
  186. .required(false)
  187. .takes_value(false))
  188. .arg(Arg::with_name(Runtime::arg_debugging_name())
  189. .long("debug")
  190. .help("Enables debugging output")
  191. .required(false)
  192. .takes_value(false))
  193. .arg(Arg::with_name(Runtime::arg_no_color_output_name())
  194. .long("no-color")
  195. .help("Disable color output")
  196. .required(false)
  197. .takes_value(false))
  198. .arg(Arg::with_name(Runtime::arg_config_name())
  199. .long("config")
  200. .help("Path to alternative config file")
  201. .required(false)
  202. .takes_value(true))
  203. .arg(Arg::with_name(Runtime::arg_config_override_name())
  204. .long("override-config")
  205. .help("Override a configuration settings. Use 'key=value' pairs, where the key is a path in the TOML configuration. The value must be present in the configuration and be convertible to the type of the configuration setting. If the argument does not contain a '=', it gets ignored. Setting Arrays and Tables is not yet supported.")
  206. .required(false)
  207. .takes_value(true))
  208. .arg(Arg::with_name(Runtime::arg_runtimepath_name())
  209. .long("rtp")
  210. .help("Alternative runtimepath")
  211. .required(false)
  212. .takes_value(true))
  213. .arg(Arg::with_name(Runtime::arg_storepath_name())
  214. .long("store")
  215. .help("Alternative storepath. Must be specified as full path, can be outside of the RTP")
  216. .required(false)
  217. .takes_value(true))
  218. .arg(Arg::with_name(Runtime::arg_editor_name())
  219. .long("editor")
  220. .help("Set editor")
  221. .required(false)
  222. .takes_value(true))
  223. }
  224. pub fn arg_names() -> Vec<&'static str> {
  225. vec![
  226. Runtime::arg_verbosity_name(),
  227. Runtime::arg_debugging_name(),
  228. Runtime::arg_no_color_output_name(),
  229. Runtime::arg_config_name(),
  230. Runtime::arg_config_override_name(),
  231. Runtime::arg_runtimepath_name(),
  232. Runtime::arg_storepath_name(),
  233. Runtime::arg_editor_name(),
  234. ]
  235. }
  236. pub fn arg_verbosity_name() -> &'static str {
  237. "verbosity"
  238. }
  239. pub fn arg_debugging_name() -> &'static str {
  240. "debugging"
  241. }
  242. pub fn arg_no_color_output_name() -> &'static str {
  243. "no-color-output"
  244. }
  245. pub fn arg_config_name() -> &'static str {
  246. "config"
  247. }
  248. pub fn arg_config_override_name() -> &'static str {
  249. "config-override"
  250. }
  251. pub fn arg_runtimepath_name() -> &'static str {
  252. "runtimepath"
  253. }
  254. pub fn arg_storepath_name() -> &'static str {
  255. "storepath"
  256. }
  257. pub fn arg_editor_name() -> &'static str {
  258. "editor"
  259. }
  260. /**
  261. * Initialize the internal logger
  262. */
  263. fn init_logger(is_debugging: bool, is_verbose: bool, colored: bool) {
  264. use std::env::var as env_var;
  265. use env_logger;
  266. if env_var("IMAG_LOG_ENV").is_ok() {
  267. env_logger::init().unwrap();
  268. } else {
  269. let lvl = if is_debugging {
  270. LogLevelFilter::Debug
  271. } else if is_verbose {
  272. LogLevelFilter::Info
  273. } else {
  274. LogLevelFilter::Warn
  275. };
  276. log::set_logger(|max_log_lvl| {
  277. max_log_lvl.set(lvl);
  278. debug!("Init logger with {}", lvl);
  279. Box::new(ImagLogger::new(lvl.to_log_level().unwrap()).with_color(colored))
  280. })
  281. .map_err(|_| {
  282. panic!("Could not setup logger");
  283. })
  284. .ok();
  285. }
  286. }
  287. /**
  288. * Get the verbosity flag value
  289. */
  290. pub fn is_verbose(&self) -> bool {
  291. self.cli_matches.is_present("verbosity")
  292. }
  293. /**
  294. * Get the debugging flag value
  295. */
  296. pub fn is_debugging(&self) -> bool {
  297. self.cli_matches.is_present("debugging")
  298. }
  299. /**
  300. * Get the runtimepath
  301. */
  302. pub fn rtp(&self) -> &PathBuf {
  303. &self.rtp
  304. }
  305. /**
  306. * Get the commandline interface matches
  307. */
  308. pub fn cli(&self) -> &ArgMatches {
  309. &self.cli_matches
  310. }
  311. /**
  312. * Get the configuration object
  313. */
  314. pub fn config(&self) -> Option<&Configuration> {
  315. self.configuration.as_ref()
  316. }
  317. /**
  318. * Get the store object
  319. */
  320. pub fn store(&self) -> &Store {
  321. &self.store
  322. }
  323. pub fn editor(&self) -> Option<Command> {
  324. self.cli()
  325. .value_of("editor")
  326. .map(String::from)
  327. .or({
  328. match self.configuration {
  329. Some(ref c) => c.editor().cloned(),
  330. _ => None,
  331. }
  332. })
  333. .or(env::var("EDITOR").ok())
  334. .map(Command::new)
  335. }
  336. }
  337. fn get_override_specs(matches: &ArgMatches) -> Vec<String> {
  338. matches
  339. .values_of("config-override")
  340. .map(|values| {
  341. values
  342. .filter(|s| {
  343. let b = s.contains("=");
  344. if !b { warn!("override '{}' does not contain '=' - will be ignored!", s); }
  345. b
  346. })
  347. .map(String::from)
  348. .collect()
  349. })
  350. .unwrap_or(vec![])
  351. }