use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
env, fs,
io::{BufRead, BufReader, Error, ErrorKind, Result},
process::{Child, Command, Stdio},
};
use sysinfo::{Pid, System};
pub type ServiceStates = HashMap<String, (ServiceState, u32)>;
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ServiceMetadata {
#[serde(default)]
pub owner: String,
#[serde(default)]
pub repository: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub license: String,
#[serde(default)]
pub build: Vec<String>,
}
impl Default for ServiceMetadata {
fn default() -> Self {
Self {
owner: String::new(),
repository: String::new(),
description: "Unknown service".to_string(),
license: "ISC".to_string(),
build: Vec::new(),
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub enum ServiceType {
Service,
Application,
}
impl Default for ServiceType {
fn default() -> Self {
Self::Service
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Service {
#[serde(default)]
pub r#type: ServiceType,
pub command: String,
pub working_directory: String,
pub environment: Option<HashMap<String, String>>,
#[serde(default)]
pub restart: bool,
#[serde(default)]
pub metadata: ServiceMetadata,
}
impl Service {
pub fn run(name: String, config: ServicesConfiguration) -> Result<(Service, Child)> {
if let Some(s) = config.service_states.get(&name) {
if s.0 == ServiceState::Running {
return Err(Error::new(
ErrorKind::AlreadyExists,
format!("Service is already running. ({name})"),
));
}
};
let service = match config.services.get(&name) {
Some(s) => s,
None => {
return Err(Error::new(
ErrorKind::NotFound,
format!("Service does not exist. ({name})"),
))
}
};
println!("info: cmd: {}", service.command);
let command_split: Vec<&str> = service.command.split(" ").collect();
let mut cmd = Command::new(command_split.get(0).unwrap());
for arg in command_split.iter().skip(1) {
cmd.arg(arg);
}
if let Some(env) = service.environment.clone() {
for var in env {
cmd.env(var.0, var.1);
}
}
cmd.current_dir(&service.working_directory);
Ok((service.to_owned(), cmd.spawn()?))
}
pub fn kill(name: String, config: ServicesConfiguration) -> Result<()> {
let s = match config.service_states.get(&name) {
Some(s) => s,
None => {
return Err(Error::new(
ErrorKind::NotFound,
format!("Service is not loaded. ({name})"),
))
}
};
if s.0 != ServiceState::Running {
return Err(Error::new(
ErrorKind::NotConnected,
"Service is not running.",
));
}
let mut config_c = config.clone();
let service = match config_c.services.get_mut(&name) {
Some(s) => s,
None => {
return Err(Error::new(
ErrorKind::NotFound,
format!("Service does not exist. ({name})"),
))
}
};
let sys = System::new_all();
match sys.process(Pid::from(s.1 as usize)) {
Some(process) => {
let supposed_to_restart = service.restart.clone();
if supposed_to_restart {
service.restart = false;
ServicesConfiguration::update_config(config_c.clone())?;
}
process.kill();
std::thread::sleep(std::time::Duration::from_secs(1));
if supposed_to_restart {
ServicesConfiguration::update_config(config.clone())?;
}
Ok(())
}
None => Err(Error::new(
ErrorKind::NotConnected,
format!("Failed to get process from PID. ({name})"),
)),
}
}
pub fn info(name: String, service_states: ServiceStates) -> Result<String> {
let s = match service_states.get(&name) {
Some(s) => s,
None => {
return Err(Error::new(
ErrorKind::NotFound,
format!("Service is not loaded. ({name})"),
))
}
};
if s.0 != ServiceState::Running {
return Err(Error::new(
ErrorKind::NotConnected,
format!("Service is not running. ({name})"),
));
}
let sys = System::new_all();
if let Some(process) = sys.process(Pid::from(s.1 as usize)) {
let info = ServiceInfo {
name: name.to_string(),
pid: process.pid().to_string().parse().unwrap(),
memory: process.memory(),
cpu: process.cpu_usage(),
status: process.status().to_string(),
running_for_seconds: process.run_time(),
};
Ok(toml::to_string_pretty(&info).unwrap())
} else {
Err(Error::new(
ErrorKind::NotConnected,
format!("Failed to get process from PID. ({name})"),
))
}
}
pub async fn observe(name: String, service_states: ServiceStates) -> Result<()> {
let s = match service_states.get(&name) {
Some(s) => s,
None => {
return Err(Error::new(
ErrorKind::NotFound,
format!("Service is not loaded. ({name})"),
))
}
};
if s.0 != ServiceState::Running {
return Err(Error::new(
ErrorKind::NotConnected,
format!("Service is not running. ({name})"),
));
}
let sys = System::new_all();
if let Some(process) = sys.process(Pid::from(s.1 as usize)) {
process.wait();
Ok(())
} else {
Err(Error::new(
ErrorKind::NotConnected,
format!("Failed to get process from PID. ({name})"),
))
}
}
async fn wait(name: String, config: &mut ServicesConfiguration) -> Result<()> {
let process = match Service::run(name.clone(), config.clone()) {
Ok(p) => p,
Err(e) => return Err(e),
};
config
.service_states
.insert(name.to_string(), (ServiceState::Running, process.1.id()));
ServicesConfiguration::update_config(config.clone()).expect("Failed to update config");
Service::observe(name.clone(), config.service_states.clone())
.await
.expect("Failed to observe service");
Ok(())
}
pub async fn spawn(name: String) -> Result<()> {
tokio::task::spawn(async move {
loop {
let mut config = ServicesConfiguration::get_config();
Service::wait(name.clone(), &mut config)
.await
.expect("Failed to wait for service");
let mut config = ServicesConfiguration::get_config();
let service = match config.services.get(&name) {
Some(s) => s,
None => return,
};
config.service_states.remove(&name);
ServicesConfiguration::update_config(config.clone())
.expect("Failed to update config");
if service.restart == false {
break;
}
println!("info: auto-restarting service \"{}\"", name);
continue; }
});
Ok(())
}
pub async fn bootstrap(&self, name: String) -> Result<()> {
let home = env::var("HOME").expect("failed to read $HOME");
if let Err(_) = fs::read_dir(format!("{home}/.config/sproc/modules")) {
if let Err(e) = fs::create_dir(format!("{home}/.config/sproc/modules")) {
panic!("{:?}", e);
}
}
let dir = format!("{home}/.config/sproc/modules/{}", name);
if let Ok(_) = fs::read_dir(&dir) {
return Err(Error::new(ErrorKind::AlreadyExists, "The requested service has already run its build commands or its build directory already exists."));
}
fs::create_dir(&dir)?;
let build_file = format!("{dir}/build.artifact.sh");
fs::write(&build_file, self.metadata.build.join("\n"))?;
let command = format!("bash {build_file}");
let command_split: Vec<&str> = command.split(" ").collect();
let mut cmd = Command::new(command_split.get(0).unwrap());
for arg in command_split.iter().skip(1) {
cmd.arg(arg);
}
cmd.current_dir(&dir);
let child_stdout = cmd
.stdout(Stdio::piped())
.spawn()?
.stdout
.expect("failed to capture command output");
let reader = BufReader::new(child_stdout);
reader
.lines()
.filter_map(|l| l.ok())
.for_each(|l| println!("build: {l}"));
Ok(())
}
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub enum ServiceState {
Running,
Stopped,
}
impl Default for ServiceState {
fn default() -> Self {
Self::Stopped
}
}
#[derive(Serialize, Deserialize)]
pub struct ServiceInfo {
pub name: String,
pub pid: u32,
pub memory: u64,
pub cpu: f32,
pub status: String,
pub running_for_seconds: u64,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct RegistryConfiguration {
pub enabled: bool,
#[serde(default)]
pub description: String,
#[serde(default = "registry_default")]
pub name: String,
}
fn registry_default() -> String {
"Registry".to_owned()
}
impl Default for RegistryConfiguration {
fn default() -> Self {
Self {
enabled: true,
description: String::new(),
name: registry_default(),
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ServerConfiguration {
pub port: u16,
pub key: String,
#[serde(default)]
pub registry: RegistryConfiguration,
}
impl Default for ServerConfiguration {
fn default() -> Self {
Self {
port: 6374,
key: String::new(),
registry: RegistryConfiguration::default(),
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ServicesConfiguration {
#[serde(default)]
pub source: String,
pub inherit: Option<Vec<String>>,
#[serde(default)]
pub server: ServerConfiguration,
pub services: HashMap<String, Service>,
#[serde(default)]
pub service_states: ServiceStates,
}
impl Default for ServicesConfiguration {
fn default() -> Self {
Self {
source: String::new(),
inherit: None,
services: HashMap::new(),
server: ServerConfiguration::default(),
service_states: HashMap::new(),
}
}
}
impl ServicesConfiguration {
pub fn read(contents: String) -> Self {
let mut res = toml::from_str::<Self>(&contents).unwrap();
if let Some(ref inherit) = res.inherit {
for path in inherit {
if let Ok(c) = fs::read_to_string(path) {
for service in toml::from_str::<Self>(&c).unwrap().services {
res.services.insert(service.0, service.1);
}
}
}
}
res
}
pub fn get_config() -> Self {
let home = env::var("HOME").expect("failed to read $HOME");
if let Err(_) = fs::read_dir(format!("{home}/.config/sproc")) {
if let Err(_) = fs::read_dir(format!("{home}/.config")) {
if let Err(e) = fs::create_dir(format!("{home}/.config")) {
panic!("{:?}", e);
}
}
if let Err(e) = fs::create_dir(format!("{home}/.config/sproc")) {
panic!("{:?}", e)
};
}
let path = format!("{home}/.config/sproc/services.toml");
match fs::read_to_string(path.clone()) {
Ok(c) => ServicesConfiguration::read(c),
Err(_) => Self::default(),
}
}
pub fn update_config(contents: Self) -> Result<()> {
let home = env::var("HOME").expect("failed to read $HOME");
fs::write(
format!("{home}/.config/sproc/services.toml"),
format!("# DO **NOT** MANUALLY EDIT THIS FILE! Please edit the source instead and run `sproc pin {{path}}`.\n{}", toml::to_string_pretty::<Self>(&contents).unwrap()),
)
}
pub fn merge_config(&mut self, other: Self) -> () {
for service in other.services {
self.services.insert(service.0, service.1);
}
}
}
#[derive(Serialize, Deserialize)]
pub struct RegistryPushRequestBody {
pub key: String,
pub content: String,
}
#[derive(Serialize, Deserialize)]
pub struct RegistryDeleteRequestBody {
pub key: String,
}
#[derive(Debug, Clone)]
pub struct Registry(pub ServerConfiguration, pub String);
impl Registry {
pub fn new(config: ServerConfiguration) -> Self {
let home = env::var("HOME").expect("failed to read $HOME");
let dir = format!("{home}/.config/sproc/registry");
if let Err(_) = fs::read_dir(&dir) {
if let Err(e) = fs::create_dir(&dir) {
panic!("{:?}", e);
}
}
Self(config, dir)
}
pub fn get(&self, service: String) -> Result<String> {
if self.0.registry.enabled == false {
return Err(Error::new(
ErrorKind::PermissionDenied,
"Registry is disabled",
));
}
fs::read_to_string(format!("{}/{}.toml", self.1, service))
}
pub fn push(&self, props: RegistryPushRequestBody, service: String) -> Result<()> {
if self.0.registry.enabled == false {
return Err(Error::new(
ErrorKind::PermissionDenied,
"Registry is disabled",
));
}
if props.key != self.0.key {
return Err(Error::new(ErrorKind::PermissionDenied, "Key is invalid"));
}
if let Err(e) = toml::from_str::<Service>(&props.content) {
return Err(Error::new(ErrorKind::InvalidInput, e.to_string()));
};
fs::write(format!("{}/{}.toml", self.1, service), &props.content)
}
pub fn delete(&self, props: RegistryDeleteRequestBody, service: String) -> Result<()> {
if self.0.registry.enabled == false {
return Err(Error::new(
ErrorKind::PermissionDenied,
"Registry is disabled",
));
}
if props.key != self.0.key {
return Err(Error::new(ErrorKind::PermissionDenied, "Key is invalid"));
}
fs::remove_file(format!("{}/{}.toml", self.1, service))
}
}