use crate::{ constants::{SERVER_WATCHER_CAPACITY, SERVER_WATCHER_PAUSE_MILLIS}, data::{ self, Change, ChangeKind, ChangeKindSet, DirEntry, FileType, Metadata, PtySize, Request, RequestData, Response, ResponseData, RunningProcess, SystemInfo, }, server::distant::{ process::{Process, PtyProcess, SimpleProcess}, state::{ProcessState, State, WatcherPath}, }, }; use derive_more::{Display, Error, From}; use futures::future; use log::*; use notify::{Config as WatcherConfig, RecursiveMode, Watcher}; use std::{ env, future::Future, path::{Path, PathBuf}, pin::Pin, sync::Arc, time::{Duration, SystemTime}, }; use tokio::{ io::{self, AsyncWriteExt}, sync::{ mpsc::{self, error::TrySendError}, Mutex, }, }; use walkdir::WalkDir; type HState = Arc>; type ReplyRet = Pin + Send + 'static>>; #[derive(Debug, Display, Error, From)] pub enum ServerError { Io(io::Error), Notify(notify::Error), WalkDir(walkdir::Error), } impl From for ResponseData { fn from(x: ServerError) -> Self { match x { ServerError::Io(x) => Self::from(x), ServerError::Notify(x) => Self::from(x), ServerError::WalkDir(x) => Self::from(x), } } } type PostHook = Box; struct Outgoing { data: ResponseData, post_hook: Option, } impl From for Outgoing { fn from(data: ResponseData) -> Self { Self { data, post_hook: None, } } } /// Processes the provided request, sending replies using the given sender pub(super) async fn process( conn_id: usize, state: HState, req: Request, tx: mpsc::Sender, ) -> Result<(), mpsc::error::SendError> { async fn inner( conn_id: usize, state: HState, data: RequestData, reply: F, ) -> Result where F: FnMut(Vec) -> ReplyRet + Clone + Send + 'static, { match data { RequestData::FileRead { path } => file_read(path).await, RequestData::FileReadText { path } => file_read_text(path).await, RequestData::FileWrite { path, data } => file_write(path, data).await, RequestData::FileWriteText { path, text } => file_write(path, text).await, RequestData::FileAppend { path, data } => file_append(path, data).await, RequestData::FileAppendText { path, text } => file_append(path, text).await, RequestData::DirRead { path, depth, absolute, canonicalize, include_root, } => dir_read(path, depth, absolute, canonicalize, include_root).await, RequestData::DirCreate { path, all } => dir_create(path, all).await, RequestData::Remove { path, force } => remove(path, force).await, RequestData::Copy { src, dst } => copy(src, dst).await, RequestData::Rename { src, dst } => rename(src, dst).await, RequestData::Watch { path, recursive, only, except, } => watch(conn_id, state, reply, path, recursive, only, except).await, RequestData::Unwatch { path } => unwatch(conn_id, state, path).await, RequestData::Exists { path } => exists(path).await, RequestData::Metadata { path, canonicalize, resolve_file_type, } => metadata(path, canonicalize, resolve_file_type).await, RequestData::ProcSpawn { cmd, args, persist, pty, } => proc_spawn(conn_id, state, reply, cmd, args, persist, pty).await, RequestData::ProcKill { id } => proc_kill(conn_id, state, id).await, RequestData::ProcStdin { id, data } => proc_stdin(conn_id, state, id, data).await, RequestData::ProcResizePty { id, size } => { proc_resize_pty(conn_id, state, id, size).await } RequestData::ProcList {} => proc_list(state).await, RequestData::SystemInfo {} => system_info().await, } } let reply = { let origin_id = req.id; let tenant = req.tenant.clone(); let tx_2 = tx.clone(); move |payload: Vec| -> ReplyRet { let tx = tx_2.clone(); let res = Response::new(tenant.to_string(), origin_id, payload); Box::pin(async move { tx.send(res).await.is_ok() }) } }; // Build up a collection of tasks to run independently let mut payload_tasks = Vec::new(); for data in req.payload { let state_2 = Arc::clone(&state); let reply_2 = reply.clone(); payload_tasks.push(tokio::spawn(async move { match inner(conn_id, state_2, data, reply_2).await { Ok(outgoing) => outgoing, Err(x) => Outgoing::from(ResponseData::from(x)), } })); } // Collect the results of our tasks into the payload entries let mut outgoing: Vec = future::join_all(payload_tasks) .await .into_iter() .map(|x| match x { Ok(outgoing) => outgoing, Err(x) => Outgoing::from(ResponseData::from(x)), }) .collect(); let post_hooks: Vec = outgoing .iter_mut() .filter_map(|x| x.post_hook.take()) .collect(); let payload = outgoing.into_iter().map(|x| x.data).collect(); let res = Response::new(req.tenant, req.id, payload); // Send out our primary response from processing the request let result = tx.send(res).await; // Invoke all post hooks for hook in post_hooks { hook(); } result } async fn file_read(path: PathBuf) -> Result { Ok(Outgoing::from(ResponseData::Blob { data: tokio::fs::read(path).await?, })) } async fn file_read_text(path: PathBuf) -> Result { Ok(Outgoing::from(ResponseData::Text { data: tokio::fs::read_to_string(path).await?, })) } async fn file_write(path: PathBuf, data: impl AsRef<[u8]>) -> Result { tokio::fs::write(path, data).await?; Ok(Outgoing::from(ResponseData::Ok)) } async fn file_append(path: PathBuf, data: impl AsRef<[u8]>) -> Result { let mut file = tokio::fs::OpenOptions::new() .create(true) .append(true) .open(path) .await?; file.write_all(data.as_ref()).await?; Ok(Outgoing::from(ResponseData::Ok)) } async fn dir_read( path: PathBuf, depth: usize, absolute: bool, canonicalize: bool, include_root: bool, ) -> Result { // Canonicalize our provided path to ensure that it is exists, not a loop, and absolute let root_path = tokio::fs::canonicalize(path).await?; // Traverse, but don't include root directory in entries (hence min depth 1), unless indicated // to do so (min depth 0) let dir = WalkDir::new(root_path.as_path()) .min_depth(if include_root { 0 } else { 1 }) .sort_by_file_name(); // If depth > 0, will recursively traverse to specified max depth, otherwise // performs infinite traversal let dir = if depth > 0 { dir.max_depth(depth) } else { dir }; // Determine our entries and errors let mut entries = Vec::new(); let mut errors = Vec::new(); #[inline] fn map_file_type(ft: std::fs::FileType) -> FileType { if ft.is_dir() { FileType::Dir } else if ft.is_file() { FileType::File } else { FileType::Symlink } } for entry in dir { match entry.map_err(data::Error::from) { // For entries within the root, we want to transform the path based on flags Ok(e) if e.depth() > 0 => { // Canonicalize the path if specified, otherwise just return // the path as is let mut path = if canonicalize { match tokio::fs::canonicalize(e.path()).await { Ok(path) => path, Err(x) => { errors.push(data::Error::from(x)); continue; } } } else { e.path().to_path_buf() }; // Strip the path of its prefix based if not flagged as absolute if !absolute { // NOTE: In the situation where we canonicalized the path earlier, // there is no guarantee that our root path is still the // parent of the symlink's destination; so, in that case we MUST just // return the path if the strip_prefix fails path = path .strip_prefix(root_path.as_path()) .map(Path::to_path_buf) .unwrap_or(path); }; entries.push(DirEntry { path, file_type: map_file_type(e.file_type()), depth: e.depth(), }); } // For the root, we just want to echo back the entry as is Ok(e) => { entries.push(DirEntry { path: e.path().to_path_buf(), file_type: map_file_type(e.file_type()), depth: e.depth(), }); } Err(x) => errors.push(x), } } Ok(Outgoing::from(ResponseData::DirEntries { entries, errors })) } async fn dir_create(path: PathBuf, all: bool) -> Result { if all { tokio::fs::create_dir_all(path).await?; } else { tokio::fs::create_dir(path).await?; } Ok(Outgoing::from(ResponseData::Ok)) } async fn remove(path: PathBuf, force: bool) -> Result { let path_metadata = tokio::fs::metadata(path.as_path()).await?; if path_metadata.is_dir() { if force { tokio::fs::remove_dir_all(path).await?; } else { tokio::fs::remove_dir(path).await?; } } else { tokio::fs::remove_file(path).await?; } Ok(Outgoing::from(ResponseData::Ok)) } async fn copy(src: PathBuf, dst: PathBuf) -> Result { let src_metadata = tokio::fs::metadata(src.as_path()).await?; if src_metadata.is_dir() { // Create the destination directory first, regardless of if anything // is in the source directory tokio::fs::create_dir_all(dst.as_path()).await?; for entry in WalkDir::new(src.as_path()) .min_depth(1) .follow_links(false) .into_iter() .filter_entry(|e| { e.file_type().is_file() || e.file_type().is_dir() || e.path_is_symlink() }) { let entry = entry?; // Get unique portion of path relative to src // NOTE: Because we are traversing files that are all within src, this // should always succeed let local_src = entry.path().strip_prefix(src.as_path()).unwrap(); // Get the file without any directories let local_src_file_name = local_src.file_name().unwrap(); // Get the directory housing the file // NOTE: Because we enforce files/symlinks, there will always be a parent let local_src_dir = local_src.parent().unwrap(); // Map out the path to the destination let dst_parent_dir = dst.join(local_src_dir); // Create the destination directory for the file when copying tokio::fs::create_dir_all(dst_parent_dir.as_path()).await?; let dst_path = dst_parent_dir.join(local_src_file_name); // Perform copying from entry to destination (if a file/symlink) if !entry.file_type().is_dir() { tokio::fs::copy(entry.path(), dst_path).await?; // Otherwise, if a directory, create it } else { tokio::fs::create_dir(dst_path).await?; } } } else { tokio::fs::copy(src, dst).await?; } Ok(Outgoing::from(ResponseData::Ok)) } async fn rename(src: PathBuf, dst: PathBuf) -> Result { tokio::fs::rename(src, dst).await?; Ok(Outgoing::from(ResponseData::Ok)) } async fn watch( conn_id: usize, state: HState, reply: F, path: PathBuf, recursive: bool, only: Vec, except: Vec, ) -> Result where F: FnMut(Vec) -> ReplyRet + Clone + Send + 'static, { let only = only.into_iter().collect::(); let except = except.into_iter().collect::(); let state_2 = Arc::clone(&state); let mut state = state.lock().await; // NOTE: Do not use get_or_insert_with since notify::recommended_watcher returns a result // and we cannot unpack the result within the above function. Since we are locking // our state, we can be confident that no one else is modifying the watcher option // concurrently; so, we do a naive check for option being populated if state.watcher.is_none() { // NOTE: Cannot be something small like 1 as this seems to cause a deadlock sometimes // with a large volume of watch requests let (tx, mut rx) = mpsc::channel(SERVER_WATCHER_CAPACITY); let mut watcher = notify::recommended_watcher(move |res| { let mut res = res; // Attempt to send our result, breaking out of the loop // if we succeed or it is impossible, otherwise trying // again after a brief sleep loop { match tx.try_send(res) { Ok(_) => break, Err(TrySendError::Full(x)) => { warn!( "Reached watcher capacity of {}! Trying again after {}ms", SERVER_WATCHER_CAPACITY, SERVER_WATCHER_PAUSE_MILLIS ); res = x; std::thread::sleep(Duration::from_millis(SERVER_WATCHER_PAUSE_MILLIS)); } Err(TrySendError::Closed(_)) => { warn!("Skipping watch event because watcher channel closed"); break; } } } })?; // Attempt to configure watcher, but don't fail if these configurations fail match watcher.configure(WatcherConfig::PreciseEvents(true)) { Ok(true) => debug!(" Watcher configured for precise events", conn_id), Ok(false) => debug!( " Watcher not configured for precise events", conn_id, ), Err(x) => error!( " Watcher configuration for precise events failed: {}", conn_id, x ), } // Attempt to configure watcher, but don't fail if these configurations fail match watcher.configure(WatcherConfig::NoticeEvents(true)) { Ok(true) => debug!(" Watcher configured for notice events", conn_id), Ok(false) => debug!( " Watcher not configured for notice events", conn_id, ), Err(x) => error!( " Watcher configuration for notice events failed: {}", conn_id, x ), } let _ = state.watcher.insert(watcher); tokio::spawn(async move { while let Some(res) = rx.recv().await { let is_ok = match res { Ok(mut x) => { let mut state = state_2.lock().await; let paths: Vec<_> = x.paths.drain(..).collect(); let kind = ChangeKind::from(x.kind); trace!( " Watcher detected '{}' change for {:?}", conn_id, kind, paths ); fn make_res_data(kind: ChangeKind, paths: &[&PathBuf]) -> ResponseData { ResponseData::Changed(Change { kind, paths: paths.iter().map(|p| p.to_path_buf()).collect(), }) } let results = state.map_paths_to_watcher_paths_and_replies(&paths); let mut is_ok = true; for (paths, only, reply) in results { // Skip sending this change if we are not watching it if (!only.is_empty() && !only.contains(&kind)) || (!except.is_empty() && except.contains(&kind)) { trace!( " Skipping change '{}' for {:?}", conn_id, kind, paths ); continue; } if !reply(vec![make_res_data(kind, &paths)]).await { is_ok = false; break; } } is_ok } Err(mut x) => { let mut state = state_2.lock().await; let paths: Vec<_> = x.paths.drain(..).collect(); let msg = x.to_string(); error!( " Watcher encountered an error {} for {:?}", conn_id, msg, paths ); fn make_res_data(msg: &str, paths: &[&PathBuf]) -> ResponseData { if paths.is_empty() { ResponseData::Error(msg.into()) } else { ResponseData::Error(format!("{} about {:?}", msg, paths).into()) } } let mut is_ok = true; // If we have no paths for the errors, then we send the error to everyone if paths.is_empty() { trace!(" Relaying error to all watchers", conn_id); for reply in state.watcher_paths.values_mut() { if !reply(vec![make_res_data(&msg, &[])]).await { is_ok = false; break; } } // Otherwise, figure out the relevant watchers from our paths and // send the error to them } else { let results = state.map_paths_to_watcher_paths_and_replies(&paths); trace!( " Relaying error to {} watchers", conn_id, results.len() ); for (paths, _, reply) in results { if !reply(vec![make_res_data(&msg, &paths)]).await { is_ok = false; break; } } } is_ok } }; if !is_ok { error!(" Watcher channel closed", conn_id); break; } } }); } match state.watcher.as_mut() { Some(watcher) => { let wp = WatcherPath::new(&path, recursive, only)?; watcher.watch( path.as_path(), if recursive { RecursiveMode::Recursive } else { RecursiveMode::NonRecursive }, )?; debug!(" Now watching {:?}", conn_id, wp.path()); state.watcher_paths.insert(wp, Box::new(reply)); Ok(Outgoing::from(ResponseData::Ok)) } None => Err(ServerError::Io(io::Error::new( io::ErrorKind::BrokenPipe, format!(" Unable to initialize watcher", conn_id), ))), } } async fn unwatch(conn_id: usize, state: HState, path: PathBuf) -> Result { if let Some(watcher) = state.lock().await.watcher.as_mut() { watcher.unwatch(path.as_path())?; // TODO: This also needs to remove any path that matches in either raw form // or canonicalized form from the map of PathBuf -> ReplyFn return Ok(Outgoing::from(ResponseData::Ok)); } Err(ServerError::Io(io::Error::new( io::ErrorKind::BrokenPipe, format!( " Unable to unwatch as watcher not initialized", conn_id, ), ))) } async fn exists(path: PathBuf) -> Result { // Following experimental `std::fs::try_exists`, which checks the error kind of the // metadata lookup to see if it is not found and filters accordingly Ok(match tokio::fs::metadata(path.as_path()).await { Ok(_) => Outgoing::from(ResponseData::Exists { value: true }), Err(x) if x.kind() == io::ErrorKind::NotFound => { Outgoing::from(ResponseData::Exists { value: false }) } Err(x) => return Err(ServerError::from(x)), }) } async fn metadata( path: PathBuf, canonicalize: bool, resolve_file_type: bool, ) -> Result { let metadata = tokio::fs::symlink_metadata(path.as_path()).await?; let canonicalized_path = if canonicalize { Some(tokio::fs::canonicalize(path.as_path()).await?) } else { None }; // If asking for resolved file type and current type is symlink, then we want to refresh // our metadata to get the filetype for the resolved link let file_type = if resolve_file_type && metadata.file_type().is_symlink() { tokio::fs::metadata(path).await?.file_type() } else { metadata.file_type() }; Ok(Outgoing::from(ResponseData::Metadata(Metadata { canonicalized_path, accessed: metadata .accessed() .ok() .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) .map(|d| d.as_millis()), created: metadata .created() .ok() .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) .map(|d| d.as_millis()), modified: metadata .modified() .ok() .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok()) .map(|d| d.as_millis()), len: metadata.len(), readonly: metadata.permissions().readonly(), file_type: if file_type.is_dir() { FileType::Dir } else if file_type.is_file() { FileType::File } else { FileType::Symlink }, #[cfg(unix)] unix: Some({ use std::os::unix::prelude::*; let mode = metadata.mode(); crate::data::UnixMetadata::from(mode) }), #[cfg(not(unix))] unix: None, #[cfg(windows)] windows: Some({ use std::os::windows::prelude::*; let attributes = metadata.file_attributes(); crate::data::WindowsMetadata::from(attributes) }), #[cfg(not(windows))] windows: None, }))) } async fn proc_spawn( conn_id: usize, state: HState, reply: F, cmd: String, args: Vec, persist: bool, pty: Option, ) -> Result where F: FnMut(Vec) -> ReplyRet + Clone + Send + 'static, { debug!(" Spawning {} {}", conn_id, cmd, args.join(" ")); let mut child: Box = match pty { Some(size) => Box::new(PtyProcess::spawn(cmd.clone(), args.clone(), size)?), None => Box::new(SimpleProcess::spawn(cmd.clone(), args.clone())?), }; let id = child.id(); let stdin = child.take_stdin(); let stdout = child.take_stdout(); let stderr = child.take_stderr(); let killer = child.clone_killer(); let pty = child.clone_pty(); let state_2 = Arc::clone(&state); let post_hook = Box::new(move || { // Spawn a task that sends stdout as a response if let Some(mut stdout) = stdout { let mut reply_2 = reply.clone(); let _ = tokio::spawn(async move { loop { match stdout.recv().await { Ok(Some(data)) => { let payload = vec![ResponseData::ProcStdout { id, data }]; if !reply_2(payload).await { error!(" Stdout channel closed", conn_id, id); break; } } Ok(None) => break, Err(x) => { error!( " Reading stdout failed: {}", conn_id, id, x ); break; } } } }); } // Spawn a task that sends stderr as a response if let Some(mut stderr) = stderr { let mut reply_2 = reply.clone(); let _ = tokio::spawn(async move { loop { match stderr.recv().await { Ok(Some(data)) => { let payload = vec![ResponseData::ProcStderr { id, data }]; if !reply_2(payload).await { error!(" Stderr channel closed", conn_id, id); break; } } Ok(None) => break, Err(x) => { error!( " Reading stderr failed: {}", conn_id, id, x ); break; } } } }); } // Spawn a task that waits on the process to exit but can also // kill the process when triggered let mut reply_2 = reply.clone(); let _ = tokio::spawn(async move { let status = child.wait().await; debug!(" Completed {:?}", conn_id, id, status); state_2.lock().await.remove_process(conn_id, id); match status { Ok(status) => { let payload = vec![ResponseData::ProcDone { id, success: status.success, code: status.code, }]; if !reply_2(payload).await { error!(" Failed to send done", conn_id, id,); } } Err(x) => { let payload = vec![ResponseData::from(x)]; if !reply_2(payload).await { error!( " Failed to send error for waiting", conn_id, id, ); } } } }); }); state.lock().await.push_process_state( conn_id, ProcessState { cmd, args, persist, id, stdin, killer, pty, }, ); debug!( " Spawned successfully! Will enter post hook later", conn_id, id ); Ok(Outgoing { data: ResponseData::ProcSpawned { id }, post_hook: Some(post_hook), }) } async fn proc_kill(conn_id: usize, state: HState, id: usize) -> Result { if let Some(mut process) = state.lock().await.processes.remove(&id) { if process.killer.kill().await.is_ok() { return Ok(Outgoing::from(ResponseData::Ok)); } } Err(ServerError::Io(io::Error::new( io::ErrorKind::BrokenPipe, format!( " Unable to send kill signal to process", conn_id, id ), ))) } async fn proc_stdin( conn_id: usize, state: HState, id: usize, data: Vec, ) -> Result { if let Some(process) = state.lock().await.processes.get_mut(&id) { if let Some(stdin) = process.stdin.as_mut() { if stdin.send(&data).await.is_ok() { return Ok(Outgoing::from(ResponseData::Ok)); } } } Err(ServerError::Io(io::Error::new( io::ErrorKind::BrokenPipe, format!( " Unable to send stdin to process", conn_id, id, ), ))) } async fn proc_resize_pty( conn_id: usize, state: HState, id: usize, size: PtySize, ) -> Result { if let Some(process) = state.lock().await.processes.get(&id) { let _ = process.pty.resize_pty(size)?; return Ok(Outgoing::from(ResponseData::Ok)); } Err(ServerError::Io(io::Error::new( io::ErrorKind::BrokenPipe, format!( " Unable to resize pty to {:?}", conn_id, id, size, ), ))) } async fn proc_list(state: HState) -> Result { Ok(Outgoing::from(ResponseData::ProcEntries { entries: state .lock() .await .processes .values() .map(|p| RunningProcess { cmd: p.cmd.to_string(), args: p.args.clone(), persist: p.persist, // TODO: Support retrieving current pty size pty: None, id: p.id, }) .collect(), })) } async fn system_info() -> Result { Ok(Outgoing::from(ResponseData::SystemInfo(SystemInfo { family: env::consts::FAMILY.to_string(), os: env::consts::OS.to_string(), arch: env::consts::ARCH.to_string(), current_dir: env::current_dir().unwrap_or_default(), main_separator: std::path::MAIN_SEPARATOR, }))) } #[cfg(test)] mod tests { use super::*; use assert_fs::prelude::*; use once_cell::sync::Lazy; use predicates::prelude::*; use std::time::Duration; static TEMP_SCRIPT_DIR: Lazy = Lazy::new(|| assert_fs::TempDir::new().unwrap()); static SCRIPT_RUNNER: Lazy = Lazy::new(|| String::from("bash")); static ECHO_ARGS_TO_STDOUT_SH: Lazy = Lazy::new(|| { let script = TEMP_SCRIPT_DIR.child("echo_args_to_stdout.sh"); script .write_str(indoc::indoc!( r#" #/usr/bin/env bash printf "%s" "$*" "# )) .unwrap(); script }); static ECHO_ARGS_TO_STDERR_SH: Lazy = Lazy::new(|| { let script = TEMP_SCRIPT_DIR.child("echo_args_to_stderr.sh"); script .write_str(indoc::indoc!( r#" #/usr/bin/env bash printf "%s" "$*" 1>&2 "# )) .unwrap(); script }); static ECHO_STDIN_TO_STDOUT_SH: Lazy = Lazy::new(|| { let script = TEMP_SCRIPT_DIR.child("echo_stdin_to_stdout.sh"); script .write_str(indoc::indoc!( r#" #/usr/bin/env bash while IFS= read; do echo "$REPLY"; done "# )) .unwrap(); script }); static SLEEP_SH: Lazy = Lazy::new(|| { let script = TEMP_SCRIPT_DIR.child("sleep.sh"); script .write_str(indoc::indoc!( r#" #!/usr/bin/env bash sleep "$1" "# )) .unwrap(); script }); static DOES_NOT_EXIST_BIN: Lazy = Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin")); fn setup( buffer: usize, ) -> ( usize, Arc>, mpsc::Sender, mpsc::Receiver, ) { let (tx, rx) = mpsc::channel(buffer); ( rand::random(), Arc::new(Mutex::new(State::default())), tx, rx, ) } #[tokio::test] async fn file_read_should_send_error_if_fails_to_read_file() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let path = temp.child("missing-file").path().to_path_buf(); let req = Request::new("test-tenant", vec![RequestData::FileRead { path }]); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); } #[tokio::test] async fn file_read_should_send_blob_with_file_contents() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); file.write_str("some file contents").unwrap(); let req = Request::new( "test-tenant", vec![RequestData::FileRead { path: file.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::Blob { data } => assert_eq!(data, b"some file contents"), x => panic!("Unexpected response: {:?}", x), } } #[tokio::test] async fn file_read_text_should_send_error_if_fails_to_read_file() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let path = temp.child("missing-file").path().to_path_buf(); let req = Request::new("test-tenant", vec![RequestData::FileReadText { path }]); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); } #[tokio::test] async fn file_read_text_should_send_text_with_file_contents() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); file.write_str("some file contents").unwrap(); let req = Request::new( "test-tenant", vec![RequestData::FileReadText { path: file.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::Text { data } => assert_eq!(data, "some file contents"), x => panic!("Unexpected response: {:?}", x), } } #[tokio::test] async fn file_write_should_send_error_if_fails_to_write_file() { let (conn_id, state, tx, mut rx) = setup(1); // Create a temporary path and add to it to ensure that there are // extra components that don't exist to cause writing to fail let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("dir").child("test-file"); let req = Request::new( "test-tenant", vec![RequestData::FileWrite { path: file.path().to_path_buf(), data: b"some text".to_vec(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); // Also verify that we didn't actually create the file file.assert(predicate::path::missing()); } #[tokio::test] async fn file_write_should_send_ok_when_successful() { let (conn_id, state, tx, mut rx) = setup(1); // Path should point to a file that does not exist, but all // other components leading up to it do let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); let req = Request::new( "test-tenant", vec![RequestData::FileWrite { path: file.path().to_path_buf(), data: b"some text".to_vec(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Also verify that we actually did create the file // with the associated contents file.assert("some text"); } #[tokio::test] async fn file_write_text_should_send_error_if_fails_to_write_file() { let (conn_id, state, tx, mut rx) = setup(1); // Create a temporary path and add to it to ensure that there are // extra components that don't exist to cause writing to fail let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("dir").child("test-file"); let req = Request::new( "test-tenant", vec![RequestData::FileWriteText { path: file.path().to_path_buf(), text: String::from("some text"), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); // Also verify that we didn't actually create the file file.assert(predicate::path::missing()); } #[tokio::test] async fn file_write_text_should_send_ok_when_successful() { let (conn_id, state, tx, mut rx) = setup(1); // Path should point to a file that does not exist, but all // other components leading up to it do let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); let req = Request::new( "test-tenant", vec![RequestData::FileWriteText { path: file.path().to_path_buf(), text: String::from("some text"), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Also verify that we actually did create the file // with the associated contents file.assert("some text"); } #[tokio::test] async fn file_append_should_send_error_if_fails_to_create_file() { let (conn_id, state, tx, mut rx) = setup(1); // Create a temporary path and add to it to ensure that there are // extra components that don't exist to cause writing to fail let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("dir").child("test-file"); let req = Request::new( "test-tenant", vec![RequestData::FileAppend { path: file.path().to_path_buf(), data: b"some extra contents".to_vec(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); // Also verify that we didn't actually create the file file.assert(predicate::path::missing()); } #[tokio::test] async fn file_append_should_create_file_if_missing() { let (conn_id, state, tx, mut rx) = setup(1); // Don't create the file directly, but define path // where the file should be let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); let req = Request::new( "test-tenant", vec![RequestData::FileAppend { path: file.path().to_path_buf(), data: b"some extra contents".to_vec(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Yield to allow chance to finish appending to file tokio::time::sleep(Duration::from_millis(50)).await; // Also verify that we actually did create to the file file.assert("some extra contents"); } #[tokio::test] async fn file_append_should_send_ok_when_successful() { let (conn_id, state, tx, mut rx) = setup(1); // Create a temporary file and fill it with some contents let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); file.write_str("some file contents").unwrap(); let req = Request::new( "test-tenant", vec![RequestData::FileAppend { path: file.path().to_path_buf(), data: b"some extra contents".to_vec(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Yield to allow chance to finish appending to file tokio::time::sleep(Duration::from_millis(50)).await; // Also verify that we actually did append to the file file.assert("some file contentssome extra contents"); } #[tokio::test] async fn file_append_text_should_send_error_if_fails_to_create_file() { let (conn_id, state, tx, mut rx) = setup(1); // Create a temporary path and add to it to ensure that there are // extra components that don't exist to cause writing to fail let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("dir").child("test-file"); let req = Request::new( "test-tenant", vec![RequestData::FileAppendText { path: file.path().to_path_buf(), text: String::from("some extra contents"), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); // Also verify that we didn't actually create the file file.assert(predicate::path::missing()); } #[tokio::test] async fn file_append_text_should_create_file_if_missing() { let (conn_id, state, tx, mut rx) = setup(1); // Don't create the file directly, but define path // where the file should be let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); let req = Request::new( "test-tenant", vec![RequestData::FileAppendText { path: file.path().to_path_buf(), text: "some extra contents".to_string(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Yield to allow chance to finish appending to file tokio::time::sleep(Duration::from_millis(50)).await; // Also verify that we actually did create to the file file.assert("some extra contents"); } #[tokio::test] async fn file_append_text_should_send_ok_when_successful() { let (conn_id, state, tx, mut rx) = setup(1); // Create a temporary file and fill it with some contents let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("test-file"); file.write_str("some file contents").unwrap(); let req = Request::new( "test-tenant", vec![RequestData::FileAppendText { path: file.path().to_path_buf(), text: String::from("some extra contents"), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Yield to allow chance to finish appending to file tokio::time::sleep(Duration::from_millis(50)).await; // Also verify that we actually did append to the file file.assert("some file contentssome extra contents"); } #[tokio::test] async fn dir_read_should_send_error_if_directory_does_not_exist() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let dir = temp.child("test-dir"); let req = Request::new( "test-tenant", vec![RequestData::DirRead { path: dir.path().to_path_buf(), depth: 0, absolute: false, canonicalize: false, include_root: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); } // /root/ // /root/file1 // /root/link1 -> /root/sub1/file2 // /root/sub1/ // /root/sub1/file2 async fn setup_dir() -> assert_fs::TempDir { let root_dir = assert_fs::TempDir::new().unwrap(); root_dir.child("file1").touch().unwrap(); let sub1 = root_dir.child("sub1"); sub1.create_dir_all().unwrap(); let file2 = sub1.child("file2"); file2.touch().unwrap(); let link1 = root_dir.child("link1"); link1.symlink_to_file(file2.path()).unwrap(); root_dir } #[tokio::test] async fn dir_read_should_support_depth_limits() { let (conn_id, state, tx, mut rx) = setup(1); // Create directory with some nested items let root_dir = setup_dir().await; let req = Request::new( "test-tenant", vec![RequestData::DirRead { path: root_dir.path().to_path_buf(), depth: 1, absolute: false, canonicalize: false, include_root: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::DirEntries { entries, .. } => { assert_eq!(entries.len(), 3, "Wrong number of entries found"); assert_eq!(entries[0].file_type, FileType::File); assert_eq!(entries[0].path, Path::new("file1")); assert_eq!(entries[0].depth, 1); assert_eq!(entries[1].file_type, FileType::Symlink); assert_eq!(entries[1].path, Path::new("link1")); assert_eq!(entries[1].depth, 1); assert_eq!(entries[2].file_type, FileType::Dir); assert_eq!(entries[2].path, Path::new("sub1")); assert_eq!(entries[2].depth, 1); } x => panic!("Unexpected response: {:?}", x), } } #[tokio::test] async fn dir_read_should_support_unlimited_depth_using_zero() { let (conn_id, state, tx, mut rx) = setup(1); // Create directory with some nested items let root_dir = setup_dir().await; let req = Request::new( "test-tenant", vec![RequestData::DirRead { path: root_dir.path().to_path_buf(), depth: 0, absolute: false, canonicalize: false, include_root: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::DirEntries { entries, .. } => { assert_eq!(entries.len(), 4, "Wrong number of entries found"); assert_eq!(entries[0].file_type, FileType::File); assert_eq!(entries[0].path, Path::new("file1")); assert_eq!(entries[0].depth, 1); assert_eq!(entries[1].file_type, FileType::Symlink); assert_eq!(entries[1].path, Path::new("link1")); assert_eq!(entries[1].depth, 1); assert_eq!(entries[2].file_type, FileType::Dir); assert_eq!(entries[2].path, Path::new("sub1")); assert_eq!(entries[2].depth, 1); assert_eq!(entries[3].file_type, FileType::File); assert_eq!(entries[3].path, Path::new("sub1").join("file2")); assert_eq!(entries[3].depth, 2); } x => panic!("Unexpected response: {:?}", x), } } #[tokio::test] async fn dir_read_should_support_including_directory_in_returned_entries() { let (conn_id, state, tx, mut rx) = setup(1); // Create directory with some nested items let root_dir = setup_dir().await; let req = Request::new( "test-tenant", vec![RequestData::DirRead { path: root_dir.path().to_path_buf(), depth: 1, absolute: false, canonicalize: false, include_root: true, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::DirEntries { entries, .. } => { assert_eq!(entries.len(), 4, "Wrong number of entries found"); // NOTE: Root entry is always absolute, resolved path assert_eq!(entries[0].file_type, FileType::Dir); assert_eq!(entries[0].path, root_dir.path().canonicalize().unwrap()); assert_eq!(entries[0].depth, 0); assert_eq!(entries[1].file_type, FileType::File); assert_eq!(entries[1].path, Path::new("file1")); assert_eq!(entries[1].depth, 1); assert_eq!(entries[2].file_type, FileType::Symlink); assert_eq!(entries[2].path, Path::new("link1")); assert_eq!(entries[2].depth, 1); assert_eq!(entries[3].file_type, FileType::Dir); assert_eq!(entries[3].path, Path::new("sub1")); assert_eq!(entries[3].depth, 1); } x => panic!("Unexpected response: {:?}", x), } } #[tokio::test] async fn dir_read_should_support_returning_absolute_paths() { let (conn_id, state, tx, mut rx) = setup(1); // Create directory with some nested items let root_dir = setup_dir().await; let req = Request::new( "test-tenant", vec![RequestData::DirRead { path: root_dir.path().to_path_buf(), depth: 1, absolute: true, canonicalize: false, include_root: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::DirEntries { entries, .. } => { assert_eq!(entries.len(), 3, "Wrong number of entries found"); let root_path = root_dir.path().canonicalize().unwrap(); assert_eq!(entries[0].file_type, FileType::File); assert_eq!(entries[0].path, root_path.join("file1")); assert_eq!(entries[0].depth, 1); assert_eq!(entries[1].file_type, FileType::Symlink); assert_eq!(entries[1].path, root_path.join("link1")); assert_eq!(entries[1].depth, 1); assert_eq!(entries[2].file_type, FileType::Dir); assert_eq!(entries[2].path, root_path.join("sub1")); assert_eq!(entries[2].depth, 1); } x => panic!("Unexpected response: {:?}", x), } } #[tokio::test] async fn dir_read_should_support_returning_canonicalized_paths() { let (conn_id, state, tx, mut rx) = setup(1); // Create directory with some nested items let root_dir = setup_dir().await; let req = Request::new( "test-tenant", vec![RequestData::DirRead { path: root_dir.path().to_path_buf(), depth: 1, absolute: false, canonicalize: true, include_root: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::DirEntries { entries, .. } => { assert_eq!(entries.len(), 3, "Wrong number of entries found"); assert_eq!(entries[0].file_type, FileType::File); assert_eq!(entries[0].path, Path::new("file1")); assert_eq!(entries[0].depth, 1); // Symlink should be resolved from $ROOT/link1 -> $ROOT/sub1/file2 assert_eq!(entries[1].file_type, FileType::Symlink); assert_eq!(entries[1].path, Path::new("sub1").join("file2")); assert_eq!(entries[1].depth, 1); assert_eq!(entries[2].file_type, FileType::Dir); assert_eq!(entries[2].path, Path::new("sub1")); assert_eq!(entries[2].depth, 1); } x => panic!("Unexpected response: {:?}", x), } } #[tokio::test] async fn dir_create_should_send_error_if_fails() { let (conn_id, state, tx, mut rx) = setup(1); // Make a path that has multiple non-existent components // so the creation will fail let root_dir = setup_dir().await; let path = root_dir.path().join("nested").join("new-dir"); let req = Request::new( "test-tenant", vec![RequestData::DirCreate { path: path.to_path_buf(), all: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); // Also verify that the directory was not actually created assert!(!path.exists(), "Path unexpectedly exists"); } #[tokio::test] async fn dir_create_should_send_ok_when_successful() { let (conn_id, state, tx, mut rx) = setup(1); let root_dir = setup_dir().await; let path = root_dir.path().join("new-dir"); let req = Request::new( "test-tenant", vec![RequestData::DirCreate { path: path.to_path_buf(), all: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Also verify that the directory was actually created assert!(path.exists(), "Directory not created"); } #[tokio::test] async fn dir_create_should_support_creating_multiple_dir_components() { let (conn_id, state, tx, mut rx) = setup(1); let root_dir = setup_dir().await; let path = root_dir.path().join("nested").join("new-dir"); let req = Request::new( "test-tenant", vec![RequestData::DirCreate { path: path.to_path_buf(), all: true, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Also verify that the directory was actually created assert!(path.exists(), "Directory not created"); } #[tokio::test] async fn remove_should_send_error_on_failure() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("missing-file"); let req = Request::new( "test-tenant", vec![RequestData::Remove { path: file.path().to_path_buf(), force: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); // Also, verify that path does not exist file.assert(predicate::path::missing()); } #[tokio::test] async fn remove_should_support_deleting_a_directory() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let dir = temp.child("dir"); dir.create_dir_all().unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Remove { path: dir.path().to_path_buf(), force: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Also, verify that path does not exist dir.assert(predicate::path::missing()); } #[tokio::test] async fn remove_should_delete_nonempty_directory_if_force_is_true() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let dir = temp.child("dir"); dir.create_dir_all().unwrap(); dir.child("file").touch().unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Remove { path: dir.path().to_path_buf(), force: true, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Also, verify that path does not exist dir.assert(predicate::path::missing()); } #[tokio::test] async fn remove_should_support_deleting_a_single_file() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("some-file"); file.touch().unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Remove { path: file.path().to_path_buf(), force: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Also, verify that path does not exist file.assert(predicate::path::missing()); } #[tokio::test] async fn copy_should_send_error_on_failure() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); let dst = temp.child("dst"); let req = Request::new( "test-tenant", vec![RequestData::Copy { src: src.path().to_path_buf(), dst: dst.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); // Also, verify that destination does not exist dst.assert(predicate::path::missing()); } #[tokio::test] async fn copy_should_support_copying_an_entire_directory() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.create_dir_all().unwrap(); let src_file = src.child("file"); src_file.write_str("some contents").unwrap(); let dst = temp.child("dst"); let dst_file = dst.child("file"); let req = Request::new( "test-tenant", vec![RequestData::Copy { src: src.path().to_path_buf(), dst: dst.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Verify that we have source and destination directories and associated contents src.assert(predicate::path::is_dir()); src_file.assert(predicate::path::is_file()); dst.assert(predicate::path::is_dir()); dst_file.assert(predicate::path::eq_file(src_file.path())); } #[tokio::test] async fn copy_should_support_copying_an_empty_directory() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.create_dir_all().unwrap(); let dst = temp.child("dst"); let req = Request::new( "test-tenant", vec![RequestData::Copy { src: src.path().to_path_buf(), dst: dst.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Verify that we still have source and destination directories src.assert(predicate::path::is_dir()); dst.assert(predicate::path::is_dir()); } #[tokio::test] async fn copy_should_support_copying_a_directory_that_only_contains_directories() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.create_dir_all().unwrap(); let src_dir = src.child("dir"); src_dir.create_dir_all().unwrap(); let dst = temp.child("dst"); let dst_dir = dst.child("dir"); let req = Request::new( "test-tenant", vec![RequestData::Copy { src: src.path().to_path_buf(), dst: dst.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Verify that we have source and destination directories and associated contents src.assert(predicate::path::is_dir().name("src")); src_dir.assert(predicate::path::is_dir().name("src/dir")); dst.assert(predicate::path::is_dir().name("dst")); dst_dir.assert(predicate::path::is_dir().name("dst/dir")); } #[tokio::test] async fn copy_should_support_copying_a_single_file() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.write_str("some text").unwrap(); let dst = temp.child("dst"); let req = Request::new( "test-tenant", vec![RequestData::Copy { src: src.path().to_path_buf(), dst: dst.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Verify that we still have source and that destination has source's contents src.assert(predicate::path::is_file()); dst.assert(predicate::path::eq_file(src.path())); } #[tokio::test] async fn rename_should_send_error_on_failure() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); let dst = temp.child("dst"); let req = Request::new( "test-tenant", vec![RequestData::Rename { src: src.path().to_path_buf(), dst: dst.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); // Also, verify that destination does not exist dst.assert(predicate::path::missing()); } #[tokio::test] async fn rename_should_support_renaming_an_entire_directory() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.create_dir_all().unwrap(); let src_file = src.child("file"); src_file.write_str("some contents").unwrap(); let dst = temp.child("dst"); let dst_file = dst.child("file"); let req = Request::new( "test-tenant", vec![RequestData::Rename { src: src.path().to_path_buf(), dst: dst.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Verify that we moved the contents src.assert(predicate::path::missing()); src_file.assert(predicate::path::missing()); dst.assert(predicate::path::is_dir()); dst_file.assert("some contents"); } #[tokio::test] async fn rename_should_support_renaming_a_single_file() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let src = temp.child("src"); src.write_str("some text").unwrap(); let dst = temp.child("dst"); let req = Request::new( "test-tenant", vec![RequestData::Rename { src: src.path().to_path_buf(), dst: dst.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Verify that we moved the file src.assert(predicate::path::missing()); dst.assert("some text"); } /// Validates a response as being a series of changes that include the provided paths fn validate_changed_paths( res: &Response, expected_paths: &[PathBuf], should_panic: bool, ) -> bool { match &res.payload[0] { ResponseData::Changed(change) if should_panic => { let paths: Vec = change .paths .iter() .map(|x| x.canonicalize().unwrap()) .collect(); assert_eq!(paths, expected_paths, "Wrong paths reported: {:?}", change); true } ResponseData::Changed(change) => { let paths: Vec = change .paths .iter() .map(|x| x.canonicalize().unwrap()) .collect(); paths == expected_paths } x if should_panic => panic!("Unexpected response: {:?}", x), _ => false, } } #[tokio::test] async fn watch_should_support_watching_a_single_file() { // NOTE: Supporting multiple replies being sent back as part of creating, modifying, etc. let (conn_id, state, tx, mut rx) = setup(100); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.touch().unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Watch { path: file.path().to_path_buf(), recursive: false, only: Default::default(), except: Default::default(), }], ); // NOTE: We need to clone state so we don't drop the watcher // as part of dropping the state process(conn_id, Arc::clone(&state), req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Update the file and verify we get a notification file.write_str("some text").unwrap(); let res = rx .recv() .await .expect("Channel closed before we got change"); assert_eq!(res.payload.len(), 1, "Wrong payload size"); validate_changed_paths( &res, &[file.path().to_path_buf().canonicalize().unwrap()], /* should_panic */ true, ); } #[tokio::test] async fn watch_should_support_watching_a_directory_recursively() { // NOTE: Supporting multiple replies being sent back as part of creating, modifying, etc. let (conn_id, state, tx, mut rx) = setup(100); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.touch().unwrap(); let dir = temp.child("dir"); dir.create_dir_all().unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Watch { path: temp.path().to_path_buf(), recursive: true, only: Default::default(), except: Default::default(), }], ); // NOTE: We need to clone state so we don't drop the watcher // as part of dropping the state process(conn_id, Arc::clone(&state), req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); // Update the file and verify we get a notification file.write_str("some text").unwrap(); // Create a nested file and verify we get a notification let nested_file = dir.child("nested-file"); nested_file.write_str("some text").unwrap(); // Sleep a bit to give time to get all changes happening // TODO: Can we slim down this sleep? Or redesign test in some other way? tokio::time::sleep(Duration::from_millis(100)).await; // Collect all responses, as we may get multiple for interactions within a directory let mut responses = Vec::new(); while let Ok(res) = rx.try_recv() { responses.push(res); } // Validate that we have at least one change reported for each of our paths assert!( responses.len() >= 2, "Less than expected total responses: {:?}", responses ); let path = file.path().to_path_buf(); assert!( responses.iter().any(|res| validate_changed_paths( res, &[file.path().to_path_buf().canonicalize().unwrap()], /* should_panic */ false, )), "Missing {:?} in {:?}", path, responses .iter() .map(|x| format!("{:?}", x)) .collect::>(), ); let path = nested_file.path().to_path_buf(); assert!( responses.iter().any(|res| validate_changed_paths( res, &[file.path().to_path_buf().canonicalize().unwrap()], /* should_panic */ false, )), "Missing {:?} in {:?}", path, responses .iter() .map(|x| format!("{:?}", x)) .collect::>(), ); } #[tokio::test] async fn watch_should_report_changes_using_the_request_id() { // NOTE: Supporting multiple replies being sent back as part of creating, modifying, etc. let (conn_id, state, tx, mut rx) = setup(100); let temp = assert_fs::TempDir::new().unwrap(); let file_1 = temp.child("file_1"); file_1.touch().unwrap(); let file_2 = temp.child("file_2"); file_2.touch().unwrap(); // Sleep a bit to give time to get all changes happening // TODO: Can we slim down this sleep? Or redesign test in some other way? tokio::time::sleep(Duration::from_millis(100)).await; // Initialize watch on file 1 let file_1_origin_id = { let req = Request::new( "test-tenant", vec![RequestData::Watch { path: file_1.path().to_path_buf(), recursive: false, only: Default::default(), except: Default::default(), }], ); let origin_id = req.id; // NOTE: We need to clone state so we don't drop the watcher // as part of dropping the state process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); origin_id }; // Initialize watch on file 2 let file_2_origin_id = { let req = Request::new( "test-tenant", vec![RequestData::Watch { path: file_2.path().to_path_buf(), recursive: false, only: Default::default(), except: Default::default(), }], ); let origin_id = req.id; // NOTE: We need to clone state so we don't drop the watcher // as part of dropping the state process(conn_id, Arc::clone(&state), req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Ok), "Unexpected response: {:?}", res.payload[0] ); origin_id }; // Update the files and verify we get notifications from different origins { file_1.write_str("some text").unwrap(); let res = rx .recv() .await .expect("Channel closed before we got change"); assert_eq!(res.payload.len(), 1, "Wrong payload size"); validate_changed_paths( &res, &[file_1.path().to_path_buf().canonicalize().unwrap()], /* should_panic */ true, ); assert_eq!(res.origin_id, file_1_origin_id, "Wrong origin id (file 1)"); // Process any extra messages (we might get create, content, and more) loop { // Sleep a bit to give time to get all changes happening // TODO: Can we slim down this sleep? Or redesign test in some other way? tokio::time::sleep(Duration::from_millis(100)).await; if rx.try_recv().is_err() { break; } } } // Update the files and verify we get notifications from different origins { file_2.write_str("some text").unwrap(); let res = rx .recv() .await .expect("Channel closed before we got change"); assert_eq!(res.payload.len(), 1, "Wrong payload size"); validate_changed_paths( &res, &[file_2.path().to_path_buf().canonicalize().unwrap()], /* should_panic */ true, ); assert_eq!(res.origin_id, file_2_origin_id, "Wrong origin id (file 2)"); } } #[tokio::test] async fn exists_should_send_true_if_path_exists() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.touch().unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Exists { path: file.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert_eq!(res.payload[0], ResponseData::Exists { value: true }); } #[tokio::test] async fn exists_should_send_false_if_path_does_not_exist() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); let req = Request::new( "test-tenant", vec![RequestData::Exists { path: file.path().to_path_buf(), }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert_eq!(res.payload[0], ResponseData::Exists { value: false }); } #[tokio::test] async fn metadata_should_send_error_on_failure() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); let req = Request::new( "test-tenant", vec![RequestData::Metadata { path: file.path().to_path_buf(), canonicalize: false, resolve_file_type: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); } #[tokio::test] async fn metadata_should_send_back_metadata_on_file_if_exists() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Metadata { path: file.path().to_path_buf(), canonicalize: false, resolve_file_type: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!( res.payload[0], ResponseData::Metadata(Metadata { canonicalized_path: None, file_type: FileType::File, len: 9, readonly: false, .. }) ), "Unexpected response: {:?}", res.payload[0] ); } #[cfg(unix)] #[tokio::test] async fn metadata_should_include_unix_specific_metadata_on_unix_platform() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Metadata { path: file.path().to_path_buf(), canonicalize: false, resolve_file_type: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::Metadata(Metadata { unix, windows, .. }) => { assert!(unix.is_some(), "Unexpectedly missing unix metadata on unix"); assert!( windows.is_none(), "Unexpectedly got windows metadata on unix" ); } x => panic!("Unexpected response: {:?}", x), } } #[cfg(windows)] #[tokio::test] async fn metadata_should_include_unix_specific_metadata_on_windows_platform() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Metadata { path: file.path().to_path_buf(), canonicalize: false, resolve_file_type: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::Metadata(Metadata { unix, windows, .. }) => { assert!( windows.is_some(), "Unexpectedly missing windows metadata on windows" ); assert!(unix.is_none(), "Unexpectedly got unix metadata on windows"); } x => panic!("Unexpected response: {:?}", x), } } #[tokio::test] async fn metadata_should_send_back_metadata_on_dir_if_exists() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let dir = temp.child("dir"); dir.create_dir_all().unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Metadata { path: dir.path().to_path_buf(), canonicalize: false, resolve_file_type: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!( res.payload[0], ResponseData::Metadata(Metadata { canonicalized_path: None, file_type: FileType::Dir, readonly: false, .. }) ), "Unexpected response: {:?}", res.payload[0] ); } #[tokio::test] async fn metadata_should_send_back_metadata_on_symlink_if_exists() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let symlink = temp.child("link"); symlink.symlink_to_file(file.path()).unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Metadata { path: symlink.path().to_path_buf(), canonicalize: false, resolve_file_type: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!( res.payload[0], ResponseData::Metadata(Metadata { canonicalized_path: None, file_type: FileType::Symlink, readonly: false, .. }) ), "Unexpected response: {:?}", res.payload[0] ); } #[tokio::test] async fn metadata_should_include_canonicalized_path_if_flag_specified() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let symlink = temp.child("link"); symlink.symlink_to_file(file.path()).unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Metadata { path: symlink.path().to_path_buf(), canonicalize: true, resolve_file_type: false, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::Metadata(Metadata { canonicalized_path: Some(path), file_type: FileType::Symlink, readonly: false, .. }) => assert_eq!( path, &file.path().canonicalize().unwrap(), "Symlink canonicalized path does not match referenced file" ), x => panic!("Unexpected response: {:?}", x), } } #[tokio::test] async fn metadata_should_resolve_file_type_of_symlink_if_flag_specified() { let (conn_id, state, tx, mut rx) = setup(1); let temp = assert_fs::TempDir::new().unwrap(); let file = temp.child("file"); file.write_str("some text").unwrap(); let symlink = temp.child("link"); symlink.symlink_to_file(file.path()).unwrap(); let req = Request::new( "test-tenant", vec![RequestData::Metadata { path: symlink.path().to_path_buf(), canonicalize: false, resolve_file_type: true, }], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::Metadata(Metadata { file_type: FileType::File, .. }) => {} x => panic!("Unexpected response: {:?}", x), } } #[tokio::test] async fn proc_spawn_should_send_error_on_failure() { let (conn_id, state, tx, mut rx) = setup(1); let req = Request::new( "test-tenant", vec![RequestData::ProcSpawn { cmd: DOES_NOT_EXIST_BIN.to_str().unwrap().to_string(), args: Vec::new(), persist: false, pty: None, }], ); process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(&res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); } #[tokio::test] async fn proc_spawn_should_send_back_proc_start_on_success() { let (conn_id, state, tx, mut rx) = setup(1); let req = Request::new( "test-tenant", vec![RequestData::ProcSpawn { cmd: SCRIPT_RUNNER.to_string(), args: vec![ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string()], persist: false, pty: None, }], ); process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(&res.payload[0], ResponseData::ProcSpawned { .. }), "Unexpected response: {:?}", res.payload[0] ); } // NOTE: Ignoring on windows because it's using WSL which wants a Linux path // with / but thinks it's on windows and is providing \ #[tokio::test] #[cfg_attr(windows, ignore)] async fn proc_spawn_should_send_back_stdout_periodically_when_available() { let (conn_id, state, tx, mut rx) = setup(1); // Run a program that echoes to stdout let req = Request::new( "test-tenant", vec![RequestData::ProcSpawn { cmd: SCRIPT_RUNNER.to_string(), args: vec![ ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap().to_string(), String::from("some stdout"), ], persist: false, pty: None, }], ); process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(&res.payload[0], ResponseData::ProcSpawned { .. }), "Unexpected response: {:?}", res.payload[0] ); // Gather two additional responses: // // 1. An indirect response for stdout // 2. An indirect response that is proc completing // // Note that order is not a guarantee, so we have to check that // we get one of each type of response let res1 = rx.recv().await.expect("Missing first response"); let res2 = rx.recv().await.expect("Missing second response"); let mut got_stdout = false; let mut got_done = false; let mut check_res = |res: &Response| { assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::ProcStdout { data, .. } => { assert_eq!(data, b"some stdout", "Got wrong stdout"); got_stdout = true; } ResponseData::ProcDone { success, .. } => { assert!(success, "Process should have completed successfully"); got_done = true; } x => panic!("Unexpected response: {:?}", x), } }; check_res(&res1); check_res(&res2); assert!(got_stdout, "Missing stdout response"); assert!(got_done, "Missing done response"); } // NOTE: Ignoring on windows because it's using WSL which wants a Linux path // with / but thinks it's on windows and is providing \ #[tokio::test] #[cfg_attr(windows, ignore)] async fn proc_spawn_should_send_back_stderr_periodically_when_available() { let (conn_id, state, tx, mut rx) = setup(1); // Run a program that echoes to stderr let req = Request::new( "test-tenant", vec![RequestData::ProcSpawn { cmd: SCRIPT_RUNNER.to_string(), args: vec![ ECHO_ARGS_TO_STDERR_SH.to_str().unwrap().to_string(), String::from("some stderr"), ], persist: false, pty: None, }], ); process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert!( matches!(&res.payload[0], ResponseData::ProcSpawned { .. }), "Unexpected response: {:?}", res.payload[0] ); // Gather two additional responses: // // 1. An indirect response for stderr // 2. An indirect response that is proc completing // // Note that order is not a guarantee, so we have to check that // we get one of each type of response let res1 = rx.recv().await.expect("Missing first response"); let res2 = rx.recv().await.expect("Missing second response"); let mut got_stderr = false; let mut got_done = false; let mut check_res = |res: &Response| { assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::ProcStderr { data, .. } => { assert_eq!(data, b"some stderr", "Got wrong stderr"); got_stderr = true; } ResponseData::ProcDone { success, .. } => { assert!(success, "Process should have completed successfully"); got_done = true; } x => panic!("Unexpected response: {:?}", x), } }; check_res(&res1); check_res(&res2); assert!(got_stderr, "Missing stderr response"); assert!(got_done, "Missing done response"); } // NOTE: Ignoring on windows because it's using WSL which wants a Linux path // with / but thinks it's on windows and is providing \ #[tokio::test] #[cfg_attr(windows, ignore)] async fn proc_spawn_should_clear_process_from_state_when_done() { let (conn_id, state, tx, mut rx) = setup(1); // Run a program that ends after a little bit let req = Request::new( "test-tenant", vec![RequestData::ProcSpawn { cmd: SCRIPT_RUNNER.to_string(), args: vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("0.1")], persist: false, pty: None, }], ); process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); let id = match &res.payload[0] { ResponseData::ProcSpawned { id } => *id, x => panic!("Unexpected response: {:?}", x), }; // Verify that the state has the process assert!( state.lock().await.processes.contains_key(&id), "Process {} not in state", id ); // Wait for process to finish let _ = rx.recv().await.unwrap(); // Verify that the state was cleared assert!( !state.lock().await.processes.contains_key(&id), "Process {} still in state", id ); } #[tokio::test] async fn proc_spawn_should_clear_process_from_state_when_killed() { let (conn_id, state, tx, mut rx) = setup(1); // Run a program that ends slowly let req = Request::new( "test-tenant", vec![RequestData::ProcSpawn { cmd: SCRIPT_RUNNER.to_string(), args: vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("1")], persist: false, pty: None, }], ); process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); let id = match &res.payload[0] { ResponseData::ProcSpawned { id } => *id, x => panic!("Unexpected response: {:?}", x), }; // Verify that the state has the process assert!( state.lock().await.processes.contains_key(&id), "Process {} not in state", id ); // Send kill signal let req = Request::new("test-tenant", vec![RequestData::ProcKill { id }]); process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); // Wait for two responses, a kill confirmation and the done let _ = rx.recv().await.unwrap(); let _ = rx.recv().await.unwrap(); // Verify that the state was cleared assert!( !state.lock().await.processes.contains_key(&id), "Process {} still in state", id ); } #[tokio::test] async fn proc_kill_should_send_error_on_failure() { let (conn_id, state, tx, mut rx) = setup(1); // Send kill to a non-existent process let req = Request::new( "test-tenant", vec![RequestData::ProcKill { id: 0xDEADBEEF }], ); process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); // Verify that we get an error assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); } #[tokio::test] async fn proc_kill_should_send_ok_and_done_responses_on_success() { let (conn_id, state, tx, mut rx) = setup(1); // First, run a program that sits around (sleep for 1 second) let req = Request::new( "test-tenant", vec![RequestData::ProcSpawn { cmd: SCRIPT_RUNNER.to_string(), args: vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("1")], persist: false, pty: None, }], ); process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); // Second, grab the id of the started process let id = match &res.payload[0] { ResponseData::ProcSpawned { id } => *id, x => panic!("Unexpected response: {:?}", x), }; // Third, send kill for process let req = Request::new("test-tenant", vec![RequestData::ProcKill { id }]); // NOTE: We cannot let the state get dropped as it results in killing // the child process automatically; so, we clone another reference here process(conn_id, Arc::clone(&state), req, tx).await.unwrap(); // Fourth, gather two responses: // // 1. A direct response saying that received (ok) // 2. An indirect response that is proc completing // // Note that order is not a guarantee, so we have to check that // we get one of each type of response let res1 = rx.recv().await.expect("Missing first response"); let res2 = rx.recv().await.expect("Missing second response"); let mut got_ok = false; let mut got_done = false; let mut check_res = |res: &Response| { assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::Ok => got_ok = true, ResponseData::ProcDone { success, .. } => { assert!(!success, "Process should not have completed successfully"); got_done = true; } x => panic!("Unexpected response: {:?}", x), } }; check_res(&res1); check_res(&res2); assert!(got_ok, "Missing ok response"); assert!(got_done, "Missing done response"); } #[tokio::test] async fn proc_stdin_should_send_error_on_failure() { let (conn_id, state, tx, mut rx) = setup(1); // Send stdin to a non-existent process let req = Request::new( "test-tenant", vec![RequestData::ProcStdin { id: 0xDEADBEEF, data: b"some input".to_vec(), }], ); process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); // Verify that we get an error assert!( matches!(res.payload[0], ResponseData::Error(_)), "Unexpected response: {:?}", res.payload[0] ); } // NOTE: Ignoring on windows because it's using WSL which wants a Linux path // with / but thinks it's on windows and is providing \ #[tokio::test] #[cfg_attr(windows, ignore)] async fn proc_stdin_should_send_ok_on_success_and_properly_send_stdin_to_process() { let (conn_id, state, tx, mut rx) = setup(1); // First, run a program that listens for stdin let req = Request::new( "test-tenant", vec![RequestData::ProcSpawn { cmd: SCRIPT_RUNNER.to_string(), args: vec![ECHO_STDIN_TO_STDOUT_SH.to_str().unwrap().to_string()], persist: false, pty: None, }], ); process(conn_id, Arc::clone(&state), req, tx.clone()) .await .unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); // Second, grab the id of the started process let id = match &res.payload[0] { ResponseData::ProcSpawned { id } => *id, x => panic!("Unexpected response: {:?}", x), }; // Third, send stdin to the remote process let req = Request::new( "test-tenant", vec![RequestData::ProcStdin { id, data: b"hello world\n".to_vec(), }], ); // NOTE: We cannot let the state get dropped as it results in killing // the child process; so, we clone another reference here process(conn_id, Arc::clone(&state), req, tx).await.unwrap(); // Fourth, gather two responses: // // 1. A direct response to processing the stdin // 2. An indirect response that is stdout from echoing our stdin // // Note that order is not a guarantee, so we have to check that // we get one of each type of response let res1 = rx.recv().await.expect("Missing first response"); let res2 = rx.recv().await.expect("Missing second response"); let mut got_ok = false; let mut got_stdout = false; let mut check_res = |res: &Response| { assert_eq!(res.payload.len(), 1, "Wrong payload size"); match &res.payload[0] { ResponseData::Ok => got_ok = true, ResponseData::ProcStdout { data, .. } => { assert_eq!(data, b"hello world\n", "Mirrored data didn't match"); got_stdout = true; } x => panic!("Unexpected response: {:?}", x), } }; check_res(&res1); check_res(&res2); assert!(got_ok, "Missing ok response"); assert!(got_stdout, "Missing mirrored stdin response"); } #[tokio::test] async fn proc_list_should_send_proc_entry_list() { let (conn_id, state, tx, mut rx) = setup(1); // Run a process and get the list that includes that process // at the same time (using sleep of 1 second) let req = Request::new( "test-tenant", vec![ RequestData::ProcSpawn { cmd: SCRIPT_RUNNER.to_string(), args: vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("1")], persist: false, pty: None, }, RequestData::ProcList {}, ], ); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 2, "Wrong payload size"); // Grab the id of the started process let id = match &res.payload[0] { ResponseData::ProcSpawned { id } => *id, x => panic!("Unexpected response: {:?}", x), }; // Verify our process shows up in our entry list assert_eq!( res.payload[1], ResponseData::ProcEntries { entries: vec![RunningProcess { cmd: SCRIPT_RUNNER.to_string(), args: vec![SLEEP_SH.to_str().unwrap().to_string(), String::from("1")], persist: false, pty: None, id, }], }, "Unexpected response: {:?}", res.payload[0] ); } #[tokio::test] async fn system_info_should_send_system_info_based_on_binary() { let (conn_id, state, tx, mut rx) = setup(1); let req = Request::new("test-tenant", vec![RequestData::SystemInfo {}]); process(conn_id, state, req, tx).await.unwrap(); let res = rx.recv().await.unwrap(); assert_eq!(res.payload.len(), 1, "Wrong payload size"); assert_eq!( res.payload[0], ResponseData::SystemInfo(SystemInfo { family: env::consts::FAMILY.to_string(), os: env::consts::OS.to_string(), arch: env::consts::ARCH.to_string(), current_dir: env::current_dir().unwrap_or_default(), main_separator: std::path::MAIN_SEPARATOR, }), "Unexpected response: {:?}", res.payload[0] ); } }