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]
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::{Event, KeyCode};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::ffi::{OsStr, OsString};
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::widgets::TableState;
use tui::Terminal;
@ -15,11 +19,13 @@ pub fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Resu
loop {
terminal.draw(|f| view::render_ui(f, &mut app))?;
app.recv_stdouts();
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') => match app.selected {
Some(_) => app.leave_service(),
None => return Ok(()),
None => break,
},
KeyCode::Char('r') => app.run_service(),
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 {
pub fn new(config: Config) -> App {
App {
pub fn new(config: Config) -> io::Result<App> {
Ok(App {
table: AppState {
table_state: TableState::default(),
items: config
services: config
.into_iter()
.map(|(name, service)| Service {
command: service.command,
name,
workdir: service
.workdir
.unwrap_or_else(|| std::env::current_dir().unwrap()),
env: service.env.unwrap_or_else(HashMap::new),
status: ServiceStatus::NotStarted,
child: None,
.map(|(name, service)| -> io::Result<Service> {
let (stdout_send, stdout_recv) = mpsc::channel();
Ok(Service {
command: service.command,
name,
workdir: service
.workdir
.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,
}
thread_terminates: Vec::new(),
})
}
pub fn is_table(&self) -> bool {
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) {
if self.is_table() {
if let Some(selected) = self.table.table_state.selected() {
@ -74,7 +116,7 @@ impl App {
fn next(&mut self) {
let i = match self.table.table_state.selected() {
Some(i) => {
if i >= self.table.items.len() - 1 {
if i >= self.table.services.len() - 1 {
0
} else {
i + 1
@ -89,7 +131,7 @@ impl App {
let i = match self.table.table_state.selected() {
Some(i) => {
if i == 0 {
self.table.items.len() - 1
self.table.services.len() - 1
} else {
i - 1
}
@ -108,8 +150,16 @@ impl App {
}
fn start_service(&mut self, service: usize) {
let service = &mut self.table.items[service];
service.status = ServiceStatus::Running;
let service = &mut self.table.services[service];
*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");
@ -117,8 +167,73 @@ impl App {
cmd.envs(service.env.iter());
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,
};
use std::io::StdoutLock;
use std::{env, error::Error, fs, io};
use std::{env, fs, io};
use tui::backend::CrosstermBackend;
use tui::Terminal;
use crate::model::config::Config;
use crate::model::App;
fn main() -> Result<(), Box<dyn Error>> {
fn main() {
let file_path = env::args()
.nth(1)
.or_else(|| env::var("SERVICE_MANAGER_CONFIG_PATH").ok())
.or_else(|| Some("config.toml".to_string()))
.unwrap_or_else(|| {
eprintln!(
"error: config file not found
@ -41,26 +42,36 @@ or use the environment variable SERVICE_MANAGER_CONFIG_PATH"
let stdout = io::stdout();
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
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
disable_raw_mode()?;
execute!(
if let Err(e) = disable_raw_mode() {
eprintln!("error: {}", e);
}
if let Err(e) = execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{:?}", err)
) {
eprintln!("error: {}", e);
}
Ok(())
if let Err(e) = terminal.show_cursor() {
eprintln!("error: {}", e);
}
}
fn setup_terminal(mut stdout: StdoutLock) -> io::Result<Terminal<CrosstermBackend<StdoutLock>>> {

View file

@ -1,19 +1,20 @@
use std::collections::HashMap;
use std::io;
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Child;
use std::sync::{mpsc, Mutex};
use tui::widgets::TableState;
#[derive(Debug)]
pub struct App {
pub table: AppState,
pub selected: Option<usize>,
pub thread_terminates: Vec<mpsc::Sender<()>>,
}
#[derive(Debug)]
pub struct AppState {
pub table_state: TableState,
pub items: Vec<Service>,
pub services: Vec<Service>,
}
#[derive(Debug)]
@ -22,11 +23,14 @@ pub struct Service {
pub name: String,
pub workdir: PathBuf,
pub env: HashMap<String, String>,
pub status: ServiceStatus,
pub child: Option<io::Result<Child>>,
pub status: Mutex<ServiceStatus>,
pub stdout_buf: OsString,
pub stdout_recv: mpsc::Receiver<Vec<u8>>,
pub stdout_send: Mutex<Option<mpsc::Sender<Vec<u8>>>>,
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum ServiceStatus {
NotStarted,
Running,
@ -37,7 +41,6 @@ pub enum ServiceStatus {
pub mod config {
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::path::PathBuf;
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]);
}
Some(index) => {
let name = &app.table.items[index].name;
f.render_widget(
Block::default().borders(Borders::ALL).title(name.as_ref()),
chunks[0],
)
render_full_view(f, &mut app.table, index, 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) {
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
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)
.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 cells = [
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)
});