use anyhow::{anyhow, Result};
use log::{debug, error, info, warn};
use sopht::{
create_local_dirs, socket_file_path, Command, Connection, Error, Message,
ProcessStatus, Response, State,
};
use std::os::unix::net::UnixListener;
use std::path::Path;
use std::sync::atomic::{self, AtomicBool};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::Duration;
type Quit = bool;
struct ManagerThread {
pub handle: JoinHandle<()>,
pub command_sender: Sender<Command>,
pub response_receiver: Receiver<Response>,
}
fn main() {
env_logger::init_from_env(
env_logger::Env::new()
.filter_or("SOPHT_LOG", "info")
.write_style("SOPHT_LOG_STYLE"),
);
if let Err(e) = create_local_dirs() {
error!("failed to create sopht local dirs. error: {}", e);
return;
}
let socket_file = match socket_file_path() {
Ok(fp) => fp,
Err(e) => {
error!("could not obtain path to socket file. error: {}", e);
return;
}
};
let interrupt = Arc::new(AtomicBool::new(false));
signal_hook::flag::register_conditional_shutdown(
signal_hook::consts::SIGINT,
1,
interrupt.clone(),
)
.expect("could not register conditional shutdown for SIGINT");
signal_hook::flag::register(
signal_hook::consts::SIGTERM,
Arc::clone(&interrupt),
)
.expect("could not set SIGTERM handler");
signal_hook::flag::register(
signal_hook::consts::SIGINT,
Arc::clone(&interrupt),
)
.expect("could not set SIGINT handler");
if socket_file.exists() {
error!("could not start server. socket already exists");
return;
}
let state = State::new(1);
let mut manager = spawn_manager_thread(state, interrupt.clone());
if let Err(e) = listen(&mut manager, &socket_file, interrupt.clone()) {
interrupt.store(true, atomic::Ordering::Relaxed);
error!("failed to open listen server. error: {}", e);
}
manager
.handle
.join()
.expect("failed to join manager thread. this might need some cleanup!");
match std::fs::remove_file(socket_file) {
Ok(_) => info!("shutdown OK"),
Err(e) => error!("could not remove socket file. error: {}", e),
}
}
fn spawn_manager_thread(
mut state: State,
interrupt: Arc<AtomicBool>,
) -> ManagerThread {
let (cmdtx, cmdrx) = mpsc::channel::<Command>();
let (rsptx, rsprx) = mpsc::channel::<Response>();
let timeout = Duration::from_secs(1);
let handle = thread::spawn(move || loop {
let cmd: Option<Command> = match cmdrx.recv_timeout(timeout) {
Ok(cmd) => Some(cmd),
Err(RecvTimeoutError::Timeout) => None,
Err(e) => {
panic!("manager thread receiver disconnected. error: {}", e)
}
};
if let Some(cmd) = cmd {
let resp = do_command(&mut state, &cmd);
rsptx
.send(resp)
.expect("manager thread failed to send response");
if let Command::Shutdown = cmd {
info!("manager thread received shutdown command. exiting");
return;
}
} else {
match state.upkeep() {
Ok(info) => {
for pid in info.restarted {
let new_pid = state.status_of(pid).restarted.unwrap();
warn!(
"process {} failed and was restarted. new PID: {}",
pid, new_pid,
);
}
}
Err(e) => warn!("encountered error during upkeep: {}", e),
}
}
if interrupt.load(atomic::Ordering::Relaxed) {
return;
}
});
ManagerThread {
handle,
command_sender: cmdtx,
response_receiver: rsprx,
}
}
fn listen(
manager: &mut ManagerThread,
socket_path: &Path,
interrupt: Arc<AtomicBool>,
) -> Result<()> {
let listener = match UnixListener::bind(socket_path) {
Ok(l) => l,
Err(e) => return Err(anyhow!(e)),
};
listener
.set_nonblocking(true)
.expect("failed to make listener nonblocking");
info!(
"sophtd v{} successfully started!",
env!("CARGO_PKG_VERSION")
);
while !interrupt.load(atomic::Ordering::Relaxed) {
let stream = listener.accept().map(|(s, _)| s);
match stream {
Ok(stream) => match Connection::new(stream) {
Ok(conn) => match handle_connection(manager, conn) {
Ok(true) => {
info!("received shutdown command. quitting");
break;
}
Ok(_) => {}
Err(e) => {
warn!("connection terminated with error: {}", e)
}
},
Err(e) => {
warn!("failed to create connection. error: {}", e);
}
},
Err(e) => {
if e.kind() != std::io::ErrorKind::WouldBlock {
warn!("failed to connect to stream. error: {}", e);
}
}
}
std::thread::sleep(Duration::from_secs(1));
}
Ok(())
}
fn handle_connection(
manager: &mut ManagerThread,
mut conn: Connection,
) -> Result<Quit> {
loop {
let cmd = match conn.recv_command() {
Ok(cmd) => cmd,
Err(Error::ConnectionClosed) => break,
Err(e) => return Err(anyhow!(e)),
};
debug!("received command: {:?}", cmd);
let shutdown = Command::Shutdown == cmd;
manager.command_sender.send(cmd)?;
let resp = manager.response_receiver.recv()?;
debug!("generated response: {:?}", resp);
match conn.send(&Message::Response(resp)) {
Ok(_) => {}
Err(Error::ConnectionClosed) => break,
Err(e) => return Err(anyhow!(e)),
}
if shutdown {
return Ok(true);
}
}
Ok(false)
}
fn do_command(state: &mut State, cmd: &Command) -> Response {
match cmd {
Command::Hello => succeed("hello!"),
Command::Shutdown => succeed("shutdown"),
Command::Start(args) => match state.start(args) {
Ok(p) => {
info!("started \"{}\" with PID {}", args.prog, p.pid);
succeed_status(
format!("started `{}` with PID {}", p.prog, p.pid),
&[p],
)
}
Err(e) => {
warn!("failed to start \"{}\". error: {}", args.prog, e);
fail(format!("{}", e))
}
},
Command::Output(pid) => match state.output(*pid) {
Ok(data) => succeed(String::from_utf8_lossy(data)),
Err(e) => fail(format!("{}", e)),
},
Command::Status(count) => Response {
success: true,
message: "".into(),
status: Some(state.status(count)),
},
Command::Stop(pid) => match state.stop(*pid) {
Ok(p) => {
info!("stopped process {}", pid);
succeed_status("successfully stopped process", &[p])
}
Err(e) => {
warn!("failed to stop process {}", pid);
fail(format!("{}", e))
}
},
Command::Restart(pid) => match state.restart(*pid, false) {
Ok(p) => {
info!("restarted process {} with new PID {}", pid, p.pid);
succeed_status(
format!(
"successfully restarted process. new PID: {}",
p.pid
),
&[p],
)
}
Err(e) => {
warn!("failed to restart process {}. error: {}", pid, e);
fail(format!("{}", e))
}
},
Command::Send(args) => match state.send(args) {
Ok(_) => succeed("successfully sent input to process"),
Err(e) => fail(format!("could not send input. error: {}", e)),
},
Command::ChangeRestartPolicy(args) => {
match state.change_restart_policy(args) {
Ok(status) => {
succeed_status("altered restart policy", &[status])
}
Err(e) => {
fail(format!("could not change policy. error: {}", e))
}
}
}
}
}
fn succeed<S: Into<String>>(message: S) -> Response {
Response {
success: true,
message: message.into(),
status: None,
}
}
fn fail<S: Into<String>>(message: S) -> Response {
Response {
success: false,
message: message.into(),
status: None,
}
}
fn succeed_status<S: Into<String>>(
message: S,
status: &[ProcessStatus],
) -> Response {
Response {
success: true,
message: message.into(),
status: Some(Vec::from(status)),
}
}