254 lines
6.8 KiB
Rust
254 lines
6.8 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use anyhow::{Context, Result};
|
|
use command_group::CommandGroup;
|
|
use nix::sys::signal;
|
|
use nix::sys::wait::{self, WaitStatus};
|
|
use nix::unistd::Pid;
|
|
use std::fs::File as StdFile;
|
|
use std::os::unix::io::FromRawFd;
|
|
use std::os::unix::io::IntoRawFd;
|
|
use std::process::Command;
|
|
use std::process::Stdio;
|
|
use std::sync::Arc;
|
|
use tokio::fs::File;
|
|
use tokio::io::AsyncBufReadExt;
|
|
use tokio::io::BufReader;
|
|
use tokio::signal::unix;
|
|
use tokio::sync::oneshot;
|
|
use tokio::sync::Mutex;
|
|
|
|
mod buffer;
|
|
pub use buffer::Logs;
|
|
|
|
pub struct Process {
|
|
cmd: String,
|
|
env: HashMap<String, String>,
|
|
cwd: String,
|
|
}
|
|
type WaitChannel = oneshot::Receiver<WaitStatus>;
|
|
|
|
pub struct Child {
|
|
pub pid: Pid,
|
|
ch: WaitChannel,
|
|
}
|
|
|
|
impl Child {
|
|
pub fn new(pid: Pid, ch: WaitChannel) -> Child {
|
|
Child { pid, ch }
|
|
}
|
|
|
|
pub async fn wait(self) -> Result<WaitStatus> {
|
|
Ok(self.ch.await?)
|
|
}
|
|
}
|
|
|
|
type Handler = oneshot::Sender<WaitStatus>;
|
|
|
|
impl Process {
|
|
pub fn new<S: Into<String>>(cmd: S, cwd: S, env: Option<HashMap<String, String>>) -> Process {
|
|
let env = env.unwrap_or_default();
|
|
|
|
Process {
|
|
env,
|
|
cmd: cmd.into(),
|
|
cwd: cwd.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub enum Log {
|
|
None,
|
|
Stdout,
|
|
Ring(String),
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct ProcessManager {
|
|
table: Arc<Mutex<HashMap<Pid, Handler>>>,
|
|
ring: buffer::Ring,
|
|
env: Environ,
|
|
}
|
|
|
|
impl ProcessManager {
|
|
pub fn new(cap: usize) -> ProcessManager {
|
|
ProcessManager {
|
|
table: Arc::new(Mutex::new(HashMap::new())),
|
|
ring: buffer::Ring::new(cap),
|
|
env: Environ::new(),
|
|
}
|
|
}
|
|
|
|
fn wait_process() -> Vec<WaitStatus> {
|
|
let mut statuses: Vec<WaitStatus> = Vec::new();
|
|
loop {
|
|
let status = match wait::waitpid(Option::None, Some(wait::WaitPidFlag::WNOHANG)) {
|
|
Ok(status) => status,
|
|
Err(_) => {
|
|
return statuses;
|
|
}
|
|
};
|
|
match status {
|
|
WaitStatus::StillAlive => break,
|
|
_ => statuses.push(status),
|
|
}
|
|
}
|
|
statuses
|
|
}
|
|
|
|
pub fn start(&self) {
|
|
let table = Arc::clone(&self.table);
|
|
let mut signals = match unix::signal(unix::SignalKind::child()) {
|
|
Ok(s) => s,
|
|
Err(err) => {
|
|
panic!("failed to bind to signals: {}", err);
|
|
}
|
|
};
|
|
|
|
tokio::spawn(async move {
|
|
loop {
|
|
signals.recv().await;
|
|
let mut table = table.lock().await;
|
|
for exited in Self::wait_process() {
|
|
if let Some(pid) = exited.pid() {
|
|
if let Some(sender) = table.remove(&pid) {
|
|
if sender.send(exited).is_err() {
|
|
debug!("failed to send exit state to process: {}", pid);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn sink(&self, file: File, prefix: String) {
|
|
let ring = self.ring.clone();
|
|
let reader = BufReader::new(file);
|
|
|
|
tokio::spawn(async move {
|
|
let mut lines = reader.lines();
|
|
while let Ok(line) = lines.next_line().await {
|
|
let _ = match line {
|
|
Some(line) => ring.push(format!("{}: {}", prefix, line)).await,
|
|
None => break,
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
pub async fn stream(&self, existing_logs: bool, follow: bool) -> Logs {
|
|
self.ring.stream(existing_logs, follow).await
|
|
}
|
|
|
|
pub fn signal(&self, pid: Pid, sig: signal::Signal) -> Result<()> {
|
|
Ok(signal::killpg(pid, sig)?)
|
|
}
|
|
|
|
pub async fn run(&self, cmd: Process, log: Log) -> Result<Child> {
|
|
let args = shlex::split(&cmd.cmd).context("failed to parse command")?;
|
|
if args.is_empty() {
|
|
bail!("invalid command");
|
|
}
|
|
|
|
let mut child = Command::new(&args[0]);
|
|
|
|
let child = if !cmd.cwd.is_empty() {
|
|
child.current_dir(&cmd.cwd)
|
|
} else {
|
|
child.current_dir("/")
|
|
};
|
|
|
|
let child = child.args(&args[1..]).envs(&self.env.0).envs(cmd.env);
|
|
|
|
let child = match log {
|
|
Log::None => child.stdout(Stdio::null()).stderr(Stdio::null()),
|
|
Log::Ring(_) => child.stdout(Stdio::piped()).stderr(Stdio::piped()),
|
|
_ => child, // default to inherit
|
|
};
|
|
|
|
let mut table = self.table.lock().await;
|
|
|
|
let mut child = child
|
|
.group_spawn()
|
|
.context("failed to spawn command")?
|
|
.into_inner();
|
|
|
|
if let Log::Ring(prefix) = log {
|
|
let _ = self
|
|
.ring
|
|
.push(format!("[-] {}: ------------ [start] ------------", prefix))
|
|
.await;
|
|
|
|
if let Some(out) = child.stdout.take() {
|
|
let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) });
|
|
self.sink(out, format!("[+] {}", prefix))
|
|
}
|
|
|
|
if let Some(out) = child.stderr.take() {
|
|
let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) });
|
|
self.sink(out, format!("[-] {}", prefix))
|
|
}
|
|
}
|
|
|
|
let (tx, rx) = oneshot::channel();
|
|
|
|
let id = child.id();
|
|
|
|
let pid = Pid::from_raw(id as i32);
|
|
table.insert(pid, tx);
|
|
|
|
Ok(Child::new(pid, rx))
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct Environ(HashMap<String, String>);
|
|
|
|
impl Environ {
|
|
fn new() -> Environ {
|
|
let env = match Environ::parse("/etc/environment") {
|
|
Ok(r) => r,
|
|
Err(err) => {
|
|
error!("failed to load /etc/environment file: {}", err);
|
|
HashMap::new()
|
|
}
|
|
};
|
|
|
|
Environ(env)
|
|
}
|
|
|
|
fn parse<P>(p: P) -> Result<HashMap<String, String>, std::io::Error>
|
|
where
|
|
P: AsRef<std::path::Path>,
|
|
{
|
|
let mut m = HashMap::new();
|
|
let txt = match std::fs::read_to_string(p) {
|
|
Ok(txt) => txt,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
info!("skipping /etc/environment file because it does not exist");
|
|
"".into()
|
|
}
|
|
Err(err) => return Err(err),
|
|
};
|
|
|
|
for line in txt.lines() {
|
|
let line = line.trim();
|
|
if line.starts_with('#') {
|
|
continue;
|
|
}
|
|
let parts: Vec<&str> = line.splitn(2, '=').collect();
|
|
let key = String::from(parts[0]);
|
|
let value = match parts.len() {
|
|
2 => String::from(parts[1]),
|
|
_ => String::default(),
|
|
};
|
|
//m.into_iter()
|
|
m.insert(key, value);
|
|
}
|
|
|
|
Ok(m)
|
|
}
|
|
}
|