diff --git a/Cargo.toml b/Cargo.toml index 37a3f94..329a080 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,12 @@ build = "src/build.rs" users = "0.11.0" regex = "1.0.1" futures-util = "0.3" -async-trait = "0.1" -tokio = { version = "1.6.1", features = ["rt", "process", "io-util", "macros", "sync"] } +async-trait = "0.1.65" +tokio = { version = "1.26", features = ["rt", "process", "io-util", "macros", "sync"] } once_cell = "1.4" +slog = { version = "2", features = ["max_level_trace", "release_max_level_trace"] } +slog-term = "2.5" +slog-async = "2.7" [dev-dependencies] tempfile = "3" diff --git a/src/async_utils.rs b/src/async_utils.rs index aaae4db..d4443a5 100644 --- a/src/async_utils.rs +++ b/src/async_utils.rs @@ -36,26 +36,20 @@ impl Future for TimerFuture { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut state = self.state.lock().unwrap(); - if let State::Completed = *state { - return Poll::Ready(()); - } - - if let State::NotStarted(duration) = *state { - let thread_state = self.state.clone(); - thread::spawn(move || { - thread::sleep(duration); - let mut state = thread_state.lock().unwrap(); - let waker = if let State::Running(waker) = &*state { - Some(waker.clone()) - } else { - None - }; - *state = State::Completed; - if let Some(w) = waker { - w.wake(); - } - }); - } + match *state { + State::Completed => return Poll::Ready(()), + State::NotStarted(duration) => { + let thread_state = self.state.clone(); + thread::spawn(move || { + thread::sleep(duration); + let mut state = thread_state.lock().unwrap(); + if let State::Running(waker) = std::mem::replace(&mut *state, State::Completed) { + waker.wake(); + }; + }); + } + State::Running(_) => {} + }; *state = State::Running(cx.waker().clone()); Poll::Pending @@ -84,10 +78,10 @@ mod test { loop { futures_util::select! { _ = sleep => {}, - _ = ok => assert!((Instant::now() - start).as_millis() < 100), + _ = ok => assert!(start.elapsed().as_millis() < 100), complete => break, } } - }) + }); } } diff --git a/src/build.rs b/src/build.rs index 02566e2..772339c 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,57 +1,53 @@ use std::env; -use std::fs::{read_dir, File}; +use std::error::Error; +use std::fs::{read as read_file, File}; use std::io::ErrorKind::NotFound; -use std::io::Read; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::Path; -fn get_const_name>(p: &P) -> String { - let mut file_name_without_extension = p.clone().into(); - file_name_without_extension.set_extension(""); - String::from( - file_name_without_extension - .file_name() - .unwrap() - .to_string_lossy(), - ) - .to_uppercase() +pub fn create_static_output( + source_path: &Path, + mut target: impl Write, +) -> Result<(), Box> { + let const_name = source_path + .file_stem() + .ok_or("Not a filename")? + .to_str() + .ok_or("Filename is not valid unicode")? + .to_uppercase(); + let content = String::from_utf8(read_file(source_path)?)?; + let fence = content.chars().filter(|&c| c == '#').collect::() + "#"; + + writeln!( + target, + "pub const {const_name}: &str = r{fence}\"{content}\"{fence};" + )?; + Ok(()) } -pub fn create_static_output_files(source_dir: &str) { - let out_dir = env::var("OUT_DIR").unwrap(); - let dest_path = Path::new(&out_dir).join("static_files.rs"); - let mut f = File::create(&dest_path).unwrap(); - match read_dir(source_dir) { +pub fn create_static_output_files( + source_dir: &Path, + dest_path: &Path, +) -> Result<(), Box> { + let mut f = File::create(dest_path)?; + match source_dir.read_dir() { Ok(dir_content) => { for maybe_dir_entry in dir_content { - let file_path = maybe_dir_entry.unwrap().path(); - let mut buffer = String::new(); - File::open(file_path.clone()) - .unwrap() - .read_to_string(&mut buffer) - .unwrap(); - let fence = buffer.chars().filter(|c| *c == '#').collect::() + "#"; - f.write_all( - format!( - "pub const {}: &str = r{1}\"{2}\"{1};\n", - get_const_name(&file_path), - fence, - buffer - ) - .as_bytes(), - ) - .unwrap(); + create_static_output(&maybe_dir_entry?.path(), &mut f)?; } } Err(err) => { if err.kind() != NotFound { - panic!("Unexpected error: {}", err) + return Err(format!("Unexpected error: {err}").into()); } } } + Ok(()) } -#[allow(unused)] -fn main() { - create_static_output_files("static_files"); +pub fn main() -> Result<(), Box> { + create_static_output_files( + Path::new("static_files"), + &(Path::new(&env::var("OUT_DIR")?).join("static_files.rs")), + ) } diff --git a/src/builder.rs b/src/builder.rs index 9ec4fb1..394d3d1 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -115,12 +115,8 @@ impl ImplementationBuilder> for DefaultBuilder { ) } - type Implementation = CertSymbol< - SetuidCommandRunner<'static, String, StdCommandRunner>, - SetuidCommandRunner<'static, String, StdCommandRunner>, - D, - PathBuf, - >; + type Implementation = + CertSymbol, SetuidCommandRunner, D, PathBuf>; fn create( resource: &Cert, target: & as Resource>::Artifact, @@ -128,7 +124,7 @@ impl ImplementationBuilder> for DefaultBuilder { ) -> Self::Implementation { CertSymbol::new( resource.0.clone(), - SetuidCommandRunner::new(user_name.0, &StdCommandRunner), + SetuidCommandRunner::new(user_name.0), root_cert.into(), account_key.into(), challenges_dir.into(), @@ -286,7 +282,7 @@ impl, C: Clone + Into> &resource.0, cert, key, - nginx::php_snippet(resource.2, &pool.0, &resource.1) + &resource.3, + nginx::php_snippet(resource.2, pool.0, &resource.1) + &resource.3, challenges_snippet_path, ), ), @@ -432,7 +428,7 @@ impl ImplementationBuilder> for DefaultBuilder { ( FileSymbol::new( conf_path.clone().into(), - php_fpm_pool_config(&user_name.0, &socket_path, &resource.1), + php_fpm_pool_config(&user_name.0, socket_path, &resource.1), ), ReloadServiceSymbol::new(StdCommandRunner, service_name.0.clone()), ) @@ -448,7 +444,7 @@ impl> ImplementationBuilder> for De FileSymbol, SystemdUserSessionSymbol<'static, String, StdCommandRunner>, OwnerSymbol, - UserServiceSymbol<'static, PathBuf, String, StdCommandRunner>, + UserServiceSymbol<'static, PathBuf, String>, ); fn create( resource: &SystemdSocketService, @@ -475,12 +471,7 @@ impl> ImplementationBuilder> for De user_name.0.clone(), StdCommandRunner, ), - UserServiceSymbol::new( - socket_path.clone().into(), - user_name.0.clone(), - resource.1, - &StdCommandRunner, - ), + UserServiceSymbol::new(socket_path.clone().into(), user_name.0.clone(), resource.1), ) } } diff --git a/src/command_runner.rs b/src/command_runner.rs index 5be8ddc..70418d8 100644 --- a/src/command_runner.rs +++ b/src/command_runner.rs @@ -63,13 +63,9 @@ impl CommandRunner for StdCommandRunner { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .spawn() - .expect("Failed to spawn child process"); + .spawn()?; let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - stdin - .write_all(input) - .await - .expect("Failed to write to stdin"); + stdin.write_all(input).await?; let res = child.wait_with_output().await; //println!("{:?}", res); #[allow(clippy::let_and_return)] @@ -78,17 +74,13 @@ impl CommandRunner for StdCommandRunner { } #[derive(Debug)] -pub struct SetuidCommandRunner<'a, U: AsRef, C: CommandRunner> { - command_runner: &'a C, +pub struct SetuidCommandRunner> { user_name: U, } -impl<'a, U: AsRef, C: CommandRunner> SetuidCommandRunner<'a, U, C> { - pub fn new(user_name: U, command_runner: &'a C) -> Self { - SetuidCommandRunner { - command_runner, - user_name, - } +impl> SetuidCommandRunner { + pub const fn new(user_name: U) -> Self { + Self { user_name } } } @@ -121,13 +113,13 @@ impl Drop for TempSetEnv<'_> { } #[async_trait(?Send)] -impl, C: CommandRunner> CommandRunner for SetuidCommandRunner<'_, U, C> { +impl> CommandRunner for SetuidCommandRunner { async fn run(&self, program: &str, args: &[&OsStr], input: &[u8]) -> IoResult { let uid = get_user_by_name(self.user_name.as_ref()) .expect("User does not exist") .uid(); let set_home = TempSetEnv::new("HOME", format!("/home/{}", self.user_name.as_ref())); - let set_dbus = TempSetEnv::new("XDG_RUNTIME_DIR", format!("/run/user/{}", uid)); + let set_dbus = TempSetEnv::new("XDG_RUNTIME_DIR", format!("/run/user/{uid}")); //println!("{} {:?}", program, args); let mut child = Command::new(program) .args(args) @@ -151,42 +143,6 @@ impl, C: CommandRunner> CommandRunner for SetuidCommandRunner<'_, } } -#[derive(Debug)] -pub struct SuCommandRunner<'a, C> -where - C: CommandRunner, -{ - command_runner: &'a C, - user_name: &'a str, -} - -impl<'a, C> SuCommandRunner<'a, C> -where - C: 'a + CommandRunner, -{ - pub fn new(user_name: &'a str, command_runner: &'a C) -> SuCommandRunner<'a, C> { - SuCommandRunner { - command_runner, - user_name, - } - } -} - -// Su doesn't set XDG_RUNTIME_DIR -// https://github.com/systemd/systemd/blob/master/src/login/pam_systemd.c#L439 -#[async_trait(?Send)] -impl<'a, C> CommandRunner for SuCommandRunner<'a, C> -where - C: 'a + CommandRunner, -{ - async fn run(&self, program: &str, args: &[&OsStr], input: &[u8]) -> IoResult { - let raw_new_args = [self.user_name, "-s", "/usr/bin/env", "--", program]; - let mut new_args: Vec<&OsStr> = raw_new_args.iter().map(AsRef::as_ref).collect(); - new_args.extend_from_slice(args); - self.command_runner.run("su", &new_args, input).await - } -} - #[cfg(test)] mod test { use crate::args; @@ -207,10 +163,10 @@ mod test { loop { futures_util::select! { _ = res => {}, - _ = ps => assert!((Instant::now() - start).as_millis() < 1000), + _ = ps => assert!(start.elapsed().as_millis() < 1000), complete => break, } } - }) + }); } } diff --git a/src/lib.rs b/src/lib.rs index dc73e82..a63506f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ clippy::cargo_common_metadata, clippy::future_not_send, clippy::missing_errors_doc, + clippy::missing_panics_doc, clippy::module_name_repetitions, rustdoc::all, missing_docs, diff --git a/src/locator.rs b/src/locator.rs index 2b0561c..3322baa 100644 --- a/src/locator.rs +++ b/src/locator.rs @@ -37,7 +37,7 @@ pub trait Policy { #[must_use] fn path_for_data(name: impl Display) -> PathBuf { - Path::new("/root/data").join(format!("_{}", name)) + Path::new("/root/data").join(format!("_{name}")) } } @@ -121,7 +121,7 @@ impl> ResourceLocator> for DefaultLocator

> ResourceLocator> for DefaultLocator { +impl> ResourceLocator> for DefaultLocator { type Prerequisites = Dir; fn locate(resource: &File

) -> ( as Resource>::Artifact, Self::Prerequisites) { ((), Dir(resource.0.as_ref().parent().unwrap().into())) @@ -370,7 +370,7 @@ impl, P: Policy> ResourceLocator> for Defaul php_version, user.0 )), user, - ServiceNameArtifact(format!("php{}-fpm", php_version)), + ServiceNameArtifact(format!("php{php_version}-fpm")), ), (), ) diff --git a/src/loggers.rs b/src/loggers.rs index 1357173..f65fdb7 100644 --- a/src/loggers.rs +++ b/src/loggers.rs @@ -1,44 +1,28 @@ use std::cell::RefCell; use std::cmp::min; -use std::io::stderr; -use std::io::Write; +use std::io::{stderr, Write}; +use std::rc::Rc; // The log crate defines // 1 - Error, 2 - Warn, 3 - Info, 4 - Debug, 5 - Trace pub type Level = usize; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Entry(pub Level, pub S); pub trait Logger { - fn write + Into>(&self, level: Level, msg: S) - where - Self: Sized; - fn writeln + Into>(&self, level: Level, msg: S) - where - Self: Sized; - fn info + Into>(&self, msg: S) - where - Self: Sized, - { + fn write(&self, level: Level, msg: &str); + fn writeln(&self, level: Level, msg: &str); + fn info(&self, msg: &str) { self.writeln(3, msg); } - fn debug + Into>(&self, msg: S) - where - Self: Sized, - { + fn debug(&self, msg: &str) { self.writeln(4, msg); } - fn trace + Into>(&self, msg: S) - where - Self: Sized, - { + fn trace(&self, msg: &str) { self.writeln(5, msg); } - fn put + Into>(&self, entries: impl IntoIterator>) -> usize - where - Self: Sized, - { + fn put<'a>(&self, entries: impl IntoIterator>) -> usize { let mut c = 0; for item in entries { self.writeln(item.0, item.1); @@ -54,21 +38,21 @@ pub struct StdErrLogger { } impl Logger for StdErrLogger { - fn write + Into>(&self, _level: Level, msg: S) { + fn write(&self, _level: Level, msg: &str) { *self.line_started.borrow_mut() = true; - write!(&mut stderr(), "{}", msg.as_ref()).unwrap(); + write!(&mut stderr(), "{msg}").expect("Could not write to stderr"); } - fn writeln + Into>(&self, _level: Level, msg: S) { + fn writeln(&self, _level: Level, msg: &str) { if self.line_started.replace(false) { - writeln!(&mut stderr()).unwrap(); + writeln!(&mut stderr()).expect("Could not write to stderr"); } - writeln!(&mut stderr(), "{}", msg.as_ref()).unwrap(); + writeln!(&mut stderr(), "{msg}").expect("Could not write to stderr"); } } impl Drop for StdErrLogger { fn drop(&mut self) { if *self.line_started.borrow() { - writeln!(&mut stderr()).unwrap(); + writeln!(&mut stderr()).expect("Could not write to stderr"); } } } @@ -86,21 +70,21 @@ impl<'a, L> FilteringLogger<'a, L> { } impl<'a, L: Logger> Logger for FilteringLogger<'a, L> { - fn write + Into>(&self, level: Level, str: S) { + fn write(&self, level: Level, msg: &str) { if level <= self.max_level { - self.logger.write(level, str); + self.logger.write(level, msg); } } - fn writeln + Into>(&self, level: Level, str: S) { + fn writeln(&self, level: Level, msg: &str) { if level <= self.max_level { - self.logger.writeln(level, str); + self.logger.writeln(level, msg); } } } #[derive(Clone, Debug, Default)] pub struct StoringLogger { - log: RefCell<(bool, Vec>)>, + log: Rc>)>>, } impl StoringLogger { @@ -109,30 +93,28 @@ impl StoringLogger { Self::default() } + #[must_use] pub fn release(self) -> Vec> { - self.log.into_inner().1 + Rc::try_unwrap(self.log).unwrap().into_inner().1 } } impl Logger for StoringLogger { - fn write + Into>(&self, level: Level, line: S) { + fn write(&self, level: Level, msg: &str) { let mut log = self.log.borrow_mut(); let entry = if log.0 { - log - .1 - .pop() - .map(|e| Entry(min(e.0, level), e.1 + line.as_ref())) + log.1.pop().map(|e| Entry(min(e.0, level), e.1 + msg)) } else { None } - .unwrap_or_else(|| Entry(level, line.into())); + .unwrap_or_else(|| Entry(level, msg.to_string())); log.0 = true; log.1.push(entry); } - fn writeln + Into>(&self, level: Level, line: S) { + fn writeln(&self, level: Level, msg: &str) { let mut log = self.log.borrow_mut(); log.0 = false; - log.1.push(Entry(level, line.into())); + log.1.push(Entry(level, msg.to_string())); } } diff --git a/src/resources/mod.rs b/src/resources/mod.rs index 9b16405..e470765 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -42,7 +42,7 @@ impl Resource for KeyAndCertBundle { #[derive(Debug, Hash, PartialEq, Eq)] pub struct File

(pub P, pub String); -impl<'a, P> Resource for File

{ +impl

Resource for File

{ type Artifact = (); } @@ -228,7 +228,7 @@ impl Resource for Cron { use std::rc::{Rc, Weak}; pub trait FromResource { - fn from_resource(from: R) -> (Self, Weak) + fn from_resource(from: &Rc) -> (Self, Weak) where Self: Sized; } @@ -250,8 +250,7 @@ macro_rules! default_resources { } $(impl<'a, D> FromResource<$type> for DefaultResources<'a, D> { - fn from_resource(from: $type) -> (Self, Weak<$type>) { - let inner = Rc::new(from); + fn from_resource(inner: &Rc<$type>) -> (Self, Weak<$type>) { (Self::$name(Rc::clone(&inner)), Rc::downgrade(&inner)) } })* diff --git a/src/setup/cache.rs b/src/setup/cache.rs new file mode 100644 index 0000000..1e28287 --- /dev/null +++ b/src/setup/cache.rs @@ -0,0 +1,88 @@ +use super::{Add, AddResult, AddableResource}; +use crate::async_utils::sleep; +use crate::resources::{FromArtifact, FromResource}; +use async_trait::async_trait; +use futures_util::future::{FutureExt, Shared}; +use slog::{trace, Logger}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::fmt::Debug; +use std::future::Future; +use std::hash::Hash; +use std::pin::Pin; +use std::rc::Rc; +use std::time::Duration; + +type ResourceCache = HashMap>>>>; + +#[derive(Debug)] +pub struct Cache { + resources: RefCell>, + inner: Rc, +} + +impl Cache { + pub fn new(inner: I) -> Self { + Self { + resources: RefCell::default(), + inner: Rc::new(inner), + } + } +} + +#[async_trait(?Send)] +impl Add for Cache +where + Rs: Hash + Eq + 'static + FromResource, + As: 'static + FromArtifact + Clone, + I: 'static + Add, +{ + // FIXME: https://github.com/rust-lang/rust-clippy/issues/6353 + #[allow(clippy::await_holding_refcell_ref)] + async fn add(&self, logger: &Rc, resource: Rc, force_run: bool) -> AddResult { + let (storable_resource, weak_resource) = Rs::from_resource(&resource); + let mut resources = self.resources.borrow_mut(); + let result = if let Some(future) = resources.remove(&storable_resource) { + assert!( + !force_run, + "Forcing to run an already-added resource is a logical error" + ); + resources.insert(storable_resource, future.clone()); + drop(resources); + trace!(logger, "Resource already added"); + Ok(future.await) + } else { + let inner_weak = Rc::downgrade(&self.inner); + let logger_weak = Rc::downgrade(logger); + let future = Box::pin(async move { + let inner = inner_weak.upgrade().expect("Dangling!"); + let logger = logger_weak.upgrade().expect("Dangling!"); + let resource = weak_resource.upgrade().expect("Dangling!"); + let result = inner.add(&logger, Rc::clone(&resource), force_run).await; + + // Need to convert Box to String for Clone for Shared + result + .map(|(t, did_run)| (As::from_artifact(t), did_run)) + .map_err(|e| e.to_string()) + }) + .shared(); + let future_clone = future.clone(); + resources.insert( + storable_resource, + (Box::pin(async move { + let result = future_clone.await; + if result.is_err() { + // Step back to give the initial caller time to handle the error before unwrapping + sleep(Duration::from_millis(0)).await; + } + result.unwrap() + }) as Pin>>) + .shared(), + ); + drop(resources); + let result = future.await; + result.map_err(std::convert::Into::into) + }; + result.map(|(t, did_run)| (t.into_artifact(), did_run)) + } +} diff --git a/src/setup/core.rs b/src/setup/core.rs deleted file mode 100644 index 4a640ea..0000000 --- a/src/setup/core.rs +++ /dev/null @@ -1,204 +0,0 @@ -use super::runnable::Runnable; -use super::setup::Setup; -use super::util::{AddableResource, InternalAddResult}; -use super::SymbolRunner; -use crate::async_utils::join; -use crate::loggers::{Logger, StoringLogger}; -use crate::resources::{FromArtifact, FromResource}; -use crate::symbols::Symbol; -use crate::to_artifact::ToArtifact; -use crate::{ImplementationBuilder, ResourceLocator}; -use async_trait::async_trait; -use std::error::Error; -use std::fmt::Debug; -use std::hash::Hash; -use std::marker::PhantomData; - -#[async_trait(?Send)] -pub trait AddGeneric { - async fn add_generic(&self, x: X) -> InternalAddResult; -} - -macro_rules! add_generic { - ( $($name:ident)* ) => ( - #[async_trait(?Send)] - #[allow(non_snake_case)] - impl - AddGeneric<($($name,)*)> for Setup - where - $( - RegularSetupCore: SetupCore<$name, Self>, - As: FromArtifact<$name>, - Rs: FromResource<$name>, - $name::Artifact: Clone - ),* - { - #[allow(unused, clippy::shadow_unrelated)] - async fn add_generic(&self, ($($name,)*): ($($name,)*)) -> Result<(StoringLogger, ($($name::Artifact,)*), bool), (StoringLogger, Box)> - { - let ($($name,)*) = join!($(self.add($name, false),)*); - let logger = StoringLogger::default(); - let mut did_run_any = false; - $( - let (log, artifact, did_run) = $name?; - logger.put(log.release()); - did_run_any = did_run_any || did_run; - let $name = artifact; - )* - Ok((logger, ($($name,)*), did_run_any)) - } - } - ); -} - -for_each_tuple!(add_generic); - -// This is for self-referential T -// FIXME: Wait for specialization -#[async_trait(?Send)] -impl< - SR: 'static + SymbolRunner, - T: AddableResource, - Rs: 'static + Hash + Eq + FromResource, - As: 'static + FromArtifact + Clone, - L: 'static + ResourceLocator>, - B: 'static + ImplementationBuilder, - > AddGeneric> for Setup -where - >::Implementation: Runnable + Debug, - Self: AddGeneric, - T::Artifact: Clone, -{ - async fn add_generic(&self, r: Option) -> InternalAddResult> { - Ok(match r { - Some(r) => { - let (logger, result, did_run) = self.add(r, false).await?; - (logger, Some(result), did_run) - } - None => (StoringLogger::default(), None, false), - }) - } -} - -#[async_trait(?Send)] -impl< - T: AddableResource, - Rs: 'static + Hash + Eq + FromResource, - As: 'static + FromArtifact + Clone, - SR: 'static, - L: 'static, - B: 'static, - > AddGeneric for Setup -where - T::Artifact: Clone, - RegularSetupCore: 'static + SetupCore, -{ - async fn add_generic(&self, r: T) -> InternalAddResult { - self.add(r, false).await - } -} - -#[async_trait(?Send)] -pub trait SetupCore { - async fn add>( - &self, - setup: &S, - resource: RR, - force_run: bool, - ) -> InternalAddResult; -} - -#[derive(Debug)] -pub struct RegularSetupCore { - symbol_runner: SR, - phantom: PhantomData<(L, B)>, -} - -impl RegularSetupCore { - pub fn new(symbol_runner: SR) -> Self { - Self { - symbol_runner, - phantom: PhantomData::default(), - } - } -} - -#[async_trait(?Send)] -impl SetupCore for RegularSetupCore -where - B: ImplementationBuilder, - >::Implementation: Runnable + Debug, - L: ResourceLocator, - S: AddGeneric + AddGeneric<>::Prerequisites>, -{ - async fn add>( - &self, - setup: &S, - resource: RR, - force_run: bool, - ) -> InternalAddResult { - let resource = resource.as_ref(); - let logger = StoringLogger::new(); - logger.write(4, format!("Adding {:?} ... ", resource)); - logger.write(4, format!("(force_run is {})", force_run)); - let (location, location_prereqs) = L::locate(resource); - logger.trace(format!("Adding location prereqs for {:?}", resource)); - let result = setup.add_generic(location_prereqs).await; - if let Err((log, e)) = result { - logger.put(log.release()); - return Err((logger, e)); - } - let (location_prereq_logger, _, location_prereqs_did_run) = result.unwrap(); - logger.put(location_prereq_logger.release()); - logger.trace(format!( - "Location prereqs for {:?} did_run: {}", - resource, location_prereqs_did_run - )); - logger.trace(format!("Adding implementation prereqs for {:?}", resource)); - let result = setup.add_generic(B::prerequisites(resource)).await; - if let Err((log, e)) = result { - logger.put(log.release()); - return Err((logger, e)); - } - let (impl_prereq_logger, prereqs, prereqs_did_run) = result.unwrap(); - logger.put(impl_prereq_logger.release()); - logger.trace(format!( - "Implementation prereqs for {:?} did_run: {}", - resource, prereqs_did_run - )); - logger.trace(format!("Running implementation for {:?}", resource)); - let implementation = B::create(resource, &location, prereqs); - let did_run_result = implementation - .run( - &self.symbol_runner, - &logger, - force_run || location_prereqs_did_run || prereqs_did_run, - ) - .await; - match did_run_result { - Ok(did_run) => { - logger.write(4, "done."); - Ok((logger, location, did_run)) - } - Err(e) => Err((logger, e)), - } - } -} - -#[async_trait(?Send)] -impl SymbolRunner for RegularSetupCore { - async fn run_symbol( - &self, - symbol: &S, - parent_logger: &LOG, - force: bool, - ) -> Result> { - let logger = StoringLogger::new(); - logger.write(4, format!("Directly running {:?} ...", symbol)); - let result = self.symbol_runner.run_symbol(symbol, &logger, force).await; - logger.write(4, "done."); - let max_level = if result.is_err() { 5 } else { 3 }; - parent_logger.put(logger.release().into_iter().filter(|e| e.0 <= max_level)); - result - } -} diff --git a/src/setup/mod.rs b/src/setup/mod.rs index a187a88..8302d77 100644 --- a/src/setup/mod.rs +++ b/src/setup/mod.rs @@ -1,12 +1,388 @@ -mod core; -mod util; -pub use util::{AddResult, AddableResource}; +mod realizer; mod symbol_runner; +mod util; pub use symbol_runner::{ DelayingSymbolRunner, DrySymbolRunner, InitializingSymbolRunner, ReportingSymbolRunner, SymbolRunner, }; +mod cache; mod runnable; -#[allow(clippy::module_inception)] -mod setup; -pub use setup::SetupFacade as Setup; +use crate::loggers::Logger; +use crate::resources::{DefaultArtifacts, DefaultResources, FromArtifact, FromResource}; +use crate::symbols::Symbol; +use crate::{DefaultBuilder, DefaultLocator}; +use crate::{ImplementationBuilder, ResourceLocator}; +use async_trait::async_trait; +use cache::Cache; +use realizer::Realizer; +use runnable::Runnable; +use slog::o; +use std::error::Error; +use std::fmt::Debug; +use std::hash::Hash; +use std::rc::Rc; +use util::{Add, AddGeneric, AddResult, AddableResource, Recorder}; + +// Necessary for the recursive type +#[derive(Debug)] +pub struct ActualSetup(Cache, Rs, As>); + +#[async_trait(?Send)] +impl Add for ActualSetup +where + Cache, Rs, As>: Add, +{ + async fn add(&self, logger: &Rc, r: Rc, force_run: bool) -> AddResult { + self.0.add(logger, r, force_run).await + } +} + +// This is for self-referential T +// FIXME: Wait for specialization +#[async_trait(?Send)] +impl< + SR: 'static + SymbolRunner, + T: AddableResource, + Rs: 'static + Hash + Eq + FromResource, + As: 'static + FromArtifact + Clone, + L: 'static + ResourceLocator>, + B: 'static + ImplementationBuilder, + > AddGeneric> for ActualSetup +where + >::Implementation: Runnable + Debug, + Self: AddGeneric, + T::Artifact: Clone, + // These bounds cannot be replaced by + // `Realizer: Add` + // because the prerequisites are Option, too, and thus this would + // require AddGeneric> to already be implemented +{ + async fn add_generic( + &self, + logger: &Rc, + r: Option, + force_run: bool, + ) -> AddResult> { + Ok(match r { + Some(r) => { + let (result, did_run) = self.add(logger, Rc::new(r), force_run).await?; + (Some(result), did_run) + } + None => (None, false), + }) + } +} + +#[derive(Debug)] +pub struct Setup< + SR, + LOG, + L = DefaultLocator, + B = DefaultBuilder, + Rs = DefaultResources<'static, &'static str>, + As = DefaultArtifacts<'static, &'static str>, +>(LOG, Rc, Rc>); + +impl Setup { + pub fn new(symbol_runner: SR, logger: LOG) -> Self { + Self::new_with(symbol_runner, logger) + } +} + +impl Setup { + pub fn new_with(symbol_runner: SR, logger: LOG) -> Self { + let runner = Rc::new(symbol_runner); + let inner = Rc::new_cyclic(|inner| { + ActualSetup(Cache::new(Realizer::new(Rc::clone(&runner), inner.clone()))) + }); + Self(logger, runner, inner) + } +} + +impl< + SR: 'static, + LOG: 'static + Logger, + L: 'static, + B: 'static, + Rs: 'static + Eq + Hash, + As: 'static, + > Setup +{ + pub async fn add_force(&self, resource: R, force_run: bool) -> AddResult + where + Rs: FromResource, + As: FromArtifact + Clone, + ActualSetup: Add, + { + let recorder = Recorder::default(); + let result = { + let log = Rc::new(slog::Logger::root(recorder.clone(), o!())); + self.2.add(&log, Rc::new(resource), force_run).await + }; + self.log_result(recorder, result.as_ref().map(|(_, did_run)| *did_run)); + result + } + + pub async fn add(&self, resource: R) -> AddResult + where + Rs: FromResource, + As: FromArtifact + Clone, + R::Artifact: Clone, + ActualSetup: Add, + { + self.add_force(resource, false).await + } + + pub async fn run_symbol( + &self, + symbol: S, + force: bool, + ) -> Result> + where + SR: SymbolRunner, + { + let recorder = Recorder::default(); + let result = { + let log = Rc::new(slog::Logger::root( + recorder.clone(), + o!("symbol" => format!("{symbol:?}")), + )); + self.1.run_symbol(&symbol, &log, force).await + }; + self.log_result(recorder, result.as_ref().copied()); + result + } + + fn log_result(&self, recorder: Recorder, result: Result>) { + let log = match result { + Ok(false) => String::new(), + Ok(true) => recorder.into_string(slog::Level::Info), + Err(e) => recorder.into_string(slog::Level::Trace), + }; + if log.is_empty() { + self.0.write(3, "."); + } else { + self.0.writeln(3, &log); + } + } +} + +#[cfg(test)] +mod test { + use super::SymbolRunner; + use crate::async_utils::run; + use crate::loggers::{Entry, StoringLogger}; + use crate::resources::{FromArtifact, FromResource, Resource}; + use crate::symbols::Symbol; + use crate::to_artifact::ToArtifact; + use crate::{ImplementationBuilder, ResourceLocator, Setup}; + use async_trait::async_trait; + use regex::Regex; + use slog::{info, Logger}; + use std::cell::RefCell; + use std::error::Error; + use std::fmt::Debug; + use std::rc::{Rc, Weak}; + + struct TestSymbolRunner { + count: Rc>, + } + + #[async_trait(?Send)] + impl SymbolRunner for TestSymbolRunner { + async fn run_symbol( + &self, + symbol: &S, + logger: &Logger, + force: bool, + ) -> Result> { + info!(logger, "run"); + let run = force || !symbol.target_reached().await?; + if run { + *self.count.borrow_mut() += 1; + } + Ok(run) + } + } + + #[derive(Debug, PartialEq, Eq, Hash)] + struct TestResource(&'static str, T); + impl Resource for TestResource { + type Artifact = (); + } + + #[derive(Debug, Hash, PartialEq, Eq)] + enum Resources { + A(Rc>), + B(Rc>), + } + impl FromResource> for Resources { + fn from_resource( + inner: &Rc>, + ) -> (Self, Weak>) { + (Self::A(Rc::clone(&inner)), Rc::downgrade(&inner)) + } + } + impl FromResource> for Resources { + fn from_resource(inner: &Rc>) -> (Self, Weak>) { + (Self::B(Rc::clone(&inner)), Rc::downgrade(&inner)) + } + } + + #[derive(Clone)] + struct Artifacts; + impl FromArtifact> for Artifacts { + fn from_artifact(_from: ()) -> Self { + Self + } + #[allow(clippy::unused_unit)] + fn into_artifact(self) -> () { + #[allow(clippy::unused_unit)] + () + } + } + + struct TestResourceLocator; + impl ResourceLocator> for TestResourceLocator { + type Prerequisites = (); + fn locate(_resource: &TestResource) -> ( as ToArtifact>::Artifact, ()) { + ((), ()) + } + } + + struct TestImplementationBuilder; + impl ImplementationBuilder> for TestImplementationBuilder { + type Implementation = TestSymbol; + type Prerequisites = TestResource<()>; + + fn prerequisites(resource: &TestResource<&'static str>) -> Self::Prerequisites { + TestResource(resource.1, ()) + } + fn create( + resource: &TestResource<&'static str>, + (): &(), + _inputs: ::Artifact, + ) -> Self::Implementation { + TestSymbol { + reached: resource.0.chars().next().unwrap().is_uppercase(), + } + } + } + impl ImplementationBuilder> for TestImplementationBuilder { + type Implementation = TestSymbol; + type Prerequisites = (); + + fn prerequisites(_resource: &TestResource<()>) -> Self::Prerequisites {} + fn create(resource: &TestResource<()>, (): &(), (): ()) -> Self::Implementation { + TestSymbol { + reached: resource.0.chars().next().unwrap().is_uppercase(), + } + } + } + + #[derive(Debug)] + struct TestSymbol { + reached: bool, + } + + #[async_trait(?Send)] + impl Symbol for TestSymbol { + async fn target_reached(&self) -> Result> { + Ok(self.reached) + } + async fn execute(&self) -> Result<(), Box> { + Ok(()) + } + } + + #[allow(clippy::type_complexity)] + fn get_setup() -> ( + Rc>, + Setup< + TestSymbolRunner, + StoringLogger, + TestResourceLocator, + TestImplementationBuilder, + Resources, + Artifacts, + >, + StoringLogger, + ) { + let count = Rc::new(RefCell::new(0)); + let runner = TestSymbolRunner { + count: Rc::clone(&count), + }; + let logger = StoringLogger::new(); + (count, Setup::new_with(runner, logger.clone()), logger) + } + + #[test] + fn correctly_uses_force() { + run(async { + let (count, setup, _) = get_setup(); + setup.add(TestResource("A", "b")).await.unwrap(); + assert_eq!(*count.borrow(), 2); + setup.add(TestResource("A", "b")).await.unwrap(); + assert_eq!(*count.borrow(), 2); + + let (count, setup, _) = get_setup(); + setup.add(TestResource("A", "B")).await.unwrap(); + assert_eq!(*count.borrow(), 0); + }); + } + + #[test] + fn run_reached_symbol() { + run(async { + let (count, setup, log) = get_setup(); + let did_run = setup + .run_symbol(TestSymbol { reached: true }, false) + .await + .unwrap(); + drop(setup); + assert!(!did_run); + assert_eq!(*count.borrow(), 0); + assert_eq!(log.release(), vec![Entry(3, ".".into())]); + }); + } + + #[test] + fn run_not_reached_symbol() { + run(async { + let (count, setup, log) = get_setup(); + let did_run = setup + .run_symbol(TestSymbol { reached: false }, false) + .await + .unwrap(); + drop(setup); + assert!(did_run); + assert_eq!(*count.borrow(), 1); + let log = log.release(); + assert_eq!(log.len(), 1); + assert_eq!(log[0].0, 3); + let re = Regex::new(r"^symbol: TestSymbol \{ reached: false \}\n \w+ \d{1,2} \d{2}:\d{2}:\d{2}.\d{3} INFO run\n$").unwrap(); + assert!(re.is_match(&log[0].1)); + }); + } + + use super::{ActualSetup, AddGeneric, Cache, Realizer}; + + #[test] + fn empty_tuple_add_generic() { + let setup = Rc::new_cyclic(|inner| { + ActualSetup(Cache::, (), ()>::new( + Realizer::new(Rc::new(()), inner.clone()), + )) + }); + run(async { + assert!(setup + .add_generic( + &Rc::new(slog::Logger::root(slog::Discard, slog::o!())), + (), + false + ) + .await + .is_ok()); + }) + } +} diff --git a/src/setup/realizer.rs b/src/setup/realizer.rs new file mode 100644 index 0000000..2757186 --- /dev/null +++ b/src/setup/realizer.rs @@ -0,0 +1,74 @@ +use super::{Add, AddGeneric, AddResult, AddableResource, Runnable, SymbolRunner}; +use crate::{ImplementationBuilder, ResourceLocator}; +use async_trait::async_trait; +use slog::{debug, o, trace, Logger}; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::rc::{Rc, Weak}; + +#[derive(Debug)] +pub struct Realizer { + symbol_runner: Rc, + outer: Weak, + phantom: PhantomData<(L, B)>, +} + +impl Realizer { + pub fn new(symbol_runner: Rc, outer: Weak) -> Self { + Self { + symbol_runner, + outer, + phantom: PhantomData::default(), + } + } +} + +#[async_trait(?Send)] +impl Add for Realizer +where + R: AddableResource, + SR: SymbolRunner, + L: ResourceLocator, + B: ImplementationBuilder, + >::Implementation: Runnable + Debug, + S: AddGeneric + AddGeneric<>::Prerequisites>, +{ + async fn add(&self, logger: &Rc, resource: Rc, force_run: bool) -> AddResult { + let setup = self.outer.upgrade().unwrap(); + let logger = Rc::new(logger.new(o!("resource" => format!("{resource:?}")))); + trace!(logger, "(force_run is {})", force_run); + let (location, location_prereqs) = L::locate(&resource); + trace!(logger, "Adding location prereqs ..."); + let (_, location_prereqs_did_run) = (*setup) + .add_generic(&logger, location_prereqs, false) + .await?; + trace!( + logger, + "Location prereqs did_run: {}", + location_prereqs_did_run + ); + + trace!(logger, "Adding implementation prereqs ..."); + let (prereqs, prereqs_did_run) = (*setup) + .add_generic(&logger, B::prerequisites(&resource), false) + .await?; + trace!( + logger, + "Implementation prereqs did_run: {}", + prereqs_did_run + ); + + trace!(logger, "Running implementation ..."); + let implementation = B::create(&resource, &location, prereqs); + let did_run = implementation + .run( + &*self.symbol_runner, + &logger, + force_run || location_prereqs_did_run || prereqs_did_run, + ) + .await?; + debug!(logger, "done."); + + Ok((location, did_run)) + } +} diff --git a/src/setup/runnable.rs b/src/setup/runnable.rs index 4e3e087..092ea0f 100644 --- a/src/setup/runnable.rs +++ b/src/setup/runnable.rs @@ -1,16 +1,17 @@ use super::SymbolRunner; -use crate::loggers::Logger; use crate::symbols::Symbol; use async_trait::async_trait; +use slog::Logger; use std::error::Error; use std::fmt::Debug; +// A generalization over symbols and tuples of symbols #[async_trait(?Send)] pub trait Runnable { - async fn run( + async fn run( &self, runner: &R, - logger: &L, + logger: &Logger, force: bool, ) -> Result>; } @@ -21,10 +22,10 @@ impl Runnable for S where Self: Symbol + Debug, { - async fn run( + async fn run( &self, runner: &R, - logger: &L, + logger: &Logger, force: bool, ) -> Result> { runner.run_symbol(self, logger, force).await @@ -38,7 +39,7 @@ macro_rules! runnable_for_tuple { #[allow(non_snake_case)] impl<$($name: Symbol + Debug,)*> Runnable for ($($name,)*) { #[allow(unused)] - async fn run<_R: SymbolRunner, _L: Logger>(&self, runner: &_R, logger: &_L, force: bool) -> Result> { + async fn run<_R: SymbolRunner>(&self, runner: &_R, logger: &Logger, force: bool) -> Result> { let ($($name,)*) = self; let mut result = false; $(result = runner.run_symbol($name, logger, force || result).await? || result;)* @@ -54,10 +55,10 @@ for_each_tuple!(runnable_for_tuple); mod test { use super::Runnable; use crate::async_utils::run; - use crate::loggers::{Logger, StoringLogger}; use crate::symbols::Symbol; use crate::SymbolRunner; use async_trait::async_trait; + use slog::{o, Discard, Logger}; use std::cell::RefCell; use std::error::Error; use std::fmt::Debug; @@ -116,10 +117,10 @@ mod test { #[async_trait(?Send)] impl SymbolRunner for TestSymbolRunner { - async fn run_symbol( + async fn run_symbol( &self, symbol: &S, - _logger: &L, + _logger: &Logger, force: bool, ) -> Result> { let run = force || !symbol.target_reached().await?; @@ -135,7 +136,7 @@ mod test { force: bool, ) -> (Rc>, Result>) { let (count, runner) = get_runner(); - let res = run(runnable.run(&runner, &StoringLogger::new(), force)); + let res = run(runnable.run(&runner, &Logger::root(Discard, o!()), force)); (count, res) } diff --git a/src/setup/setup.rs b/src/setup/setup.rs deleted file mode 100644 index 6d06eef..0000000 --- a/src/setup/setup.rs +++ /dev/null @@ -1,338 +0,0 @@ -use super::core::{RegularSetupCore, SetupCore}; -use super::runnable::Runnable; -use super::util::{AddResult, AddableResource, InternalAddResult}; -use super::SymbolRunner; -use crate::async_utils::sleep; -use crate::loggers::{Logger, StoringLogger}; -use crate::resources::{DefaultArtifacts, DefaultResources, FromArtifact, FromResource}; -use crate::{DefaultBuilder, DefaultLocator}; -use futures_util::future::{FutureExt, Shared}; -use std::cell::{RefCell, RefMut}; -use std::collections::HashMap; -use std::error::Error; -use std::future::Future; -use std::hash::Hash; -use std::pin::Pin; -use std::rc::Rc; -use std::time::Duration; - -type Cache = HashMap>>>>; - -#[derive(Debug)] -struct SetupInner { - core: CORE, - resources: RefCell>, -} - -#[derive(Debug)] -pub struct Setup(Rc, Rs, As>>); - -impl - Setup -{ - fn borrow_resources(&self) -> RefMut<'_, Cache> { - self.0.resources.borrow_mut() - } - - // FIXME: https://github.com/rust-lang/rust-clippy/issues/6353 - #[allow(clippy::await_holding_refcell_ref)] - pub async fn add(&self, resource: R, force_run: bool) -> InternalAddResult - where - Rs: FromResource, - As: FromArtifact + Clone, - R::Artifact: Clone, - RegularSetupCore: SetupCore, - { - let (storable_resource, weak_resource) = Rs::from_resource(resource); - let mut resources = self.borrow_resources(); - let result = if let Some(future) = resources.remove(&storable_resource) { - assert!( - !force_run, - "Forcing to run an already-added resource is a logical error" - ); - resources.insert(storable_resource, future.clone()); - drop(resources); - let logger = StoringLogger::default(); - logger.trace(format!( - "{:?} already added", - weak_resource.upgrade().expect("Dangling!") - )); - let (t, did_run) = future.await; - Ok((logger, t, did_run)) - } else { - let inner_weak = Rc::downgrade(&self.0); - let future = Box::pin(async move { - let this = Self(inner_weak.upgrade().expect("Dangling!")); - let resource = weak_resource.upgrade().expect("Dangling!"); - // Need to convert Box to String for Clone for Shared - let result = this.0.core.add(&this, resource, force_run).await; - - result - .map(|(logger, t, did_run)| (logger, As::from_artifact(t), did_run)) - .map_err(|(logger, e)| (logger, e.to_string())) - }) - .shared(); - let future_clone = future.clone(); - resources.insert( - storable_resource, - (Box::pin(async move { - let result = future_clone.await; - if result.is_err() { - // Step back to give the initial caller time to handle the error before unwrapping - sleep(Duration::from_millis(0)).await; - } - result.map(|(_, t, did_run)| (t, did_run)).unwrap() - }) as Pin>>) - .shared(), - ); - drop(resources); - let result = future.await; - result.map_err(|(logger, e)| (logger, e.into())) - }; - result.map(|(logger, t, did_run)| (logger, t.into_artifact(), did_run)) - } -} - -#[derive(Debug)] -pub struct SetupFacade< - SR, - LOG, - L = DefaultLocator, - B = DefaultBuilder, - Rs = DefaultResources<'static, &'static str>, - As = DefaultArtifacts<'static, &'static str>, ->(LOG, Setup); - -impl SetupFacade { - pub fn new(symbol_runner: SR, logger: LOG) -> Self { - Self::new_with(symbol_runner, logger) - } -} - -impl SetupFacade { - pub fn new_with(symbol_runner: SR, logger: LOG) -> Self { - Self( - logger, - Setup(Rc::new(SetupInner { - core: RegularSetupCore::new(symbol_runner), - resources: RefCell::default(), - })), - ) - } -} - -impl< - SR: 'static, - LOG: 'static + Logger, - L: 'static, - B: 'static, - Rs: 'static + Eq + Hash, - As: 'static, - > SetupFacade -{ - pub async fn add_force(&self, resource: R, force_run: bool) -> AddResult - where - Rs: FromResource, - As: FromArtifact + Clone, - R::Artifact: Clone, - RegularSetupCore: SetupCore>, - { - let result = self.1.add(resource, force_run).await; - match result { - Ok((logger, t, did_run)) => { - if self - .0 - .put(logger.release().into_iter().filter(|e| e.0 <= 3)) - == 0 - { - self.0.write(3, "."); - } - Ok((t, did_run)) - } - Err((logger, e)) => { - self.0.put(logger.release()); - Err(e) - } - } - } - pub async fn add(&self, resource: R) -> AddResult - where - RegularSetupCore: SetupCore>, - Rs: FromResource, - As: FromArtifact + Clone, - R::Artifact: Clone, - SR: SymbolRunner, - { - self.add_force(resource, false).await - } - - pub async fn run_symbol( - &self, - symbol: S, - force: bool, - ) -> Result> - where - RegularSetupCore: SymbolRunner, - { - symbol.run(&(self.1).0.core, &self.0, force).await - } -} - -#[cfg(test)] -mod test { - use super::SymbolRunner; - use crate::async_utils::run; - use crate::loggers::{Logger, StoringLogger}; - use crate::resources::{FromArtifact, FromResource, Resource}; - use crate::symbols::Symbol; - use crate::to_artifact::ToArtifact; - use crate::{ImplementationBuilder, ResourceLocator, Setup}; - use async_trait::async_trait; - use std::cell::RefCell; - use std::error::Error; - use std::fmt::Debug; - use std::rc::{Rc, Weak}; - - struct TestSymbolRunner { - count: Rc>, - } - - #[async_trait(?Send)] - impl SymbolRunner for TestSymbolRunner { - async fn run_symbol( - &self, - symbol: &S, - _logger: &L, - force: bool, - ) -> Result> { - let run = force || !symbol.target_reached().await?; - if run { - *self.count.borrow_mut() += 1; - } - Ok(run) - } - } - - #[derive(Debug, PartialEq, Eq, Hash)] - struct TestResource(&'static str, T); - impl Resource for TestResource { - type Artifact = (); - } - - #[derive(Debug, Hash, PartialEq, Eq)] - enum Resources { - A(Rc>), - B(Rc>), - } - impl FromResource> for Resources { - fn from_resource(from: TestResource<&'static str>) -> (Self, Weak>) { - let inner = Rc::new(from); - (Self::A(Rc::clone(&inner)), Rc::downgrade(&inner)) - } - } - impl FromResource> for Resources { - fn from_resource(from: TestResource<()>) -> (Self, Weak>) { - let inner = Rc::new(from); - (Self::B(Rc::clone(&inner)), Rc::downgrade(&inner)) - } - } - - #[derive(Clone)] - struct Artifacts; - impl FromArtifact> for Artifacts { - fn from_artifact(_from: ()) -> Self { - Self - } - #[allow(clippy::unused_unit)] - fn into_artifact(self) -> () { - #[allow(clippy::unused_unit)] - () - } - } - - struct TestResourceLocator; - impl ResourceLocator> for TestResourceLocator { - type Prerequisites = (); - fn locate(_resource: &TestResource) -> ( as ToArtifact>::Artifact, ()) { - ((), ()) - } - } - - struct TestImplementationBuilder; - impl ImplementationBuilder> for TestImplementationBuilder { - type Implementation = TestSymbol; - type Prerequisites = TestResource<()>; - - fn prerequisites(resource: &TestResource<&'static str>) -> Self::Prerequisites { - TestResource(resource.1, ()) - } - fn create( - resource: &TestResource<&'static str>, - (): &(), - _inputs: ::Artifact, - ) -> Self::Implementation { - TestSymbol { - reached: resource.0.chars().next().unwrap().is_uppercase(), - } - } - } - impl ImplementationBuilder> for TestImplementationBuilder { - type Implementation = TestSymbol; - type Prerequisites = (); - - fn prerequisites(_resource: &TestResource<()>) -> Self::Prerequisites {} - fn create(resource: &TestResource<()>, (): &(), (): ()) -> Self::Implementation { - TestSymbol { - reached: resource.0.chars().next().unwrap().is_uppercase(), - } - } - } - - #[derive(Debug)] - struct TestSymbol { - reached: bool, - } - - #[async_trait(?Send)] - impl Symbol for TestSymbol { - async fn target_reached(&self) -> Result> { - Ok(self.reached) - } - async fn execute(&self) -> Result<(), Box> { - Ok(()) - } - } - - fn get_setup() -> ( - Rc>, - Setup< - TestSymbolRunner, - StoringLogger, - TestResourceLocator, - TestImplementationBuilder, - Resources, - Artifacts, - >, - ) { - let count = Rc::new(RefCell::new(0)); - let runner = TestSymbolRunner { - count: Rc::clone(&count), - }; - (count, Setup::new_with(runner, StoringLogger::new())) - } - - #[test] - fn correctly_uses_force() { - run(async { - let (count, setup) = get_setup(); - setup.add(TestResource("A", "b")).await.unwrap(); - assert_eq!(*count.borrow(), 2); - setup.add(TestResource("A", "b")).await.unwrap(); - assert_eq!(*count.borrow(), 2); - - let (count, setup) = get_setup(); - setup.add(TestResource("A", "B")).await.unwrap(); - assert_eq!(*count.borrow(), 0); - }); - } -} diff --git a/src/setup/symbol_runner.rs b/src/setup/symbol_runner.rs index e9b3b10..ea628a3 100644 --- a/src/setup/symbol_runner.rs +++ b/src/setup/symbol_runner.rs @@ -1,7 +1,7 @@ use crate::async_utils::sleep; -use crate::loggers::Logger; use crate::symbols::Symbol; use async_trait::async_trait; +use slog::{debug, info, o, trace, Logger}; use std::error::Error; use std::fmt; use std::fmt::Debug; @@ -9,10 +9,10 @@ use std::time::Duration; #[async_trait(?Send)] pub trait SymbolRunner { - async fn run_symbol( + async fn run_symbol( &self, symbol: &S, - logger: &L, + logger: &Logger, force: bool, ) -> Result>; } @@ -37,18 +37,19 @@ impl InitializingSymbolRunner { Self } - async fn exec_symbol( + async fn exec_symbol( &self, symbol: &S, - logger: &L, + logger: &Logger, ) -> Result<(), Box> { - logger.info(format!("Executing {:?}", symbol)); + info!(logger, "Executing {:?}", symbol); symbol.execute().await?; let target_reached = symbol.target_reached().await?; - logger.trace(format!( + trace!( + logger, "Symbol reports target_reached: {:?} (should be true)", target_reached - )); + ); if target_reached { Ok(()) } else { @@ -59,25 +60,26 @@ impl InitializingSymbolRunner { #[async_trait(?Send)] impl SymbolRunner for InitializingSymbolRunner { - async fn run_symbol( + async fn run_symbol( &self, symbol: &S, - logger: &L, + logger: &Logger, force: bool, ) -> Result> { let executed = if force { - logger.debug("Forcing symbol execution"); + debug!(logger, "Forcing symbol execution"); self.exec_symbol(symbol, logger).await?; true } else { let target_reached = symbol.target_reached().await?; if target_reached { - logger.debug(format!("{:?} already reached", symbol)); + debug!(logger, "{:?} already reached", symbol); } else { - logger.trace(format!( + trace!( + logger, "Symbol reports target_reached: {:?}", target_reached - )); + ); self.exec_symbol(symbol, logger).await?; } !target_reached @@ -106,10 +108,10 @@ where clippy::cast_possible_truncation, clippy::cast_precision_loss )] - async fn run_symbol( + async fn run_symbol( &self, symbol: &S, - logger: &L, + logger: &Logger, force: bool, ) -> Result> { sleep(Duration::from_millis( @@ -139,23 +141,23 @@ impl DrySymbolRunner { #[async_trait(?Send)] impl SymbolRunner for DrySymbolRunner { - async fn run_symbol( + async fn run_symbol( &self, symbol: &S, - logger: &L, + logger: &Logger, force: bool, ) -> Result> { let would_execute = if force { - logger.info(format!("Would force-execute {:?}", symbol)); + info!(logger, "Would force-execute"); true } else { let target_reached = symbol.target_reached().await?; - logger.debug(format!( - "Symbol reports target_reached: {:?}", - target_reached - )); + debug!( + logger, + "Symbol reports target_reached: {:?}", target_reached + ); if !target_reached { - logger.info(format!("Would execute {:?}", symbol)); + info!(logger, "Would execute"); } !target_reached }; @@ -178,18 +180,19 @@ impl SymbolRunner for ReportingSymbolRunner where R: SymbolRunner, { - async fn run_symbol( + async fn run_symbol( &self, symbol: &S, - logger: &L, + logger: &Logger, force: bool, ) -> Result> { - logger.debug(format!("Running symbol {:?}", symbol)); - let res = self.0.run_symbol(symbol, logger, force).await; + let logger = logger.new(o!("symbol" => format!("{symbol:?}"))); + debug!(logger, "Running ..."); + let res = self.0.run_symbol(symbol, &logger, force).await; if let Err(ref e) = res { - logger.info(format!("Failed on {:?} with {}, aborting.", symbol, e)); + info!(logger, "failed with {}", e); } else { - logger.debug(format!("Successfully finished {:?}", symbol)); + debug!(logger, "Successfully finished"); } res } @@ -200,9 +203,9 @@ mod test { use super::{DrySymbolRunner, InitializingSymbolRunner, ReportingSymbolRunner, SymbolRunner}; use crate::async_utils::sleep; use crate::async_utils::{run, try_join}; - use crate::loggers::StoringLogger; use crate::symbols::Symbol; use async_trait::async_trait; + use slog::{o, Discard, Logger}; use std::cell::RefCell; use std::error::Error; use std::fmt; @@ -263,7 +266,7 @@ mod test { } fn run_symbol(s: S) -> Result> { - run(InitializingSymbolRunner::new().run_symbol(&s, &StoringLogger::new(), false)) + run(InitializingSymbolRunner::new().run_symbol(&s, &Logger::root(Discard, o!()), false)) } #[test] @@ -316,8 +319,8 @@ mod test { let s1 = SleeperSymbol; let s2 = DummySymbol::new(vec![Ok(false), Ok(true)], vec![Ok(())]); - let l1 = StoringLogger::new(); - let l2 = StoringLogger::new(); + let l1 = Logger::root(Discard, o!()); + let l2 = Logger::root(Discard, o!()); let runner1 = InitializingSymbolRunner::new(); let result = try_join!( runner1.run_symbol(&s1, &l1, false), @@ -327,8 +330,6 @@ mod test { assert_eq!(result, (false, true)); let s2 = DummySymbol::new(vec![Ok(false), Ok(true)], vec![Ok(())]); - let l1 = StoringLogger::new(); - let l2 = StoringLogger::new(); let runner2 = DrySymbolRunner::new(); let result = try_join!( runner2.run_symbol(&s1, &l1, false), @@ -338,8 +339,6 @@ mod test { assert_eq!(result, (false, true)); let s2 = DummySymbol::new(vec![Ok(false), Ok(true)], vec![Ok(())]); - let l1 = StoringLogger::new(); - let l2 = StoringLogger::new(); let runner3 = ReportingSymbolRunner::new(runner1); let result = try_join!( runner3.run_symbol(&s1, &l1, false), diff --git a/src/setup/util.rs b/src/setup/util.rs index 542482f..6951165 100644 --- a/src/setup/util.rs +++ b/src/setup/util.rs @@ -1,12 +1,131 @@ -use crate::loggers::StoringLogger; +use crate::async_utils::join; use crate::resources::Resource; use crate::to_artifact::ToArtifact; +use async_trait::async_trait; +use slog::{Drain, Filter, Logger, OwnedKVList, Record}; +use slog_async::AsyncRecord; +use std::cell::RefCell; use std::error::Error; use std::fmt::Debug; +use std::io::{self, Write}; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; pub trait AddableResource: 'static + Resource + Debug {} impl AddableResource for R where R: 'static + Resource + Debug {} pub type AddResult = Result<(::Artifact, bool), Box>; -pub type InternalAddResult = - Result<(StoringLogger, ::Artifact, bool), (StoringLogger, Box)>; + +#[async_trait(?Send)] +pub trait Add { + async fn add(&self, logger: &Rc, resource: Rc, force_run: bool) -> AddResult; +} + +#[async_trait(?Send)] +pub trait AddGeneric { + async fn add_generic(&self, logger: &Rc, x: X, force_run: bool) -> AddResult; +} + +macro_rules! add_generic { + ( $($name:ident)* ) => ( + #[async_trait(?Send)] + #[allow(non_snake_case)] + impl<_S, $($name: AddableResource,)*> + AddGeneric<($($name,)*)> for _S + where + $( + _S: AddGeneric<$name> + ),* + { + #[allow(unused, clippy::shadow_unrelated)] + async fn add_generic(&self, logger: &Rc, ($($name,)*): ($($name,)*), force_run: bool) -> AddResult<($($name,)*)> + { + let ($($name,)*) = join!($(self.add_generic(logger, $name, force_run),)*); + let mut did_run_any = false; + $( + let (artifact, did_run) = $name?; + did_run_any = did_run_any || did_run; + let $name = artifact; + )* + Ok((($($name,)*), did_run_any)) + } + } + ); +} + +for_each_tuple!(add_generic); + +#[async_trait(?Send)] +impl> AddGeneric for S { + async fn add_generic(&self, logger: &Rc, r: R, force_run: bool) -> AddResult { + self.add(logger, Rc::new(r), force_run).await + } +} + +// From https://users.rust-lang.org/t/how-to-send-a-writer-into-a-thread/4965/10 +#[derive(Clone)] +struct Output(Rc>); + +impl Output { + pub fn new(w: W) -> Self { + Self(Rc::new(RefCell::new(w))) + } +} + +impl Write for Output { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.borrow_mut().write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.0.borrow_mut().flush() + } +} + +#[derive(Clone, Default)] +pub struct Recorder(Arc>>); + +impl Drain for Recorder { + type Ok = (); + type Err = slog::Never; + + fn log(&self, record: &Record<'_>, logger_values: &OwnedKVList) -> Result { + self + .0 + .lock() + .unwrap() + .push(AsyncRecord::from(record, logger_values)); + Ok(()) + } +} + +impl Recorder { + pub fn into_string(self, filter_level: slog::Level) -> String { + let output = Output::new(vec![]); + { + let decorator = slog_term::PlainDecorator::new(output.clone()); + let drain = Filter::new( + slog_term::CompactFormat::new(decorator).build(), + move |record| record.level().is_at_least(filter_level), + ); + let Ok(mutex) = Arc::try_unwrap(self.0) else { panic!("cannot unwrap Arc") }; // AsyncRecord does not implement Debug, so we cannot unwrap + for record in mutex.into_inner().unwrap() { + record.log_to(&drain).unwrap(); + } + } + String::from_utf8(Rc::try_unwrap(output.0).unwrap().into_inner()) + .expect("Record output should be valid UTF-8") + } +} + +#[cfg(test)] +mod test { + use super::Recorder; + use slog::Level; + + #[test] + fn records_no_output() { + let recorder = Recorder::default(); + assert_eq!(recorder.into_string(Level::Trace), ""); + } +} diff --git a/src/symbols/git/checkout.rs b/src/symbols/git/checkout.rs index f5399c7..43be14b 100644 --- a/src/symbols/git/checkout.rs +++ b/src/symbols/git/checkout.rs @@ -17,7 +17,7 @@ pub struct Checkout<_C, C, P, S, B> { phantom: PhantomData<_C>, } -impl Checkout<_C, C, P, S, B> { +impl<_C, C, P, S, B> Checkout<_C, C, P, S, B> { pub fn new(target: P, source: S, branch: B, command_runner: C) -> Self { Self { target, @@ -29,7 +29,7 @@ impl Checkout<_C, C, P, S, B> { } } -impl, P: AsRef, S, B> Checkout { +impl<_C: CommandRunner, C: Borrow<_C>, P: AsRef, S, B> Checkout<_C, C, P, S, B> { async fn run_git(&self, args: &[impl AsRef]) -> Result, Box> { let mut new_args = Vec::with_capacity(args.len() + 2); new_args.extend_from_slice(args!["-C", self.target.as_ref()]); @@ -43,8 +43,8 @@ impl, P: AsRef, S, B> Checkout, P: AsRef, S: AsRef, B: AsRef> Symbol - for Checkout +impl<_C: CommandRunner, C: Borrow<_C>, P: AsRef, S: AsRef, B: AsRef> Symbol + for Checkout<_C, C, P, S, B> { async fn target_reached(&self) -> Result> { if !self.target.as_ref().exists() { @@ -132,7 +132,7 @@ mod test { args: RefCell::new(vec![]), }; let checkout: Checkout = - Checkout::new("target", "source", "branch", &c); + Checkout::new(".", "source", "branch", &c); let start = Instant::now(); assert!(run(checkout.target_reached()).unwrap()); let end = Instant::now(); @@ -142,12 +142,11 @@ mod test { assert_eq!( first_two_args, [ - ["-C", "target", "fetch", "source", "branch"], - ["-C", "target", "rev-list", "-1", "HEAD"], + ["-C", ".", "fetch", "source", "branch"], + ["-C", ".", "rev-list", "-1", "HEAD"], ] ); - drop(first_two_args); - assert_eq!(args[2], ["-C", "target", "rev-list", "-1", "FETCH_HEAD"]); + assert_eq!(args[2], ["-C", ".", "rev-list", "-1", "FETCH_HEAD"]); assert!((end - start).as_millis() >= 100); assert!((end - start).as_millis() < 150); diff --git a/src/symbols/mariadb/database.rs b/src/symbols/mariadb/database.rs index caace4e..95ae693 100644 --- a/src/symbols/mariadb/database.rs +++ b/src/symbols/mariadb/database.rs @@ -12,7 +12,7 @@ pub struct Database<'a, D, S, C> { } impl<'a, D, S, C: CommandRunner> Database<'a, D, S, C> { - pub fn new(db_name: D, seed_file: S, command_runner: &'a C) -> Self { + pub const fn new(db_name: D, seed_file: S, command_runner: &'a C) -> Self { Self { db_name, seed_file, diff --git a/src/symbols/mariadb/dump.rs b/src/symbols/mariadb/dump.rs index 11accac..7503ad1 100644 --- a/src/symbols/mariadb/dump.rs +++ b/src/symbols/mariadb/dump.rs @@ -13,7 +13,7 @@ pub struct Dump<'a, N, C, S> { } impl<'a, N, C: CommandRunner, S> Dump<'a, N, C, S> { - pub fn new(db_name: N, storage: S, command_runner: &'a C) -> Self { + pub const fn new(db_name: N, storage: S, command_runner: &'a C) -> Self { Self { db_name, storage, diff --git a/src/symbols/mariadb/user.rs b/src/symbols/mariadb/user.rs index b8eb4b5..9fb3730 100644 --- a/src/symbols/mariadb/user.rs +++ b/src/symbols/mariadb/user.rs @@ -10,7 +10,7 @@ pub struct User<'a, U, C> { } impl<'a, U: AsRef, C: CommandRunner> User<'a, U, C> { - pub fn new(user_name: U, command_runner: &'a C) -> Self { + pub const fn new(user_name: U, command_runner: &'a C) -> Self { Self { user_name, command_runner, diff --git a/src/symbols/npm.rs b/src/symbols/npm.rs index b2a95bc..5275d64 100644 --- a/src/symbols/npm.rs +++ b/src/symbols/npm.rs @@ -12,7 +12,7 @@ pub struct Install<'a, T: AsRef, C: CommandRunner> { } impl<'a, T: AsRef, C: CommandRunner> Install<'a, T, C> { - pub fn new(target: T, command_runner: &'a C) -> Self { + pub const fn new(target: T, command_runner: &'a C) -> Self { Self { target, command_runner, diff --git a/src/symbols/postgresql/database.rs b/src/symbols/postgresql/database.rs index bf36fea..3b825fd 100644 --- a/src/symbols/postgresql/database.rs +++ b/src/symbols/postgresql/database.rs @@ -12,7 +12,7 @@ pub struct PostgreSQLDatabase<'a, N: AsRef, S: AsRef, C: CommandRunner } impl<'a, N: AsRef, S: AsRef, C: CommandRunner> PostgreSQLDatabase<'a, N, S, C> { - pub fn new(name: N, seed_file: S, command_runner: &'a C) -> Self { + pub const fn new(name: N, seed_file: S, command_runner: &'a C) -> Self { PostgreSQLDatabase { name, seed_file, @@ -25,7 +25,7 @@ impl<'a, N: AsRef, S: AsRef, C: CommandRunner> PostgreSQLDatabase<'a, .command_runner .get_output( "su", - args!["-", "postgres", "-c", format!("psql -t -c \"{}\"", sql)], + args!["-", "postgres", "-c", format!("psql -t -c \"{sql}\"")], ) .await?; Ok(String::from_utf8(b)?) diff --git a/src/symbols/saved_directory.rs b/src/symbols/saved_directory.rs index d52091a..6e45c89 100644 --- a/src/symbols/saved_directory.rs +++ b/src/symbols/saved_directory.rs @@ -10,7 +10,7 @@ use std::marker::PhantomData; use std::path::Path; use std::str::FromStr; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum StorageDirection { Load, Store, diff --git a/src/symbols/systemd/user_service.rs b/src/symbols/systemd/user_service.rs index f795786..67ed3b2 100644 --- a/src/symbols/systemd/user_service.rs +++ b/src/symbols/systemd/user_service.rs @@ -8,28 +8,23 @@ use std::path::Path; use std::time::Duration; #[derive(Debug)] -pub struct UserService<'a, S: AsRef, U: AsRef, R: CommandRunner> { +pub struct UserService<'a, S: AsRef, U: AsRef> { socket_path: S, service_name: &'a str, - command_runner: SetuidCommandRunner<'a, U, R>, + command_runner: SetuidCommandRunner, } -impl, U: AsRef, R: CommandRunner> UserService<'static, S, U, R> { - pub fn new( - socket_path: S, - user_name: U, - service_name: &'static str, - command_runner: &'static R, - ) -> Self { +impl, U: AsRef> UserService<'static, S, U> { + pub const fn new(socket_path: S, user_name: U, service_name: &'static str) -> Self { Self { socket_path, service_name, - command_runner: SetuidCommandRunner::new(user_name, command_runner), + command_runner: SetuidCommandRunner::new(user_name), } } } -impl, U: AsRef, R: CommandRunner> UserService<'_, S, U, R> { +impl, U: AsRef> UserService<'_, S, U> { async fn systemctl_wait_for_dbus(&self, args: &[&OsStr]) -> Result> { let mut tries = 5; loop { @@ -85,7 +80,7 @@ impl, U: AsRef, R: CommandRunner> UserService<'_, S, U, R> { } #[async_trait(?Send)] -impl, U: AsRef, R: CommandRunner> Symbol for UserService<'_, S, U, R> { +impl, U: AsRef> Symbol for UserService<'_, S, U> { async fn target_reached(&self) -> Result> { self.check_if_service().await } diff --git a/src/symbols/tls/key.rs b/src/symbols/tls/key.rs index 720d7ab..be2ea9f 100644 --- a/src/symbols/tls/key.rs +++ b/src/symbols/tls/key.rs @@ -8,7 +8,7 @@ use std::path::Path; pub struct Key { file_path: P, command_runner: C, - bytes: u32, + bits: u32, } impl Key { @@ -16,7 +16,7 @@ impl Key { Self { file_path, command_runner, - bytes: 4096, + bits: 4096, } } } @@ -42,8 +42,10 @@ impl> Symbol for Key { ], ) .await?; - // FIXME check bytes - Ok(stdout.ends_with(b"RSA key ok\n")) + Ok( + stdout.ends_with(b"RSA key ok\n") + && stdout.starts_with(format!("RSA Private-Key: ({} bit, 2 primes)\n", self.bits).as_ref()), + ) } async fn execute(&self) -> Result<(), Box> { @@ -55,7 +57,7 @@ impl> Symbol for Key { "genrsa", "-out", self.file_path.as_ref(), - self.bytes.to_string(), + self.bits.to_string(), ], ) .await diff --git a/src/symbols/wordpress/plugin.rs b/src/symbols/wordpress/plugin.rs index 8c280d8..6ac40e2 100644 --- a/src/symbols/wordpress/plugin.rs +++ b/src/symbols/wordpress/plugin.rs @@ -16,7 +16,7 @@ pub struct Plugin<'a, P, N, R> { } impl<'a, P: AsRef, N: AsRef, R: CommandRunner> Plugin<'a, P, N, R> { - pub fn new(base: P, name: N, command_runner: &'a R) -> Self { + pub const fn new(base: P, name: N, command_runner: &'a R) -> Self { Self { base, name, diff --git a/src/symbols/wordpress/translation.rs b/src/symbols/wordpress/translation.rs index 6643c9e..ac16ec3 100644 --- a/src/symbols/wordpress/translation.rs +++ b/src/symbols/wordpress/translation.rs @@ -19,7 +19,7 @@ pub struct Translation<'a, C, D, R> { } impl<'a, D, C: AsRef, R: CommandRunner> Translation<'a, C, D, R> { - pub fn new(path: D, version: &'a str, locale: C, command_runner: &'a R) -> Self { + pub const fn new(path: D, version: &'a str, locale: C, command_runner: &'a R) -> Self { Self { path, version, @@ -33,7 +33,7 @@ impl, D: AsRef, R: CommandRunner> Translation<'_, C, D, R> { fn get_pairs(&self) -> Vec<(String, PathBuf)> { let version_x = self .version - .trim_end_matches(|c: char| c.is_digit(10)) + .trim_end_matches(|c: char| c.is_ascii_digit()) .to_owned() + "x"; let locale = self.locale.as_ref(); @@ -51,7 +51,7 @@ impl, D: AsRef, R: CommandRunner> Translation<'_, C, D, R> { ] { for format in &["po", "mo"] { res.push(( - format!("https://translate.wordpress.org/projects/wp/{}/{}{}/default/export-translations?format={}", version_x, in_slug, path_locale, format), + format!("https://translate.wordpress.org/projects/wp/{version_x}/{in_slug}{path_locale}/default/export-translations?format={format}"), [self.path.as_ref(), format!("{}{}.{}", out_slug, self.locale.as_ref(), format).as_ref()].iter().collect() )); } diff --git a/src/templates/nginx/mod.rs b/src/templates/nginx/mod.rs index 1134378..aa2992d 100644 --- a/src/templates/nginx/mod.rs +++ b/src/templates/nginx/mod.rs @@ -11,3 +11,55 @@ pub fn acme_challenges_snippet>(path: P) -> String { path.as_ref().to_str().unwrap() ) } + +#[cfg(test)] +mod test { + use super::{server_config, uwsgi_snippet}; + + #[test] + fn test_uwsgi() { + assert_eq!( + server_config( + "testdomain", + "/certpath", + "/keypath", + uwsgi_snippet("/uwsgi.sock", "/static"), + "/challenges_snippet.conf" + ), + "server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name testdomain; + include \"/challenges_snippet.conf\"; + + ssl_certificate /certpath; + ssl_certificate_key /keypath; + add_header Strict-Transport-Security \"max-age=31536000\"; + + root /static; + + location / { + try_files $uri @proxy; + } + + location @proxy { + include uwsgi_params; + uwsgi_pass unix:/uwsgi.sock; + } +} + +# Redirect all HTTP links to the matching HTTPS page +server { + listen 80; + listen [::]:80; + server_name testdomain; + include \"/challenges_snippet.conf\"; + + location / { + return 301 https://$host$request_uri; + } +} +" + ); + } +} diff --git a/src/templates/nginx/server.rs b/src/templates/nginx/server.rs index 9f04bbd..dd28bc4 100644 --- a/src/templates/nginx/server.rs +++ b/src/templates/nginx/server.rs @@ -78,21 +78,26 @@ pub fn php_snippet, STATIC: AsRef>( pub fn redir_snippet(target: &str) -> String { format!( "location / {{ - return 301 $scheme://{}$request_uri; - }}", - target + return 301 $scheme://{target}$request_uri; + }}" ) } pub trait SocketSpec { - fn to_nginx(&self) -> String; + fn to_proxy_pass(&self) -> String; + fn to_uwsgi_pass(&self) -> String; } impl> SocketSpec for T { #[must_use] - fn to_nginx(&self) -> String { + fn to_proxy_pass(&self) -> String { format!("unix:{}:", self.as_ref().to_str().unwrap()) } + + #[must_use] + fn to_uwsgi_pass(&self) -> String { + format!("unix:{}", self.as_ref().to_str().unwrap()) + } } #[derive(Debug)] @@ -107,7 +112,12 @@ impl LocalTcpSocket { impl SocketSpec for LocalTcpSocket { #[must_use] - fn to_nginx(&self) -> String { + fn to_proxy_pass(&self) -> String { + format!("localhost:{}", self.0) + } + + #[must_use] + fn to_uwsgi_pass(&self) -> String { format!("localhost:{}", self.0) } } @@ -129,12 +139,33 @@ pub fn proxy_snippet>( proxy_redirect off; }}", static_path.as_ref().to_str().unwrap(), - socket_path.to_nginx() + socket_path.to_proxy_pass() ) } #[must_use] -pub fn static_snippet>(static_path: S) -> String { +pub fn uwsgi_snippet>( + socket_path: S, + static_path: STATIC, +) -> String { + format!( + "root {}; + + location / {{ + try_files $uri @proxy; + }} + + location @proxy {{ + include uwsgi_params; + uwsgi_pass {}; + }}", + static_path.as_ref().to_str().unwrap(), + socket_path.to_uwsgi_pass() + ) +} + +#[must_use] +pub fn static_snippet(static_path: impl AsRef) -> String { format!( "root {}; try_files $uri $uri/ $uri.html =404; @@ -144,7 +175,7 @@ pub fn static_snippet>(static_path: S) -> String { } #[must_use] -pub fn dokuwiki_snippet() -> String { +pub const fn dokuwiki_snippet() -> &'static str { " location ~ /(data/|conf/|bin/|inc/|install.php) { deny all; } @@ -156,11 +187,11 @@ pub fn dokuwiki_snippet() -> String { rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last; rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last; rewrite ^/(.*) /doku.php?id=$1&$args last; - }".into() + }" } #[must_use] -pub fn nextcloud_snippet() -> String { +pub const fn nextcloud_snippet() -> &'static str { " client_max_body_size 500M; @@ -210,5 +241,20 @@ pub fn nextcloud_snippet() -> String { access_log off; } " - .into() +} + +#[cfg(test)] +mod test { + use super::default_server; + #[test] + fn test_default_server() { + assert_eq!( + default_server("filename"), + r#"server { + listen 80 default_server; + listen [::]:80 default_server; + include "filename"; + }"# + ); + } } diff --git a/src/templates/php.rs b/src/templates/php.rs index 7bd7695..6e47e28 100644 --- a/src/templates/php.rs +++ b/src/templates/php.rs @@ -56,3 +56,25 @@ env[PATH] = /usr/local/bin:/usr/bin:/bin config ) } + +#[cfg(test)] +mod test { + use super::fpm_pool_config; + #[test] + fn test_fpm_pool_config() { + assert_eq!( + fpm_pool_config("user", "socket", &5.into()), + r"[user] + +user = user +group = www-data +listen = socket +listen.owner = www-data +pm = ondemand +catch_workers_output = yes +env[PATH] = /usr/local/bin:/usr/bin:/bin +pm.max_children = 5 +" + ); + } +} diff --git a/src/to_artifact.rs b/src/to_artifact.rs index e45cda9..e158f07 100644 --- a/src/to_artifact.rs +++ b/src/to_artifact.rs @@ -17,8 +17,6 @@ macro_rules! to_artifact { for_each_tuple!(to_artifact); impl ToArtifact for Option { - // FIXME: https://github.com/rust-lang/rust-clippy/issues/2843 - #![allow(clippy::use_self)] type Artifact = Option; } diff --git a/tests/file.rs b/tests/file.rs index b74c8d9..3043cdc 100644 --- a/tests/file.rs +++ b/tests/file.rs @@ -98,7 +98,7 @@ fn may_not_read_file() { #[test] fn may_not_create_file() { - let symbol = get_symbol(&Path::new("/proc/somefile")); + let symbol = get_symbol(Path::new("/proc/somefile")); run(async { // Could also return an error @@ -109,7 +109,7 @@ fn may_not_create_file() { #[test] fn directory_missing() { - let symbol = get_symbol(&Path::new("/nonexisting")); + let symbol = get_symbol(Path::new("/nonexisting")); run(async { // Could also return an error diff --git a/tests/setup.rs b/tests/setup.rs index 077c4fc..e30520f 100644 --- a/tests/setup.rs +++ b/tests/setup.rs @@ -1,100 +1,149 @@ use async_trait::async_trait; +use regex::Regex; use schematics::async_utils::{run, sleep}; -use schematics::loggers::{Logger, StoringLogger}; +use schematics::loggers::{Entry, StoringLogger}; use schematics::resources::{AcmeUser, Cert, Csr, GitCheckout}; use schematics::symbols::Symbol; use schematics::Setup; use schematics::SymbolRunner; -use std::cell::RefCell; +use slog::{info, Logger as SlogLogger}; use std::error::Error; use std::fmt::Debug; -use std::rc::Rc; use std::time::Duration; #[derive(Clone, Debug)] struct TestSymbolRunner { - count: Rc>, + run: bool, +} + +impl TestSymbolRunner { + fn new(run: bool) -> Self { + Self { run } + } } #[async_trait(?Send)] impl SymbolRunner for TestSymbolRunner { - async fn run_symbol( + async fn run_symbol( &self, _symbol: &S, - _logger: &L, + logger: &SlogLogger, _force: bool, ) -> Result> { - *self.count.borrow_mut() += 1; + info!(logger, "run_symbol"); sleep(Duration::from_millis(0)).await; - Ok(false) + Ok(self.run) } } +fn test( + count: usize, + body: fn(setup: Setup) -> (), +) -> Vec> { + let runner = TestSymbolRunner::new(false); + let logger = StoringLogger::new(); + { + let setup = Setup::new(runner, logger.clone()); + body(setup); + } + assert_eq!(logger.release(), vec![Entry(3, ".".repeat(count))]); + + let runner = TestSymbolRunner::new(true); + let logger = StoringLogger::new(); + { + let setup = Setup::new(runner, logger.clone()); + body(setup); + } + logger.release() +} + #[test] fn can_create_an_acme_user() { - let count = Rc::new(RefCell::new(0)); - let runner = TestSymbolRunner { - count: Rc::clone(&count), - }; - let setup = Setup::new(runner, StoringLogger::new()); - assert_eq!((run(setup.add(AcmeUser)).unwrap().0).0, "acme"); + let mut result = test(1, |setup| { + assert_eq!((run(setup.add(AcmeUser)).unwrap().0).0, "acme"); + }); + let entry = result + .pop() + .expect("log is empty but should contain one entry"); + assert_eq!(result.len(), 0, "log has more than one entry"); + assert_eq!(entry.0, 3, "log entry has wrong level"); + let re = + Regex::new(r"^resource: AcmeUser\n \w+ \d{1,2} \d{2}:\d{2}:\d{2}.\d{3} INFO run_symbol\n$") + .unwrap(); + assert!( + re.is_match(&entry.1), + "log output {} does not match {}", + entry.1, + re + ); } #[test] fn runs_only_once() { - run(async { - let count = Rc::new(RefCell::new(0)); - let runner = TestSymbolRunner { - count: Rc::clone(&count), - }; - let setup = Setup::new(runner, StoringLogger::new()); - assert_eq!( - (setup.add(Csr("somehost")).await.unwrap().0) - .as_ref() - .to_str() - .unwrap(), - "/etc/ssl/local_certs/somehost.csr", - ); - assert_eq!( - (setup.add(Csr("somehost")).await.unwrap().0) - .as_ref() - .to_str() - .unwrap(), - "/etc/ssl/local_certs/somehost.csr", - ); - assert_eq!(*count.borrow(), 2 + 5); // Key and CSR + 5 dirs + let mut result = test(2, |setup| { + run(async { + assert_eq!( + (setup.add(Csr("somehost")).await.unwrap().0) + .as_ref() + .to_str() + .unwrap(), + "/etc/ssl/local_certs/somehost.csr", + ); + assert_eq!( + (setup.add(Csr("somehost")).await.unwrap().0) + .as_ref() + .to_str() + .unwrap(), + "/etc/ssl/local_certs/somehost.csr", + ); + }); }); + let entry = result + .pop() + .expect("log is empty but should contain entries"); + assert_eq!(entry.0, 3, "log entry has wrong level"); + assert_eq!(entry.1, ".", "log entry has wrong content"); + let entry = result + .pop() + .expect("log is empty but should contain entries"); + assert_eq!(entry.0, 3, "log entry has wrong level"); + assert_eq!(entry.1.matches("run_symbol").count(), 7); // Key and CSR + 5 dirs + assert_eq!(result.len(), 0, "log has more than one entry"); } #[test] fn can_create_an_acme_cert() { - let count = Rc::new(RefCell::new(0)); - let runner = TestSymbolRunner { - count: Rc::clone(&count), - }; - let setup = Setup::new(runner, StoringLogger::new()); - assert_eq!( - (run(setup.add(Cert("somehost"))).unwrap().0) - .as_ref() - .to_str() - .unwrap(), - "/etc/ssl/local_certs/somehost.crt", - ); - assert_eq!(*count.borrow(), 19); + let mut result = test(1, |setup| { + assert_eq!( + (run(setup.add(Cert("somehost"))).unwrap().0) + .as_ref() + .to_str() + .unwrap(), + "/etc/ssl/local_certs/somehost.crt", + ); + }); + let entry = result + .pop() + .expect("log is empty but should contain one entry"); + assert_eq!(entry.0, 3, "log entry has wrong level"); + assert_eq!(entry.1.matches("run_symbol").count(), 19); + assert_eq!(result.len(), 0, "log has more than one entry"); } #[test] fn can_create_a_git_checkout() { - let count = Rc::new(RefCell::new(0)); - let runner = TestSymbolRunner { - count: Rc::clone(&count), - }; - let setup = Setup::new(runner, StoringLogger::new()); - run(setup.add(GitCheckout( - "/tmp/somepath".into(), - "/tmp/some_src_repo", - "master", - ))) - .unwrap(); - assert_eq!(*count.borrow(), 3); + let mut result = test(1, |setup| { + run(setup.add(GitCheckout( + "/tmp/somepath".into(), + "/tmp/some_src_repo", + "master", + ))) + .unwrap(); + }); + let entry = result + .pop() + .expect("log is empty but should contain one entry"); + assert_eq!(entry.0, 3, "log entry has wrong level"); + assert_eq!(entry.1.matches("run_symbol").count(), 3); + assert_eq!(result.len(), 0, "log has more than one entry"); }