use std::collections::HashMap;
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::io::Write;
use std::net::IpAddr;
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::Arc;
use nix::unistd::{setpgid, Pid};
use tempdir::TempDir;
use users;
use common::prelude::*;
use common::state::UniqueId;
use scripts::Script;
use requests::Request;
use providers::Provider;
static DEFAULT_ENV: &[&'static str] = &[
"PATH", "LC_ALL", "LANG",
];
static ENV_PREFIX: &'static str = "FISHER";
#[derive(Debug)]
pub struct Context {
pub environment: HashMap<String, String>,
pub username: String,
}
impl Default for Context {
fn default() -> Self {
let username = if let Some(name) = users::get_current_username()
.and_then(|n| n.to_str().map(|s| s.to_string()))
{
name
} else {
users::get_current_uid().to_string()
};
Context {
environment: HashMap::new(),
username,
}
}
}
struct EnvBuilderReal<'job> {
command: &'job mut Command,
data_dir: &'job Path,
last_file: Option<fs::File>,
}
#[cfg(test)]
pub struct EnvBuilderDummy {
pub env: HashMap<String, String>,
pub files: HashMap<String, Vec<u8>>,
}
enum EnvBuilderInner<'job> {
Real(EnvBuilderReal<'job>),
#[cfg(test)]
Dummy(EnvBuilderDummy),
}
pub struct EnvBuilder<'job> {
inner: EnvBuilderInner<'job>,
prefix: Option<OsString>,
}
impl<'job> EnvBuilder<'job> {
fn new(command: &'job mut Command, data_dir: &'job Path) -> Self {
EnvBuilder {
inner: EnvBuilderInner::Real(EnvBuilderReal {
command,
data_dir,
last_file: None,
}),
prefix: Some(ENV_PREFIX.into()),
}
}
#[cfg(test)]
pub fn dummy() -> Self {
EnvBuilder {
inner: EnvBuilderInner::Dummy(EnvBuilderDummy {
env: HashMap::new(),
files: HashMap::new(),
}),
prefix: None,
}
}
#[cfg(test)]
pub fn dummy_data(&self) -> &EnvBuilderDummy {
if let &EnvBuilderInner::Dummy(ref dummy) = &self.inner {
dummy
} else {
panic!("called dummy_data on a non-dummy builder");
}
}
fn set_prefix(&mut self, prefix: Option<&str>) {
if let Some(prefix) = prefix {
let prefix = prefix.chars()
.map(|c| c.to_uppercase().to_string())
.collect::<String>();
self.prefix = Some(format!("{}_{}", ENV_PREFIX, prefix).into());
} else {
self.prefix = Some(ENV_PREFIX.into());
}
}
fn env_name<N: AsRef<OsStr>>(&self, name: N) -> OsString {
if let Some(ref prefix) = self.prefix {
let mut result = prefix.clone();
result.push("_");
result.push(name);
result
} else {
name.as_ref().into()
}
}
fn clear_env(&mut self) {
match self.inner {
EnvBuilderInner::Real(ref mut inner) => {
inner.command.env_clear();
}
#[cfg(test)]
EnvBuilderInner::Dummy(ref mut inner) => {
inner.env.clear();
}
}
}
fn add_env_unprefixed<K: AsRef<OsStr>, V: AsRef<OsStr>>(
&mut self, k: K, v: V,
) {
match self.inner {
EnvBuilderInner::Real(ref mut inner) => {
inner.command.env(k, v);
}
#[cfg(test)]
EnvBuilderInner::Dummy(ref mut inner) => {
inner.env.insert(
k.as_ref().to_str().unwrap().into(),
v.as_ref().to_str().unwrap().into(),
);
}
}
}
pub fn add_env<K: AsRef<OsStr>, V: AsRef<OsStr>>(&mut self, k: K, v: V) {
let name = self.env_name(k);
self.add_env_unprefixed(name, v);
}
pub fn data_file<'a, P: AsRef<Path>>(
&'a mut self, path: P,
) -> Result<&'a mut Write> {
let env = path.as_ref().to_str().unwrap()
.chars()
.map(|c| c.to_uppercase().to_string())
.collect::<String>();
let name = self.env_name(env);
match self.inner {
EnvBuilderInner::Real(ref mut inner) => {
let dest = inner.data_dir.join(&path);
inner.command.env(name, &dest);
inner.last_file = Some(fs::File::create(&dest)?);
Ok(inner.last_file.as_mut().unwrap() as &mut Write)
}
#[cfg(test)]
EnvBuilderInner::Dummy(ref mut inner) => {
let dest = path.as_ref().to_str().unwrap().to_string();
inner.env.insert(name.to_str().unwrap().into(), dest.clone());
inner.files.insert(dest.clone(), Vec::new());
Ok(inner.files.get_mut(&dest).unwrap() as &mut Write)
}
}
}
}
#[derive(Debug, Clone)]
pub struct Job {
script: Arc<Script>,
provider: Option<Arc<Provider>>,
request: Request,
}
impl Job {
pub fn new(
script: Arc<Script>,
provider: Option<Arc<Provider>>,
request: Request,
) -> Job {
Job {
script,
provider,
request,
}
}
pub fn request_ip(&self) -> IpAddr {
match self.request {
Request::Web(ref req) => req.source,
Request::Status(ref req) => req.source_ip(),
}
}
pub fn trigger_status_hooks(&self) -> bool {
if let Some(ref provider) = self.provider {
provider.trigger_status_hooks(&self.request)
} else {
true
}
}
fn process(&self, ctx: &Context) -> Result<JobOutput> {
let mut command = Command::new(&self.script.exec());
let working_directory = TempDir::new("fisher")?;
let data_directory = TempDir::new("fisher")?;
{
let mut builder = EnvBuilder::new(
&mut command, &data_directory.path()
);
self.prepare_env(&mut builder, ctx)?;
}
command.current_dir(working_directory.path().to_str().unwrap());
command.env("HOME", working_directory.path().to_str().unwrap());
command.env("FISHER_REQUEST_IP", self.request_ip().to_string());
let request_body = self.save_request_body(data_directory.path())?;
if let Some(path) = request_body {
command.env("FISHER_REQUEST_BODY", path.to_str().unwrap());
}
for (key, value) in ctx.environment.iter() {
command.env(&key, &value);
}
command.before_exec(|| {
let _ = setpgid(Pid::this(), Pid::from_raw(0));
Ok(())
});
let output = command.output()?;
Ok(JobOutput::new(self, output))
}
fn prepare_env(
&self, builder: &mut EnvBuilder, ctx: &Context,
) -> Result<()> {
builder.clear_env();
builder.add_env_unprefixed("USER", &ctx.username);
for (key, value) in env::vars() {
if !DEFAULT_ENV.contains(&key.as_str()) {
continue;
}
builder.add_env_unprefixed(key, value);
}
if let Some(ref provider) = self.provider {
builder.set_prefix(Some(provider.name()));
provider.build_env(&self.request, builder)?;
}
builder.set_prefix(None);
Ok(())
}
fn save_request_body(&self, base: &Path) -> Result<Option<PathBuf>> {
let body = match self.request {
Request::Web(ref req) => &req.body,
Request::Status(..) => return Ok(None),
};
let mut path = base.to_path_buf();
path.push("request_body");
let mut file = fs::File::create(&path)?;
write!(file, "{}\n", body)?;
Ok(Some(path))
}
}
impl JobTrait<Script> for Job {
type Context = Context;
type Output = JobOutput;
fn execute(&self, ctx: &Context) -> Result<JobOutput> {
self.process(ctx)
}
fn script_id(&self) -> UniqueId {
self.script.id()
}
fn script_name(&self) -> &str {
self.script.name()
}
}
#[derive(Debug, Clone)]
pub struct JobOutput {
pub stdout: String,
pub stderr: String,
pub success: bool,
pub exit_code: Option<i32>,
pub signal: Option<i32>,
pub script_name: String,
pub request_ip: IpAddr,
pub trigger_status_hooks: bool,
}
impl JobOutput {
fn new<'a>(job: &'a Job, output: Output) -> Self {
JobOutput {
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
success: output.status.success(),
exit_code: output.status.code(),
signal: output.status.signal(),
script_name: job.script_name().into(),
request_ip: job.request_ip(),
trigger_status_hooks: job.trigger_status_hooks(),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use users;
use common::prelude::*;
use requests::Request;
use scripts::test_utils::*;
use utils;
use super::{Job, Context, DEFAULT_ENV};
fn parse_env(content: &str) -> HashMap<&str, &str> {
let mut result = HashMap::new();
for line in content.split("\n") {
if line.trim() == "" {
continue;
}
let (key, value) = utils::parse_env(line).unwrap();
result.insert(key, value);
}
result
}
fn create_job(env: &TestEnv, name: &str, req: Request) -> Result<Job> {
let script = env.load_script(name)?;
let (_, provider) = script.validate(&req);
Ok(Job::new(Arc::new(script), provider, req))
}
fn content<P: AsRef<Path>>(base: P, name: &str) -> Result<String> {
let mut file = File::open(&base.as_ref().join(name))?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
Ok(buf)
}
#[test]
fn test_job_creation() {
test_wrapper(|env| {
env.create_script("example.sh", &[])?;
let req = dummy_web_request().into();
let job = create_job(env, "example.sh", req)?;
assert_eq!(job.script_name(), "example.sh");
Ok(())
});
}
#[test]
fn test_job_execution() {
test_wrapper(|env| {
let ctx = Context::default();
let req: Request = dummy_web_request().into();
env.create_script("success.sh", &[
"#!/bin/bash",
"exit 0",
])?;
env.create_script("fail.sh", &[
"#!/bin/bash",
"exit 1",
])?;
let job = create_job(env, "success.sh", req.clone())?;
let result = job.process(&ctx)?;
assert!(result.success);
assert_eq!(result.exit_code, Some(0));
let job = create_job(env, "fail.sh", req.clone())?;
let result = job.process(&ctx)?;
assert!(!result.success);
assert_eq!(result.exit_code, Some(1));
Ok(())
})
}
fn collect_env(env: &mut TestEnv, ctx: &Context) -> Result<PathBuf> {
env.create_script("dump.sh", &[
r#"#!/bin/bash"#,
r#"## Fisher-Testing: {}"#,
r#"env"#,
r#"b="${FISHER_TESTING_ENV}""#,
r#"echo "executed" > "${b}/executed""#,
r#"env > "${b}/env""#,
r#"pwd > "${b}/pwd""#,
r#"cat "${FISHER_REQUEST_BODY}" > "${b}/request_body""#,
])?;
let out = env.tempdir()?;
let mut req = dummy_web_request();
req.body = "a body!".into();
req.params.insert("env".into(), out.to_str().unwrap().into());
let job = create_job(env, "dump.sh", req.into())?;
let result = job.process(ctx)?;
if !result.success {
println!("\nExit code: {:?}", result.exit_code);
println!("Killed with signal: {:?}", result.signal);
if result.stdout.trim().len() > 0 {
println!("\nJob stdout:\n{}", result.stdout);
}
if result.stderr.trim().len() > 0 {
println!("\nJob stderr:\n{}", result.stderr);
}
panic!("the job failed");
}
Ok(out)
}
#[test]
fn test_job_environment() {
test_wrapper(|mut env| {
let out = collect_env(&mut env, &Context::default())?;
assert_eq!(&content(&out, "executed")?, "executed\n");
assert_eq!(&content(&out, "request_body")?, "a body!\n");
let working_directory = content(&out, "pwd")?;
let env_content = content(&out, "env")?;
let env_vars = parse_env(&env_content);
let extra_env = vec![
"FISHER_TESTING_ENV", "FISHER_REQUEST_IP",
"FISHER_REQUEST_BODY", "FISHER_TESTING_PREPARED", "HOME",
"USER",
"PWD", "SHLVL", "_",
];
let env_expected = DEFAULT_ENV.iter()
.chain(extra_env.iter())
.collect::<Vec<_>>();
let mut found = 0;
for (key, _) in &env_vars {
if env_expected.contains(&key) {
found += 1;
} else {
panic!("Extra environment variable found: {}", key);
}
}
assert_eq!(found, env_expected.len());
let request_body_path = Path::new(&env_vars["FISHER_REQUEST_BODY"]);
let home_path = Path::new(&env_vars["HOME"]);
assert_ne!(request_body_path.parent(), home_path.parent());
assert_eq!(&env_vars["FISHER_TESTING_ENV"], &out.to_str().unwrap());
assert_eq!(&env_vars["FISHER_REQUEST_IP"], &"127.0.0.1");
assert_eq!(&env_vars["HOME"], &working_directory.trim());
assert_eq!(
&env_vars["USER"],
&users::get_current_username().unwrap()
);
for key in DEFAULT_ENV {
if let Ok(content) = env::var(key) {
assert_eq!(&content, env_vars[key]);
}
}
Ok(())
});
}
#[test]
fn test_job_environment_with_extra_env() {
test_wrapper(|mut env| {
let ctx = Context {
environment: {
let mut extra = HashMap::new();
extra.insert("TEST_ENV".into(), "yes".into());
extra
},
.. Context::default()
};
let out = collect_env(&mut env, &ctx)?;
let env_content = content(&out, "env")?;
let env_vars = parse_env(&env_content);
assert_eq!(&env_vars["TEST_ENV"], &"yes");
Ok(())
});
}
#[test]
fn test_job_environment_with_altered_user() {
test_wrapper(|mut env| {
let old_user = env::var_os("USER");
let mut new_name: OsString = if let Some(ref old) = old_user {
old.into()
} else {
users::get_current_username().unwrap().into()
};
new_name.push("-dummy");
env::set_var("USER", new_name);
let out = collect_env(&mut env, &Context::default())?;
let env_content = content(&out, "env")?;
let env_vars = parse_env(&env_content);
assert_eq!(
&env_vars["USER"],
&users::get_current_username().unwrap()
);
if let Some(name) = old_user {
env::set_var("USER", name);
} else {
env::remove_var("USER");
}
Ok(())
});
}
}