diff --git a/Cargo.lock b/Cargo.lock index 15d06a9..a50cbd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,7 +184,7 @@ dependencies = [ [[package]] name = "dip" -version = "0.1.0" +version = "0.1.3" dependencies = [ "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.23 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/src/config.rs b/src/config.rs index 04a2409..46f2c33 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,19 @@ +//! Configuration. + +use std::default::Default; +use std::env; use std::path::{Path, PathBuf}; +use std::sync::mpsc; +use std::time::Duration; use failure::{err_msg, Error}; +use notify::{self, RecommendedWatcher, RecursiveMode, Watcher}; use walkdir::WalkDir; use Hook; use {HOOKS, PROGRAMS}; +/// The configuration to be parsed from the command line. #[derive(Debug, StructOpt)] pub struct Config { /// The root configuration directory for dip. This argument is required. @@ -20,27 +28,38 @@ pub struct Config { pub hook: Option, } -impl Config { - pub fn new(root: impl AsRef) -> Self { - let root = root.as_ref().to_path_buf(); +impl Default for Config { + fn default() -> Self { + let root = env::current_dir().unwrap(); assert!(root.exists()); let bind = "0.0.0.0:5000".to_owned(); let hook = None; Config { root, bind, hook } } - pub fn bind(mut self, value: Option) -> Config { - if let Some(value) = value { - self.bind = value; +} + +pub(crate) fn watch

(root: P) -> notify::Result<()> +where + P: AsRef, +{ + let (tx, rx) = mpsc::channel(); + let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(1))?; + println!("Watching {:?}", root.as_ref().to_path_buf()); + watcher.watch(root.as_ref(), RecursiveMode::Recursive)?; + loop { + match rx.recv() { + Ok(_) => { + // for now, naively reload entire config every time + // TODO: don't do this + load_config(root.as_ref()) + } + Err(e) => eprintln!("watch error: {:?}", e), } - return self; - } - pub fn hook(mut self, value: Option) -> Config { - self.hook = value; - return self; } } +/// Load config from the root directory. This is called by the watcher. pub fn load_config

(root: P) where P: AsRef, @@ -48,7 +67,13 @@ where println!("Reloading config..."); // hold on to the lock while config is being reloaded { - let mut programs = PROGRAMS.lock().unwrap(); + let mut programs = match PROGRAMS.lock() { + Ok(programs) => programs, + Err(err) => { + eprintln!("Could not acquire programs lock: {}", err); + return; + } + }; // TODO: some kind of smart diff programs.clear(); @@ -80,7 +105,13 @@ where } } { - let mut hooks = HOOKS.lock().unwrap(); + let mut hooks = match HOOKS.lock() { + Ok(hooks) => hooks, + Err(err) => { + eprintln!("Could not acquire hooks lock: {}", err); + return; + } + }; hooks.clear(); let hooks_dir = { let mut p = root.as_ref().to_path_buf(); diff --git a/src/handler.rs b/src/handler.rs index 49ba14e..434a59f 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -15,16 +15,21 @@ use toml::Value as TomlValue; use github; +/// A single instance of handler as defined by the config. #[derive(Clone, Debug)] pub struct Handler { - pub config: TomlValue, - pub action: Action, + pub(crate) config: TomlValue, + pub(crate) action: Action, } +/// Describes an action that a hook can take. #[derive(Clone)] pub enum Action { + /// A builtin function (for example, the Github handler). Builtin(fn(&TomlValue, &JsonValue) -> Result), + /// A command represents a string to be executed by `bash -c`. Command(String), + /// A program represents one of the handlers specified in the `handlers` directory. Program(String), } @@ -38,10 +43,7 @@ impl fmt::Debug for Action { } impl Handler { - pub fn config(&self) -> &TomlValue { - &self.config - } - pub fn from(config: &TomlValue) -> Result { + pub(crate) fn from(config: &TomlValue) -> Result { let handler = config .get("type") .ok_or(err_msg("No 'type' found."))? @@ -57,23 +59,13 @@ impl Handler { Action::Command(command.to_owned()) } "github" => Action::Builtin(github::main), - handler => { - // let programs = HANDLERS.lock().unwrap(); - // let program = programs - // .get(handler) - // .ok_or(err_msg(format!("'{}' is not a valid executable", handler))) - // .and_then(|value| { - // value - // .canonicalize() - // .map_err(|_| err_msg("failed to canonicalize the path")) - // }).map(|value| value.clone())?; - Action::Program(handler.to_owned()) - } + handler => Action::Program(handler.to_owned()), }; let config = config.clone(); Ok(Handler { config, action }) } + /// Runs the given [action](Action) and produces a [Future](Future). pub fn run( config: TomlValue, action: Action, @@ -93,7 +85,11 @@ impl Handler { let command_helper = move |command: &mut Command| { command .current_dir(&temp_path) - .env("DIP_WORKDIR", &temp_path); + .env("DIP_ROOT", "lol") + .env("DIP_WORKDIR", &temp_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); }; let output: Box + Send> = match action { @@ -105,13 +101,7 @@ impl Handler { // TODO: allow some kind of simple variable replacement let mut command = Command::new("/bin/bash"); command_helper(&mut command); - let child = command - .env("DIP_ROOT", "lol") - .arg("-c") - .arg(cmd) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); + let child = command.arg("-c").arg(cmd); let result = child .output_async() .map_err(|err| err_msg(format!("failed to spawn child: {}", err))) @@ -131,12 +121,8 @@ impl Handler { let mut command = Command::new(&path); command_helper(&mut command); let mut child = command - .env("DIP_ROOT", "") .arg("--config") .arg(config_str) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) .spawn_async() .expect("could not spawn child"); diff --git a/src/hook.rs b/src/hook.rs index 068454e..acfe089 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -1,3 +1,5 @@ +//! The webhook. + use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; @@ -10,12 +12,14 @@ use toml::Value; use Handler; +/// A webhook. pub struct Hook { name: String, handlers: Vec, } impl Hook { + /// Creates a hook from a (name, config) pair. pub fn from(name: impl Into, config: &Value) -> Result { let name = name.into(); let handlers = config @@ -28,6 +32,8 @@ impl Hook { .collect::, _>>()?; Ok(Hook { name, handlers }) } + + /// Creates a hook from a configuration file. pub fn from_file

(path: P) -> Result where P: AsRef, @@ -47,10 +53,13 @@ impl Hook { let hook = Hook::from(filename, &config)?; Ok(hook) } + + /// Gets the name of this hook. pub fn get_name(&self) -> String { self.name.clone() } - pub fn handle(&self, req: JsonValue, temp_path: PathBuf) -> Result { + + pub(crate) fn handle(&self, req: JsonValue, temp_path: PathBuf) -> Result { let handlers = self .handlers .iter() diff --git a/src/lib.rs b/src/lib.rs index 91ae790..dd13e82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,19 @@ //! # Dip +//! +//! The configurable webhook server. Latest stable binary releases for Linux are available on the [releases][1] page. +//! +//! ## Getting Started +//! +//! Setup is incredibly simple: first, obtain a copy of `dip` either through the binary releases page or by compiling from source. +//! Then, create a directory that you'll use as your `DIP_ROOT` directory. It should look like this: +//! +//! ```text +//! +//! ``` +//! +//! [1]: https://github.com/acmumn/dip/releases + +#![deny(missing_docs)] extern crate hmac; extern crate secstr; @@ -26,24 +41,22 @@ extern crate toml; extern crate walkdir; pub mod config; -pub mod github; -pub mod handler; +mod github; +mod handler; pub mod hook; -pub mod service; +mod service; use std::collections::HashMap; use std::net::SocketAddrV4; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::str::FromStr; -use std::sync::{mpsc, Arc, Mutex}; +use std::sync::{Arc, Mutex}; use std::thread; -use std::time::Duration; use failure::Error; use hyper::rt::Future; use hyper::service::service_fn; use hyper::Server; -use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use regex::Regex; pub use config::Config; @@ -54,38 +67,19 @@ use service::*; const URIPATTERN_STR: &str = r"/webhook/(?P[A-Za-z._][A-Za-z0-9._]*)"; lazy_static! { - static ref URIPATTERN: Regex = Regex::new(URIPATTERN_STR).unwrap(); + static ref URIPATTERN: Regex = + Regex::new(URIPATTERN_STR).expect("Could not compile regular expression."); static ref PROGRAMS: Arc>> = Arc::new(Mutex::new(HashMap::new())); static ref HOOKS: Arc>> = Arc::new(Mutex::new(HashMap::new())); } -fn watch

(root: P) -> notify::Result<()> -where - P: AsRef, -{ - let (tx, rx) = mpsc::channel(); - let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(1))?; - println!("Watching {:?}", root.as_ref().to_path_buf()); - watcher.watch(root.as_ref(), RecursiveMode::Recursive)?; - loop { - match rx.recv() { - Ok(_) => { - // for now, naively reload entire config every time - // TODO: don't do this - config::load_config(root.as_ref()) - } - Err(e) => eprintln!("watch error: {:?}", e), - } - } -} - /// Main entry point of the entire application. pub fn run(config: &Config) -> Result<(), Error> { config::load_config(&config.root); let v = config.root.clone(); - thread::spawn(|| watch(v)); + thread::spawn(|| config::watch(v)); let addr: SocketAddrV4 = SocketAddrV4::from_str(config.bind.as_ref())?; let server = Server::bind(&addr.into()) diff --git a/src/service.rs b/src/service.rs index 7f97b67..a54fbae 100644 --- a/src/service.rs +++ b/src/service.rs @@ -6,7 +6,9 @@ use mktemp::Temp; use {HOOKS, URIPATTERN}; -pub fn dip_service(req: Request) -> Box, Error = Error> + Send> { +pub(crate) fn dip_service( + req: Request, +) -> Box, Error = Error> + Send> { let path = req.uri().path().to_owned(); let captures = match URIPATTERN.captures(path.as_ref()) { Some(value) => value,