do things that don't work

This commit is contained in:
nora 2021-12-17 22:34:49 +01:00
parent 6d572fc9d0
commit 2a2759c020
6 changed files with 207 additions and 51 deletions

View file

@ -1 +1,3 @@
# service-manager # service-manager
unix only for now, rip windows

View file

@ -10,4 +10,13 @@ env = { HELLO = "uwu hi ヾ(•ω•`)o" }
[pwd] [pwd]
command = "pwd" command = "pwd"
workdir = "./src" workdir = "./src"
[crash]
command = "cat fkasdjfölashjdflksd"
[side-effect-create]
command = "touch uwu.txt"
[side-effect-remove]
command = "rm uwu.txt"

View file

@ -4,9 +4,13 @@ use crate::{view, App};
use crossterm::event; use crossterm::event;
use crossterm::event::{Event, KeyCode}; use crossterm::event::{Event, KeyCode};
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::OsStr; use std::ffi::{OsStr, OsString};
use std::io; use std::io;
use std::process::{Command, Stdio}; use std::io::{ErrorKind, Read};
use std::os::unix::ffi::OsStrExt;
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::TryRecvError;
use std::sync::{mpsc, Mutex};
use tui::backend::Backend; use tui::backend::Backend;
use tui::widgets::TableState; use tui::widgets::TableState;
use tui::Terminal; use tui::Terminal;
@ -15,11 +19,13 @@ pub fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Resu
loop { loop {
terminal.draw(|f| view::render_ui(f, &mut app))?; terminal.draw(|f| view::render_ui(f, &mut app))?;
app.recv_stdouts();
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
match key.code { match key.code {
KeyCode::Char('q') => match app.selected { KeyCode::Char('q') => match app.selected {
Some(_) => app.leave_service(), Some(_) => app.leave_service(),
None => return Ok(()), None => break,
}, },
KeyCode::Char('r') => app.run_service(), KeyCode::Char('r') => app.run_service(),
KeyCode::Down => app.next(), KeyCode::Down => app.next(),
@ -30,35 +36,71 @@ pub fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Resu
} }
} }
} }
// terminate the child processes
for sender in app.thread_terminates {
let _ = sender.send(());
}
Ok(())
} }
impl App { impl App {
pub fn new(config: Config) -> App { pub fn new(config: Config) -> io::Result<App> {
App { Ok(App {
table: AppState { table: AppState {
table_state: TableState::default(), table_state: TableState::default(),
items: config services: config
.into_iter() .into_iter()
.map(|(name, service)| Service { .map(|(name, service)| -> io::Result<Service> {
command: service.command, let (stdout_send, stdout_recv) = mpsc::channel();
name,
workdir: service Ok(Service {
.workdir command: service.command,
.unwrap_or_else(|| std::env::current_dir().unwrap()), name,
env: service.env.unwrap_or_else(HashMap::new), workdir: service
status: ServiceStatus::NotStarted, .workdir
child: None, .ok_or_else(|| io::Error::from(ErrorKind::Other))
.or_else(|_| std::env::current_dir())?,
env: service.env.unwrap_or_else(HashMap::new),
status: Mutex::new(ServiceStatus::NotStarted),
stdout_buf: OsString::new(),
stdout_recv,
stdout_send: Mutex::new(Some(stdout_send)),
})
}) })
.collect(), .collect::<io::Result<_>>()?,
}, },
selected: None, selected: None,
} thread_terminates: Vec::new(),
})
} }
pub fn is_table(&self) -> bool { pub fn is_table(&self) -> bool {
self.selected.is_none() self.selected.is_none()
} }
fn recv_stdouts(&mut self) {
for service in self.table.services.iter_mut() {
while let Ok(vec) = service.stdout_recv.try_recv() {
service.stdout_buf.push(OsStr::from_bytes(&vec));
std::fs::write(
format!(
"debug/received_something_{}_{}.txt",
service.name,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
),
service.stdout_buf.as_bytes(),
)
.expect("debug failed fuck");
}
}
}
fn select_service(&mut self) { fn select_service(&mut self) {
if self.is_table() { if self.is_table() {
if let Some(selected) = self.table.table_state.selected() { if let Some(selected) = self.table.table_state.selected() {
@ -74,7 +116,7 @@ impl App {
fn next(&mut self) { fn next(&mut self) {
let i = match self.table.table_state.selected() { let i = match self.table.table_state.selected() {
Some(i) => { Some(i) => {
if i >= self.table.items.len() - 1 { if i >= self.table.services.len() - 1 {
0 0
} else { } else {
i + 1 i + 1
@ -89,7 +131,7 @@ impl App {
let i = match self.table.table_state.selected() { let i = match self.table.table_state.selected() {
Some(i) => { Some(i) => {
if i == 0 { if i == 0 {
self.table.items.len() - 1 self.table.services.len() - 1
} else { } else {
i - 1 i - 1
} }
@ -108,8 +150,16 @@ impl App {
} }
fn start_service(&mut self, service: usize) { fn start_service(&mut self, service: usize) {
let service = &mut self.table.items[service]; let service = &mut self.table.services[service];
service.status = ServiceStatus::Running;
*service.status.lock().expect("service.status lock poisoned") = ServiceStatus::Running;
let stdout_send = service
.stdout_send
.lock()
.expect("stdout_send lock poisoned")
.take()
.expect("stdout_send has been stolen");
let mut cmd = Command::new("sh"); let mut cmd = Command::new("sh");
@ -117,8 +167,73 @@ impl App {
cmd.envs(service.env.iter()); cmd.envs(service.env.iter());
cmd.stdout(Stdio::piped()); cmd.stdout(Stdio::piped());
let child = cmd.spawn(); cmd.stderr(Stdio::piped());
cmd.stdin(Stdio::piped());
service.child = Some(child); let child = match cmd.spawn() {
Err(err) => {
stdout_send
.send(err.to_string().into_bytes())
.expect("failed to send stdout");
return;
}
Ok(child) => child,
};
let (tx, rx) = mpsc::channel();
self.thread_terminates.push(tx);
std::thread::spawn(move || match child_process_thread(child, stdout_send, rx) {
Ok(_) => {}
Err(e) => std::fs::write("error.txt", e.to_string()).unwrap(),
});
} }
} }
fn child_process_thread(
child: Child,
stdout_send: mpsc::Sender<Vec<u8>>,
terminate_channel: mpsc::Receiver<()>,
) -> io::Result<()> {
let mut child = child;
let mut stdout = child.stdout.take().unwrap();
loop {
match terminate_channel.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => break,
Err(TryRecvError::Empty) => {}
}
let mut stdout_buf = Vec::new();
match stdout.read(&mut stdout_buf) {
Ok(0) => continue,
Ok(_) => {
std::fs::write(
format!(
"debug/read_something_{}.txt",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
),
&stdout_buf,
)
.ok();
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
};
stdout_send
.send(stdout_buf)
.map_err(|_| io::Error::from(io::ErrorKind::Other))?;
}
match child.kill() {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::InvalidInput => {}
Err(e) => return Err(e),
}
Ok(())
}

View file

@ -8,17 +8,18 @@ use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
}; };
use std::io::StdoutLock; use std::io::StdoutLock;
use std::{env, error::Error, fs, io}; use std::{env, fs, io};
use tui::backend::CrosstermBackend; use tui::backend::CrosstermBackend;
use tui::Terminal; use tui::Terminal;
use crate::model::config::Config; use crate::model::config::Config;
use crate::model::App; use crate::model::App;
fn main() -> Result<(), Box<dyn Error>> { fn main() {
let file_path = env::args() let file_path = env::args()
.nth(1) .nth(1)
.or_else(|| env::var("SERVICE_MANAGER_CONFIG_PATH").ok()) .or_else(|| env::var("SERVICE_MANAGER_CONFIG_PATH").ok())
.or_else(|| Some("config.toml".to_string()))
.unwrap_or_else(|| { .unwrap_or_else(|| {
eprintln!( eprintln!(
"error: config file not found "error: config file not found
@ -41,26 +42,36 @@ or use the environment variable SERVICE_MANAGER_CONFIG_PATH"
let stdout = io::stdout(); let stdout = io::stdout();
let stdout = stdout.lock(); let stdout = stdout.lock();
let mut terminal = setup_terminal(stdout)?; let mut terminal = setup_terminal(stdout).expect("failed to setup terminal");
// create app and run it // create app and run it
let app = App::new(config); let app = App::new(config);
let res = controller::run_app(&mut terminal, app);
if let Ok(app) = app {
let res = controller::run_app(&mut terminal, app);
if let Err(err) = res {
println!("{:?}", err)
}
}
// restore terminal // restore terminal
disable_raw_mode()?;
execute!( if let Err(e) = disable_raw_mode() {
eprintln!("error: {}", e);
}
if let Err(e) = execute!(
terminal.backend_mut(), terminal.backend_mut(),
LeaveAlternateScreen, LeaveAlternateScreen,
DisableMouseCapture DisableMouseCapture
)?; ) {
terminal.show_cursor()?; eprintln!("error: {}", e);
if let Err(err) = res {
println!("{:?}", err)
} }
Ok(()) if let Err(e) = terminal.show_cursor() {
eprintln!("error: {}", e);
}
} }
fn setup_terminal(mut stdout: StdoutLock) -> io::Result<Terminal<CrosstermBackend<StdoutLock>>> { fn setup_terminal(mut stdout: StdoutLock) -> io::Result<Terminal<CrosstermBackend<StdoutLock>>> {

View file

@ -1,19 +1,20 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::io; use std::ffi::OsString;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Child; use std::sync::{mpsc, Mutex};
use tui::widgets::TableState; use tui::widgets::TableState;
#[derive(Debug)] #[derive(Debug)]
pub struct App { pub struct App {
pub table: AppState, pub table: AppState,
pub selected: Option<usize>, pub selected: Option<usize>,
pub thread_terminates: Vec<mpsc::Sender<()>>,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct AppState { pub struct AppState {
pub table_state: TableState, pub table_state: TableState,
pub items: Vec<Service>, pub services: Vec<Service>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -22,11 +23,14 @@ pub struct Service {
pub name: String, pub name: String,
pub workdir: PathBuf, pub workdir: PathBuf,
pub env: HashMap<String, String>, pub env: HashMap<String, String>,
pub status: ServiceStatus, pub status: Mutex<ServiceStatus>,
pub child: Option<io::Result<Child>>, pub stdout_buf: OsString,
pub stdout_recv: mpsc::Receiver<Vec<u8>>,
pub stdout_send: Mutex<Option<mpsc::Sender<Vec<u8>>>>,
} }
#[derive(Debug)] #[derive(Debug)]
#[allow(dead_code)]
pub enum ServiceStatus { pub enum ServiceStatus {
NotStarted, NotStarted,
Running, Running,
@ -37,7 +41,6 @@ pub enum ServiceStatus {
pub mod config { pub mod config {
use serde::Deserialize; use serde::Deserialize;
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::path::PathBuf; use std::path::PathBuf;
pub type Config = BTreeMap<String, Service>; pub type Config = BTreeMap<String, Service>;

View file

@ -25,12 +25,7 @@ pub fn render_ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
render_table(f, &mut app.table, chunks[0]); render_table(f, &mut app.table, chunks[0]);
} }
Some(index) => { Some(index) => {
let name = &app.table.items[index].name; render_full_view(f, &mut app.table, index, chunks[0]);
f.render_widget(
Block::default().borders(Borders::ALL).title(name.as_ref()),
chunks[0],
)
} }
} }
@ -39,6 +34,20 @@ pub fn render_ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
} }
} }
fn render_full_view<B: Backend>(f: &mut Frame<B>, state: &mut AppState, index: usize, area: Rect) {
let service = &state.services[index];
let block = Block::default()
.borders(Borders::ALL)
.title("service".as_ref());
let stdout = service.stdout_buf.to_string_lossy();
let paragraph = Paragraph::new(stdout.as_ref()).block(block);
f.render_widget(paragraph, area)
}
fn render_table<B: Backend>(f: &mut Frame<B>, state: &mut AppState, area: Rect) { fn render_table<B: Backend>(f: &mut Frame<B>, state: &mut AppState, area: Rect) {
let selected_style = Style::default().add_modifier(Modifier::REVERSED); let selected_style = Style::default().add_modifier(Modifier::REVERSED);
let normal_style = Style::default().bg(Color::Blue); let normal_style = Style::default().bg(Color::Blue);
@ -51,11 +60,18 @@ fn render_table<B: Backend>(f: &mut Frame<B>, state: &mut AppState, area: Rect)
.height(1) .height(1)
.bottom_margin(1); .bottom_margin(1);
let rows = state.items.iter().map(|service| { let rows = state.services.iter().map(|service| {
let height = service.name.chars().filter(|c| *c == '\n').count() + 1; let height = service.name.chars().filter(|c| *c == '\n').count() + 1;
let cells = [ let cells = [
Cell::from(service.name.as_ref()), Cell::from(service.name.as_ref()),
Cell::from(service.status.to_string()), Cell::from(
service
.status
.lock()
.expect("service.status lock poisoned")
.to_string(),
),
]; ];
Row::new(cells).height(height as u16).bottom_margin(1) Row::new(cells).height(height as u16).bottom_margin(1)
}); });