You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
12 KiB
406 lines
12 KiB
use crate::{ |
|
combined_result::CombinedResult, |
|
executor::{ExecResult, Waiter}, |
|
misc::OneOrVec, |
|
models::{Agent, AssignedJob, AssignedJobById, FatJobMeta, JobType, Payload, ThinJobMeta}, |
|
proc_output::ProcOutput, |
|
ufs, |
|
}; |
|
use std::collections::HashMap; |
|
use std::process::exit; |
|
use tokio::process::Command; |
|
|
|
pub struct UnnamedJobsBatch { |
|
waiter: Waiter, |
|
is_running: bool, |
|
} |
|
|
|
impl UnnamedJobsBatch { |
|
pub fn from_meta_with_id(jobs: impl OneOrVec<(ThinJobMeta, AssignedJobById)>) -> Self { |
|
let jobs = jobs.into_vec(); |
|
let mut waiter = Waiter::new(); |
|
for (meta, job) in jobs { |
|
waiter.push(run_assigned_job(meta, job)); |
|
} |
|
Self { |
|
waiter, |
|
is_running: false, |
|
} |
|
} |
|
|
|
pub fn from_meta(metas: impl OneOrVec<ThinJobMeta>) -> Self { |
|
let jobs: Vec<_> = metas |
|
.into_vec() |
|
.into_iter() |
|
.map(|meta| { |
|
let job_id = meta.id; |
|
( |
|
meta, |
|
AssignedJobById { |
|
job_id, |
|
..Default::default() |
|
}, |
|
) |
|
}) |
|
.collect(); |
|
UnnamedJobsBatch::from_meta_with_id(jobs) |
|
} |
|
|
|
/// Spawn jobs |
|
pub async fn spawn(mut self) -> Self { |
|
self.waiter = self.waiter.spawn().await; |
|
self.is_running = true; |
|
self |
|
} |
|
|
|
/// Spawn jobs and wait for result |
|
pub async fn wait(self) -> Vec<ExecResult> { |
|
let waiter = if !self.is_running { |
|
self.spawn().await.waiter |
|
} else { |
|
self.waiter |
|
}; |
|
waiter.wait().await |
|
} |
|
|
|
/// Spawn one job and wait for result |
|
pub async fn wait_one(self) -> ExecResult { |
|
self.wait().await.pop().unwrap() |
|
} |
|
} |
|
|
|
pub async fn run_assigned_job(meta: ThinJobMeta, ids: AssignedJobById) -> ExecResult { |
|
let mut job = AssignedJob::from((&meta, ids)); |
|
match meta.exec_type { |
|
JobType::Shell => { |
|
let (argv, _prepared_payload) = { |
|
if let Some(ref payload) = meta.payload { |
|
let (prep_exec, prep_exec_path) = ufs::prepare_executable(payload)?; |
|
let argv_with_exec = meta.argv.replace("{}", &prep_exec_path); |
|
(argv_with_exec, Some(prep_exec)) |
|
} else { |
|
(meta.argv.clone(), None) |
|
} |
|
}; |
|
|
|
let mut split_cmd = shlex::split(&argv).unwrap().into_iter(); |
|
let cmd = split_cmd.nth(0).unwrap(); |
|
let args = split_cmd.collect::<Vec<String>>(); |
|
let cmd_result = Command::new(cmd).args(args).output().await; |
|
let (data, retcode) = match cmd_result { |
|
Ok(output) => ( |
|
ProcOutput::from_output(&output).into_vec(), |
|
output.status.code(), |
|
), |
|
Err(e) => ( |
|
ProcOutput::new() |
|
.set_stderr(e.to_string().into_bytes()) |
|
.into_vec(), |
|
None, |
|
), |
|
}; |
|
job.result = Some(data); |
|
job.retcode = retcode; |
|
} |
|
JobType::Init => { |
|
job.set_result(&Agent::run().await); |
|
job.retcode = Some(0); |
|
} |
|
JobType::Service => todo!(), |
|
JobType::Update => todo!(), |
|
JobType::Terminate => exit(0), |
|
}; |
|
Ok(job) |
|
} |
|
|
|
pub fn fat_meta_to_thin(meta: FatJobMeta<true>) -> Result<ThinJobMeta, ufs::Error> { |
|
let payload_ident = if let Some(mut payload) = meta.payload { |
|
let job_name = match &meta.alias { |
|
Some(a) => a.to_string(), |
|
None => meta.id.simple().to_string(), |
|
}; |
|
payload.write_self_into(&job_name)?; |
|
Some(job_name) |
|
} else { |
|
None |
|
}; |
|
|
|
Ok(ThinJobMeta { |
|
alias: meta.alias, |
|
argv: meta.argv, |
|
id: meta.id, |
|
exec_type: meta.exec_type, |
|
platform: meta.platform, |
|
payload: payload_ident, |
|
schedule: meta.schedule, |
|
}) |
|
} |
|
|
|
pub fn thin_meta_to_fat(meta: ThinJobMeta) -> Result<FatJobMeta<true>, ufs::Error> { |
|
let payload = if let Some(payload) = meta.payload { |
|
let mut fat_payload = Payload::Ident(payload); |
|
fat_payload.read_into_self()?; |
|
Some(fat_payload) |
|
} else { |
|
None |
|
}; |
|
|
|
Ok(FatJobMeta { |
|
alias: meta.alias, |
|
argv: meta.argv, |
|
id: meta.id, |
|
exec_type: meta.exec_type, |
|
platform: meta.platform, |
|
payload, |
|
schedule: meta.schedule, |
|
}) |
|
} |
|
|
|
/// Store jobs and get results by name |
|
pub struct NamedJobsBatch<const FINISHED: bool = false> { |
|
runner: Option<UnnamedJobsBatch>, |
|
job_names: Vec<String>, |
|
results: HashMap<String, ExecResult>, |
|
} |
|
|
|
impl NamedJobsBatch { |
|
pub fn from_shell( |
|
named_jobs: impl OneOrVec<(&'static str, &'static str)>, |
|
) -> CombinedResult<Self> { |
|
let mut result = CombinedResult::new(); |
|
let jobs: Vec<_> = named_jobs |
|
.into_vec() |
|
.into_iter() |
|
.filter_map(|(alias, cmd)| { |
|
match FatJobMeta::builder() |
|
.with_shell(cmd) |
|
.with_alias(alias) |
|
.build() |
|
{ |
|
Ok(fat_meta) => match fat_meta_to_thin(fat_meta) { |
|
Ok(thin_meta) => Some(thin_meta), |
|
Err(e) => { |
|
result.err(e); |
|
None |
|
} |
|
}, |
|
Err(e) => { |
|
result.err(e); |
|
None |
|
} |
|
} |
|
}) |
|
.collect(); |
|
result.ok(Self::from_meta(jobs)); |
|
result |
|
} |
|
|
|
pub fn from_meta(named_jobs: impl OneOrVec<ThinJobMeta>) -> Self { |
|
let (job_names, job_metas): (Vec<_>, Vec<_>) = named_jobs |
|
.into_vec() |
|
.into_iter() |
|
.map(|meta| (meta.alias.clone().unwrap(), meta)) |
|
.unzip(); |
|
Self { |
|
runner: Some(UnnamedJobsBatch::from_meta(job_metas)), |
|
job_names, |
|
results: HashMap::new(), |
|
} |
|
} |
|
|
|
pub async fn wait(mut self) -> NamedJobsBatch<true> { |
|
let results = self.runner.take().unwrap().wait().await; |
|
for (name, result) in self.job_names.into_iter().zip(results.into_iter()) { |
|
self.results.insert(name, result); |
|
} |
|
|
|
NamedJobsBatch::<true> { |
|
runner: None, |
|
job_names: vec![], |
|
results: self.results, |
|
} |
|
} |
|
} |
|
|
|
impl NamedJobsBatch<true> { |
|
pub fn pop_opt(&mut self, name: &'static str) -> Option<ExecResult> { |
|
self.results.remove(name) |
|
} |
|
|
|
pub fn pop(&mut self, name: &'static str) -> ExecResult { |
|
self.pop_opt(name).unwrap() |
|
} |
|
} |
|
|
|
#[cfg(test)] |
|
mod tests { |
|
use super::*; |
|
use crate::{ |
|
jobs::{NamedJobsBatch, UnnamedJobsBatch}, |
|
models::{misc::JobType, FatJobMeta}, |
|
unwrap_enum, UError, |
|
}; |
|
use std::time::SystemTime; |
|
|
|
type TestResult<R = ()> = Result<R, Box<dyn std::error::Error>>; |
|
|
|
#[tokio::test] |
|
async fn test_is_really_async() { |
|
const SLEEP_SECS: u64 = 1; |
|
let job = FatJobMeta::from_shell(format!("sleep {SLEEP_SECS}")).unwrap(); |
|
let sleep_jobs = (0..50) |
|
.map(|_| fat_meta_to_thin(job.clone()).unwrap()) |
|
.collect::<Vec<_>>(); |
|
|
|
let now = SystemTime::now(); |
|
UnnamedJobsBatch::from_meta(sleep_jobs).wait().await; |
|
assert!(now.elapsed().unwrap().as_secs() < SLEEP_SECS + 2) |
|
} |
|
|
|
#[rstest] |
|
#[case::sh_payload( |
|
"/bin/sh {}", |
|
Some(b"echo test01 > /tmp/asd; cat /tmp/asd".as_slice()), |
|
"test01" |
|
)] |
|
#[case::python_cmd(r#"/usr/bin/python3 -c 'print("test02")'"#, None, "test02")] |
|
#[case::sh_multiline_payload( |
|
"/{}", |
|
Some( |
|
br#"#!/bin/sh |
|
TMPPATH=/tmp/lol |
|
mkdir -p $TMPPATH |
|
echo test03 > $TMPPATH/t |
|
cat $TMPPATH/t |
|
rm -rf $TMPPATH"#.as_slice() |
|
), |
|
"test03" |
|
)] |
|
#[case::standalone_binary_with_args( |
|
"/{} 'some msg as arg'", |
|
Some(include_bytes!("../tests/fixtures/echoer").as_slice()), |
|
"some msg as arg" |
|
)] |
|
#[tokio::test] |
|
async fn test_shell_job( |
|
#[case] cmd: &str, |
|
#[case] payload: Option<&[u8]>, |
|
#[case] expected_result: &str, |
|
) -> TestResult { |
|
let mut job = FatJobMeta::builder().with_shell(cmd); |
|
if let Some(p) = payload { |
|
job = job.with_payload(p); |
|
} |
|
let job = fat_meta_to_thin(job.build().unwrap()).unwrap(); |
|
let result = UnnamedJobsBatch::from_meta(job).wait_one().await.unwrap(); |
|
let result = result.to_str_result(); |
|
assert_eq!(result.trim(), expected_result); |
|
Ok(()) |
|
} |
|
|
|
#[tokio::test] |
|
async fn test_complex_load() -> TestResult { |
|
const SLEEP_SECS: u64 = 1; |
|
let now = SystemTime::now(); |
|
let longest_job = FatJobMeta::from_shell(format!("sleep {}", SLEEP_SECS)).unwrap(); |
|
let longest_job = UnnamedJobsBatch::from_meta(fat_meta_to_thin(longest_job).unwrap()) |
|
.spawn() |
|
.await; |
|
let ls = UnnamedJobsBatch::from_meta( |
|
fat_meta_to_thin(FatJobMeta::from_shell("ls").unwrap()).unwrap(), |
|
) |
|
.wait_one() |
|
.await |
|
.unwrap(); |
|
|
|
assert_eq!(ls.retcode.unwrap(), 0); |
|
|
|
let folders = ls.to_str_result(); |
|
let subfolders_jobs = folders |
|
.lines() |
|
.map(|f| fat_meta_to_thin(FatJobMeta::from_shell(format!("ls {f}")).unwrap()).unwrap()) |
|
.collect::<Vec<_>>(); |
|
|
|
let ls_subfolders = UnnamedJobsBatch::from_meta(subfolders_jobs).wait().await; |
|
|
|
for result in ls_subfolders { |
|
assert_eq!(result.unwrap().retcode.unwrap(), 0); |
|
} |
|
|
|
longest_job.wait().await; |
|
assert_eq!(now.elapsed().unwrap().as_secs(), SLEEP_SECS); |
|
Ok(()) |
|
} |
|
/* |
|
#[tokio::test] |
|
async fn test_exec_multiple_jobs_nowait() -> UResult<()> { |
|
const REPEATS: usize = 10; |
|
let job = JobMeta::from_shell("whoami"); |
|
let sleep_jobs: Vec<JobMeta> = (0..=REPEATS).map(|_| job.clone()).collect(); |
|
build_jobs(sleep_jobs).spawn().await; |
|
let mut completed = 0; |
|
while completed < REPEATS { |
|
let c = pop_completed().await.len(); |
|
if c > 0 { |
|
completed += c; |
|
println!("{}", c); |
|
} |
|
} |
|
Ok(()) |
|
} |
|
*/ |
|
#[tokio::test] |
|
async fn test_failing_shell_job() -> TestResult { |
|
let job = fat_meta_to_thin(FatJobMeta::from_shell("lol_kek_puk").unwrap()).unwrap(); |
|
let job_result = UnnamedJobsBatch::from_meta(job).wait_one().await.unwrap(); |
|
let output = job_result.to_str_result(); |
|
assert!(output.contains("No such file")); |
|
assert!(job_result.retcode.is_none()); |
|
Ok(()) |
|
} |
|
|
|
#[rstest] |
|
#[case::no_binary("/bin/bash {}", None, "contains executable")] |
|
#[case::no_path_to_binary("/bin/bash", Some(b"whoami".as_slice()), "contains no executable")] |
|
#[tokio::test] |
|
async fn test_job_building_failed( |
|
#[case] cmd: &str, |
|
#[case] payload: Option<&[u8]>, |
|
#[case] err_str: &str, |
|
) -> TestResult { |
|
let mut job = FatJobMeta::builder().with_shell(cmd); |
|
if let Some(p) = payload { |
|
job = job.with_payload(p); |
|
} |
|
let err = job.build().unwrap_err(); |
|
let err_msg = unwrap_enum!(err, UError::JobBuildError); |
|
assert!(err_msg.contains(err_str)); |
|
Ok(()) |
|
} |
|
|
|
#[tokio::test] |
|
async fn test_different_job_types() -> TestResult { |
|
let mut jobs = NamedJobsBatch::from_meta( |
|
[ |
|
FatJobMeta::builder() |
|
.with_shell("sleep 3") |
|
.with_alias("sleeper") |
|
.build() |
|
.unwrap(), |
|
FatJobMeta::builder() |
|
.with_type(JobType::Init) |
|
.with_alias("gatherer") |
|
.build() |
|
.unwrap(), |
|
] |
|
.into_iter() |
|
.map(|meta| fat_meta_to_thin(meta).unwrap()) |
|
.collect::<Vec<_>>(), |
|
) |
|
.wait() |
|
.await; |
|
let gathered = jobs.pop("gatherer").unwrap(); |
|
assert_eq!(gathered.alias.unwrap(), "gatherer"); |
|
Ok(()) |
|
} |
|
}
|
|
|