mirror of
https://github.com/Noratrieb/service-manager.git
synced 2026-01-14 16:35:05 +01:00
do things that don't work
This commit is contained in:
parent
6d572fc9d0
commit
2a2759c020
6 changed files with 207 additions and 51 deletions
|
|
@ -1 +1,3 @@
|
||||||
# service-manager
|
# service-manager
|
||||||
|
|
||||||
|
unix only for now, rip windows
|
||||||
11
config.toml
11
config.toml
|
|
@ -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"
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
35
src/main.rs
35
src/main.rs
|
|
@ -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>>> {
|
||||||
|
|
|
||||||
15
src/model.rs
15
src/model.rs
|
|
@ -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>;
|
||||||
|
|
|
||||||
32
src/view.rs
32
src/view.rs
|
|
@ -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)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue