From c8077bca0b673478cfbb417e677a885136ba9eff Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:38:15 +0000 Subject: [PATCH] feat: Docker exec mode, closes #28 --- README.md | 1 + containerised/Dockerfile | 22 ++- containerised/Dockerfile_dev | 22 ++- docker-compose.yml | 6 +- src/app_data/mod.rs | 50 +++--- src/app_error.rs | 2 + src/docker_data/mod.rs | 58 +++---- src/input_handler/mod.rs | 323 ++++++++++++++++++++--------------- src/main.rs | 32 +--- src/parse_args.rs | 35 +++- src/ui/draw_blocks.rs | 11 +- src/ui/gui_state.rs | 37 +++- src/ui/mod.rs | 70 ++++++-- 13 files changed, 397 insertions(+), 272 deletions(-) diff --git a/README.md b/README.md index 6c0140e..aaffc2e 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ In application controls | ```( enter )```| execute selected docker command| | ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column | | ```( 0 )``` | stop sorting | +| ```( e )``` | (attempt) to exec into the selected container | | ```( h )``` | toggle help menu | | ```( m )``` | toggle mouse capture - if disabled, text on screen can be selected| | ```( q )``` | to quit at any time | diff --git a/containerised/Dockerfile b/containerised/Dockerfile index d3f255d..99242b6 100644 --- a/containerised/Dockerfile +++ b/containerised/Dockerfile @@ -45,18 +45,32 @@ RUN cargo build --release --target $(cat /.platform) RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker / +################ +## MUSL SETUP ## +################ + +FROM alpine:3.18 as MUSL_SETUP + +RUN apk add --update --no-cache docker-cli upx + +# Compress the docker executable, to reduce final image size +RUN upx -9 /usr/bin/docker + ############# ## Runtime ## ############# -FROM scratch AS RUNTIME +FROM alpine:3.18 as RUNTIME -# Set an ENV that we're running in a container, so that the application can sleep for 250ms at start +# Set an ENV to indicate that we're running in a container ENV OXKER_RUNTIME=container -# Copy application binary from builder image COPY --from=BUILDER /oxker /app/ +COPY --from=MUSL_SETUP /usr/bin/docker /usr/bin/ + +# remove sh and busybox, probably pointless +RUN rm /bin/sh /bin/busybox # Run the application -# this is used in the application itself, to stop itself from listing itself, so DO NOT EDIT +# this is used in the application itself so DO NOT EDIT ENTRYPOINT [ "/app/oxker"] diff --git a/containerised/Dockerfile_dev b/containerised/Dockerfile_dev index 6ff45ba..0a3118c 100644 --- a/containerised/Dockerfile_dev +++ b/containerised/Dockerfile_dev @@ -1,12 +1,28 @@ +################ +## MUSL SETUP ## +################ + +FROM alpine:3.18 as MUSL_SETUP + +RUN apk add --update --no-cache docker-cli upx + +# Copy application binary from builder image +RUN upx -9 /usr/bin/docker + ############# ## Runtime ## ############# -FROM scratch -# Set env that we're running in a container, so that the application can sleep for 250ms at start +FROM alpine:3.18 as RUNTIME + +# Set an ENV that we're running in a container, so that the application can sleep for 250ms at start ENV OXKER_RUNTIME=container -# Copy application binary from builder image +COPY --from=MUSL_SETUP /usr/bin/docker /usr/bin/ + + +RUN rm /bin/sh /bin/busybox + COPY ./target/x86_64-unknown-linux-musl/release/oxker /app/ # Run the application diff --git a/docker-compose.yml b/docker-compose.yml index cbf627b..0521dea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: deploy: resources: limits: - memory: 128M + memory: 1024M redis: image: redis:alpine3.18 container_name: redis @@ -27,7 +27,7 @@ services: deploy: resources: limits: - memory: 16M + memory: 384M rabbitmq: image: rabbitmq:3 container_name: rabbitmq @@ -38,6 +38,6 @@ services: deploy: resources: limits: - memory: 256M + memory: 512M diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 6a851a9..30a44e7 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -563,6 +563,8 @@ impl AppData { }) }); + let id = ContainerId::from(id.as_str()); + let is_oxker = i .command .as_ref() @@ -579,8 +581,6 @@ impl AppData { .as_ref() .map_or(String::new(), std::clone::Clone::clone); - let id = ContainerId::from(id.as_str()); - let created = i .created .map_or(0, |i| u64::try_from(i).unwrap_or_default()); @@ -624,31 +624,33 @@ impl AppData { let timestamp = self.args.timestamp; if let Some(container) = self.get_container_by_id(id) { - container.last_updated = Self::get_systemtime(); - let current_len = container.logs.len(); + if !container.is_oxker { + container.last_updated = Self::get_systemtime(); + let current_len = container.logs.len(); - for mut i in logs { - let tz = LogsTz::from(i.as_str()); - // Strip the timestamp if `-t` flag set - if !timestamp { - i = i.replace(&tz.to_string(), ""); + for mut i in logs { + let tz = LogsTz::from(i.as_str()); + // Strip the timestamp if `-t` flag set + if !timestamp { + i = i.replace(&tz.to_string(), ""); + } + let lines = if color { + log_sanitizer::colorize_logs(&i) + } else if raw { + log_sanitizer::raw(&i) + } else { + log_sanitizer::remove_ansi(&i) + }; + container.logs.insert(ListItem::new(lines), tz); } - let lines = if color { - log_sanitizer::colorize_logs(&i) - } else if raw { - log_sanitizer::raw(&i) - } else { - log_sanitizer::remove_ansi(&i) - }; - container.logs.insert(ListItem::new(lines), tz); - } - // Set the logs selected row for each container - // Either when no long currently selected, or currently selected (before updated) is already at end - if container.logs.state().selected().is_none() - || container.logs.state().selected().map_or(1, |f| f + 1) == current_len - { - container.logs.end(); + // Set the logs selected row for each container + // Either when no long currently selected, or currently selected (before updated) is already at end + if container.logs.state().selected().is_none() + || container.logs.state().selected().map_or(1, |f| f + 1) == current_len + { + container.logs.end(); + } } } } diff --git a/src/app_error.rs b/src/app_error.rs index f580b06..5a9a9aa 100644 --- a/src/app_error.rs +++ b/src/app_error.rs @@ -6,6 +6,7 @@ use std::fmt; #[derive(Debug, Clone, Copy)] pub enum AppError { DockerCommand(DockerControls), + DockerExec, DockerConnect, DockerInterval, InputPoll, @@ -18,6 +19,7 @@ impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::DockerCommand(s) => write!(f, "Unable to {s} container"), + Self::DockerExec => write!(f, "Unable to exec into container"), Self::DockerConnect => write!(f, "Unable to access docker daemon"), Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"), Self::InputPoll => write!(f, "Unable to poll user input"), diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 1540597..0f69d97 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -54,7 +54,6 @@ pub struct DockerData { app_data: Arc>, args: CliArgs, binate: Binate, - containerised: bool, docker: Arc, gui_state: Arc>, is_running: Arc, @@ -101,6 +100,7 @@ impl DockerData { spawn_id: SpawnId, spawns: Arc>>>, ) { + let mut stream = docker .stats( id.get(), @@ -191,7 +191,7 @@ impl DockerData { .into_iter() .filter_map(|f| match f.id { Some(_) => { - if self.containerised + if self.args.in_container && f.command .as_ref() .map_or(false, |c| c.starts_with(ENTRY_POINT)) @@ -286,32 +286,12 @@ impl DockerData { self.app_data.lock().sort_containers(); } - /// Animate the loading icon - fn loading_spin(loading_uuid: Uuid, gui_state: &Arc>) -> JoinHandle<()> { - let gui_state = Arc::clone(gui_state); - tokio::spawn(async move { - loop { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - gui_state.lock().next_loading(loading_uuid); - } - }) - } - - /// Stop the loading_spin function, and reset gui loading status - fn stop_loading_spin( - gui_state: &Arc>, - handle: &JoinHandle<()>, - loading_uuid: Uuid, - ) { - handle.abort(); - gui_state.lock().remove_loading(loading_uuid); - } - /// Initialize docker container data, before any messages are received async fn initialise_container_data(&mut self) { self.gui_state.lock().status_push(Status::Init); let loading_uuid = Uuid::new_v4(); - let loading_spin = Self::loading_spin(loading_uuid, &Arc::clone(&self.gui_state)); + let loading_handle = GuiState::start_loading_animation(&self.gui_state, loading_uuid); + // let handle = self.gui_state.lock().st let all_ids = self.update_all_containers().await; @@ -323,7 +303,9 @@ impl DockerData { while !self.app_data.lock().initialised(&all_ids) { tokio::time::sleep(std::time::Duration::from_millis(100)).await; } - Self::stop_loading_spin(&self.gui_state, &loading_spin, loading_uuid); + self.gui_state + .lock() + .stop_loading_animation(&loading_handle, loading_uuid); self.gui_state.lock().status_del(Status::Init); } @@ -350,27 +332,27 @@ impl DockerData { match message { DockerMessage::Pause(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker.pause_container(id.get()).await.is_err() { Self::set_error(&app_data, DockerControls::Pause, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; } DockerMessage::Restart(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker.restart_container(id.get(), None).await.is_err() { Self::set_error(&app_data, DockerControls::Restart, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; } DockerMessage::Start(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker .start_container(id.get(), None::>) .await @@ -378,33 +360,33 @@ impl DockerData { { Self::set_error(&app_data, DockerControls::Start, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; } DockerMessage::Stop(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker.stop_container(id.get(), None).await.is_err() { Self::set_error(&app_data, DockerControls::Stop, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; } DockerMessage::Unpause(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker.unpause_container(id.get()).await.is_err() { Self::set_error(&app_data, DockerControls::Unpause, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; } DockerMessage::Delete(id) => { tokio::spawn(async move { - let loading_spin = Self::loading_spin(uuid, &gui_state); + let handle = GuiState::start_loading_animation(&gui_state, uuid); if docker .remove_container( id.get(), @@ -419,7 +401,7 @@ impl DockerData { { Self::set_error(&app_data, DockerControls::Stop, &gui_state); } - Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + gui_state.lock().stop_loading_animation(&handle, uuid); }); self.update_everything().await; self.gui_state.lock().set_delete_container(None); @@ -443,7 +425,6 @@ impl DockerData { /// Initialise self, and start the message receiving loop pub async fn init( app_data: Arc>, - containerised: bool, docker: Docker, docker_rx: Receiver, gui_state: Arc>, @@ -453,7 +434,6 @@ impl DockerData { if app_data.lock().get_error().is_none() { let mut inner = Self { app_data, - containerised, args, binate: Binate::One, docker: Arc::new(docker), diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index c05164c..8e9ba2b 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -13,17 +13,20 @@ use tokio::{ sync::mpsc::{Receiver, Sender}, task::JoinHandle, }; +use uuid::Uuid; mod message; use crate::{ app_data::{AppData, DockerControls, Header}, app_error::AppError, docker_data::DockerMessage, - ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui}, + ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui, DOCKER_COMMAND}, value_capture, }; pub use message::InputMessages; +const OCI_ERROR: &str = "OCI runtime exec failed"; + /// Handle all input events #[derive(Debug)] pub struct InputHandler { @@ -161,6 +164,42 @@ impl InputHandler { self.gui_state.lock().set_delete_container(None); } + /// Validate that one can exec into a Docker container + fn e_key(&self) { + let is_oxker = self.app_data.lock().is_oxker(); + if !is_oxker { + let uuid = Uuid::new_v4(); + let handle = GuiState::start_loading_animation(&self.gui_state, uuid); + let mut exec_err = Some(()); + + let id = self.app_data.lock().get_selected_container_id(); + + if let Some(id) = id { + if let Ok(output) = std::process::Command::new(DOCKER_COMMAND) + .args(["exec", id.get(), "pwd"]) + .output() + { + if let Ok(output) = String::from_utf8(output.stdout) { + if !output.starts_with(OCI_ERROR) { + exec_err = None; + } + } + } + + if exec_err.is_some() { + self.app_data.lock().set_error( + AppError::DockerExec, + &self.gui_state, + Status::Error, + ); + } else { + self.gui_state.lock().status_push(Status::Exec); + } + } + self.gui_state.lock().stop_loading_animation(&handle, uuid); + } + } + /// Handle any keyboard button events #[allow(clippy::too_many_lines)] async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) { @@ -171,153 +210,161 @@ impl InputHandler { .status_contains(&[Status::DeleteConfirm]) ); - value_capture!( - contains_error, - self.gui_state.lock().status_contains(&[Status::Error]) - ); - value_capture!( - contains_help, - self.gui_state.lock().status_contains(&[Status::Help]) - ); + let contains = |s: Status| self.gui_state.lock().status_contains(&[s]); - // Always just quit on Ctrl + c/C or q/Q - let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C'); - let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q'); - if key_modififer == KeyModifiers::CONTROL && is_c() || is_q() { - self.quit().await; - } + let contains_error = contains(Status::Error); + let contains_help = contains(Status::Help); + let contains_exec = contains(Status::Exec); - if contains_error { - if let KeyCode::Char('c' | 'C') = key_code { - self.app_data.lock().remove_error(); - self.gui_state.lock().status_del(Status::Error); + if !contains_exec { + // Always just quit on Ctrl + c/C or q/Q + let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C'); + let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q'); + if key_modififer == KeyModifiers::CONTROL && is_c() || is_q() { + self.quit().await; } - } else if contains_help { - match key_code { - KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help), - KeyCode::Char('m' | 'M') => self.m_key(), - _ => (), - } - } else if contains_delete { - match key_code { - KeyCode::Char('y' | 'Y') => self.confirm_delete().await, - KeyCode::Char('n' | 'N') => self.clear_delete(), - _ => (), - } - } else { - match key_code { - KeyCode::Char('0') => self.app_data.lock().reset_sorted(), - KeyCode::Char('1') => self.sort(Header::State), - KeyCode::Char('2') => self.sort(Header::Status), - KeyCode::Char('3') => self.sort(Header::Cpu), - KeyCode::Char('4') => self.sort(Header::Memory), - KeyCode::Char('5') => self.sort(Header::Id), - KeyCode::Char('6') => self.sort(Header::Name), - KeyCode::Char('7') => self.sort(Header::Image), - KeyCode::Char('8') => self.sort(Header::Rx), - KeyCode::Char('9') => self.sort(Header::Tx), - KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help), - KeyCode::Char('m' | 'M') => self.m_key(), - KeyCode::Tab => { - // Skip control panel if no containers, could be refactored - let is_containers = - self.gui_state.lock().selected_panel == SelectablePanel::Containers; - let count = if self.app_data.lock().get_container_len() == 0 && is_containers { - 2 - } else { - 1 - }; - for _ in 0..count { - self.gui_state.lock().next_panel(); - } - } - KeyCode::BackTab => { - // Skip control panel if no containers, could be refactored - let is_containers = - self.gui_state.lock().selected_panel == SelectablePanel::Logs; - let count = if self.app_data.lock().get_container_len() == 0 && is_containers { - 2 - } else { - 1 - }; - for _ in 0..count { - self.gui_state.lock().previous_panel(); - } - } - KeyCode::Home => { - let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; - match selected_panel { - SelectablePanel::Containers => locked_data.containers_start(), - SelectablePanel::Logs => locked_data.log_start(), - SelectablePanel::Commands => locked_data.docker_command_start(), - } - } - KeyCode::End => { - let mut locked_data = self.app_data.lock(); - let selected_panel = self.gui_state.lock().selected_panel; - match selected_panel { - SelectablePanel::Containers => locked_data.containers_end(), - SelectablePanel::Logs => locked_data.log_end(), - SelectablePanel::Commands => locked_data.docker_command_end(), - } - } - KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(), - KeyCode::PageUp => { - for _ in 0..=6 { - self.previous(); - } - } - KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(), - KeyCode::PageDown => { - for _ in 0..=6 { - self.next(); - } - } - KeyCode::Enter => { - // This isn't great, just means you can't send docker commands before full initialization of the program - let panel = self.gui_state.lock().selected_panel; - if panel == SelectablePanel::Commands { - let option_command = self.app_data.lock().selected_docker_command(); - if let Some(command) = option_command { - let option_id = self.app_data.lock().get_selected_container_id(); - // Poor way of disallowing commands to be sent to a containerised okxer - if self.app_data.lock().is_oxker() { - return; + if contains_error { + if let KeyCode::Char('c' | 'C') = key_code { + self.app_data.lock().remove_error(); + self.gui_state.lock().status_del(Status::Error); + } + } else if contains_help { + match key_code { + KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help), + KeyCode::Char('m' | 'M') => self.m_key(), + _ => (), + } + } else if contains_delete { + match key_code { + KeyCode::Char('y' | 'Y') => self.confirm_delete().await, + KeyCode::Char('n' | 'N') => self.clear_delete(), + _ => (), + } + } else { + match key_code { + KeyCode::Char('0') => self.app_data.lock().reset_sorted(), + KeyCode::Char('1') => self.sort(Header::State), + KeyCode::Char('2') => self.sort(Header::Status), + KeyCode::Char('3') => self.sort(Header::Cpu), + KeyCode::Char('4') => self.sort(Header::Memory), + KeyCode::Char('5') => self.sort(Header::Id), + KeyCode::Char('6') => self.sort(Header::Name), + KeyCode::Char('7') => self.sort(Header::Image), + KeyCode::Char('8') => self.sort(Header::Rx), + KeyCode::Char('9') => self.sort(Header::Tx), + KeyCode::Char('e' | 'E') => self.e_key(), + KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help), + KeyCode::Char('m' | 'M') => self.m_key(), + KeyCode::Tab => { + // Skip control panel if no containers, could be refactored + let is_containers = + self.gui_state.lock().selected_panel == SelectablePanel::Containers; + let count = + if self.app_data.lock().get_container_len() == 0 && is_containers { + 2 + } else { + 1 }; - if let Some(id) = option_id { - match command { - DockerControls::Delete => self - .docker_sender - .send(DockerMessage::ConfirmDelete(id)) - .await - .ok(), - DockerControls::Pause => { - self.docker_sender.send(DockerMessage::Pause(id)).await.ok() - } - DockerControls::Unpause => self - .docker_sender - .send(DockerMessage::Unpause(id)) - .await - .ok(), - DockerControls::Start => { - self.docker_sender.send(DockerMessage::Start(id)).await.ok() - } - DockerControls::Stop => { - self.docker_sender.send(DockerMessage::Stop(id)).await.ok() - } - DockerControls::Restart => self - .docker_sender - .send(DockerMessage::Restart(id)) - .await - .ok(), + for _ in 0..count { + self.gui_state.lock().next_panel(); + } + } + KeyCode::BackTab => { + // Skip control panel if no containers, could be refactored + let is_containers = + self.gui_state.lock().selected_panel == SelectablePanel::Logs; + let count = + if self.app_data.lock().get_container_len() == 0 && is_containers { + 2 + } else { + 1 + }; + for _ in 0..count { + self.gui_state.lock().previous_panel(); + } + } + KeyCode::Home => { + let mut locked_data = self.app_data.lock(); + let selected_panel = self.gui_state.lock().selected_panel; + match selected_panel { + SelectablePanel::Containers => locked_data.containers_start(), + SelectablePanel::Logs => locked_data.log_start(), + SelectablePanel::Commands => locked_data.docker_command_start(), + } + } + KeyCode::End => { + let mut locked_data = self.app_data.lock(); + let selected_panel = self.gui_state.lock().selected_panel; + match selected_panel { + SelectablePanel::Containers => locked_data.containers_end(), + SelectablePanel::Logs => locked_data.log_end(), + SelectablePanel::Commands => locked_data.docker_command_end(), + } + } + KeyCode::Up | KeyCode::Char('k' | 'K') => self.previous(), + KeyCode::PageUp => { + for _ in 0..=6 { + self.previous(); + } + } + KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(), + KeyCode::PageDown => { + for _ in 0..=6 { + self.next(); + } + } + KeyCode::Enter => { + // This isn't great, just means you can't send docker commands before full initialization of the program + let panel = self.gui_state.lock().selected_panel; + if panel == SelectablePanel::Commands { + let option_command = self.app_data.lock().selected_docker_command(); + + if let Some(command) = option_command { + // Poor way of disallowing commands to be sent to a containerised okxer + if self.app_data.lock().is_oxker() { + return; }; + let option_id = self.app_data.lock().get_selected_container_id(); + if let Some(id) = option_id { + match command { + DockerControls::Delete => self + .docker_sender + .send(DockerMessage::ConfirmDelete(id)) + .await + .ok(), + DockerControls::Pause => self + .docker_sender + .send(DockerMessage::Pause(id)) + .await + .ok(), + DockerControls::Unpause => self + .docker_sender + .send(DockerMessage::Unpause(id)) + .await + .ok(), + DockerControls::Start => self + .docker_sender + .send(DockerMessage::Start(id)) + .await + .ok(), + DockerControls::Stop => self + .docker_sender + .send(DockerMessage::Stop(id)) + .await + .ok(), + DockerControls::Restart => self + .docker_sender + .send(DockerMessage::Restart(id)) + .await + .ok(), + }; + } } } } + _ => (), } - _ => (), } } } diff --git a/src/main.rs b/src/main.rs index ef3e279..bad3e04 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,18 +55,6 @@ fn setup_tracing() { tracing_subscriber::fmt().with_max_level(Level::INFO).init(); } -/// An ENV is set in the ./containerised/Dockerfile, if this is ENV found, then sleep for 250ms, else the container, for as yet unknown reasons, will close immediately -/// returns a bool, so that the `update_all_containers()` won't bother to check the entry point unless running via a container -fn check_if_containerised() -> bool { - if let Ok(value) = std::env::var(ENV_KEY) { - if value == ENV_VALUE { - std::thread::sleep(std::time::Duration::from_millis(250)); - return true; - } - } - false -} - /// Read the optional docker_host path, the cli args take priority over the DOCKER_HOST env fn read_docker_host(args: &CliArgs) -> Option { args.host @@ -77,7 +65,6 @@ fn read_docker_host(args: &CliArgs) -> Option { /// Create docker daemon handler, and only spawn up the docker data handler if a ping returns non-error async fn docker_init( app_data: &Arc>, - containerised: bool, docker_rx: Receiver, gui_state: &Arc>, is_running: &Arc, @@ -93,12 +80,7 @@ async fn docker_init( let gui_state = Arc::clone(gui_state); let is_running = Arc::clone(is_running); tokio::spawn(DockerData::init( - app_data, - containerised, - docker, - docker_rx, - gui_state, - is_running, + app_data, docker, docker_rx, gui_state, is_running, )); } else { app_data @@ -134,8 +116,6 @@ fn handler_init( #[tokio::main] async fn main() { - let containerised = check_if_containerised(); - setup_tracing(); let args = CliArgs::new(); @@ -146,15 +126,7 @@ async fn main() { let is_running = Arc::new(AtomicBool::new(true)); let (docker_sx, docker_rx) = tokio::sync::mpsc::channel(32); - docker_init( - &app_data, - containerised, - docker_rx, - &gui_state, - &is_running, - host, - ) - .await; + docker_init(&app_data, docker_rx, &gui_state, &is_running, host).await; if args.gui { let (input_sx, input_rx) = tokio::sync::mpsc::channel(32); diff --git a/src/parse_args.rs b/src/parse_args.rs index 7a69053..dc4f8a8 100644 --- a/src/parse_args.rs +++ b/src/parse_args.rs @@ -3,10 +3,12 @@ use std::process; use clap::Parser; use tracing::error; +use crate::{ENV_KEY, ENV_VALUE}; + #[derive(Parser, Debug, Clone)] #[allow(clippy::struct_excessive_bools)] #[command(version, about)] -pub struct CliArgs { +pub struct Args { /// Docker update interval in ms, minimum effectively 1000 #[clap(short = 'd', value_name = "ms", default_value_t = 1000)] pub docker_interval: u32, @@ -36,10 +38,34 @@ pub struct CliArgs { pub gui: bool, } +#[derive(Debug, Clone)] +#[allow(clippy::struct_excessive_bools)] +pub struct CliArgs { + pub in_container: bool, + pub color: bool, + pub docker_interval: u32, + pub gui: bool, + pub host: Option, + pub raw: bool, + pub show_self: bool, + pub timestamp: bool, +} + impl CliArgs { + /// An ENV is set in the ./containerised/Dockerfile, if this is ENV found, then sleep for 250ms, else the container, for as yet unknown reasons, will close immediately + /// returns a bool, so that the `update_all_containers()` won't bother to check the entry point unless running via a container + fn check_if_in_container() -> bool { + if let Ok(value) = std::env::var(ENV_KEY) { + if value == ENV_VALUE { + return true; + } + } + false + } + /// Parse cli arguments pub fn new() -> Self { - let args = Self::parse(); + let args = Args::parse(); // Quit the program if the docker update argument is 0 // Should maybe change it to check if less than 100 @@ -50,10 +76,11 @@ impl CliArgs { Self { color: args.color, docker_interval: args.docker_interval, - host: args.host, gui: !args.gui, - show_self: !args.show_self, + host: args.host, + in_container: Self::check_if_in_container(), raw: args.raw, + show_self: !args.show_self, timestamp: !args.timestamp, } } diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 26a9800..8d227de 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -585,6 +585,11 @@ impl HelpInfo { space(), button_item("enter"), button_desc("to send docker container command"), + ]), + Line::from(vec![ + space(), + button_item("e"), + button_desc("exec into a container"), ]), Line::from(vec![ space(), @@ -724,11 +729,7 @@ pub fn help_box(f: &mut Frame) { /// Draw the delete confirm box in the centre of the screen /// take in container id and container name here? -pub fn delete_confirm( - f: &mut Frame, - gui_state: &Arc>, - name: &str, -) { +pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &str) { let block = Block::default() .title(" Confirm Delete ") .border_type(BorderType::Rounded) diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index 18e92f5..967aff9 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -1,5 +1,10 @@ +use parking_lot::Mutex; use ratatui::layout::{Constraint, Rect}; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; +use tokio::task::JoinHandle; use uuid::Uuid; use crate::app_data::{ContainerId, Header}; @@ -150,11 +155,12 @@ const FRAMES_LEN: u8 = 9; /// Various functions (e.g input handler), operate differently depending upon current Status #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum Status { - Init, - Help, - DockerConnect, + Exec, DeleteConfirm, + DockerConnect, Error, + Help, + Init, } /// Global gui_state, stored in an Arc @@ -296,13 +302,34 @@ impl GuiState { } /// Remove a loading_uuid from the is_loading HashSet, if empty, reset loading_index to 0 - pub fn remove_loading(&mut self, uuid: Uuid) { + fn remove_loading(&mut self, uuid: Uuid) { self.is_loading.remove(&uuid); if self.is_loading.is_empty() { self.loading_index = 0; } } + /// Animate the loading icon in its own Tokio thread + pub fn start_loading_animation( + gui_state: &Arc>, + loading_uuid: Uuid, + ) -> JoinHandle<()> { + gui_state.lock().next_loading(loading_uuid); + let gui_state = Arc::clone(gui_state); + tokio::spawn(async move { + loop { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + gui_state.lock().next_loading(loading_uuid); + } + }) + } + + /// Stop the loading_spin function, and reset gui loading status + pub fn stop_loading_animation(&mut self, handle: &JoinHandle<()>, loading_uuid: Uuid) { + handle.abort(); + self.remove_loading(loading_uuid); + } + /// Set info box content pub fn set_info_box(&mut self, text: &str) { self.info_box_text = Some(text.to_owned()); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 005b1b3..7d81228 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -27,10 +27,13 @@ pub use self::color_match::*; pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; use crate::{ app_data::AppData, app_error::AppError, docker_data::DockerMessage, - input_handler::InputMessages, + input_handler::InputMessages, parse_args::CliArgs, }; +pub const DOCKER_COMMAND: &str = "docker"; + pub struct Ui { + args: CliArgs, app_data: Arc>, docker_sx: Sender, gui_state: Arc>, @@ -63,7 +66,9 @@ impl Ui { sender: Sender, ) { if let Ok(terminal) = Self::setup_terminal() { + let args = app_data.lock().args.clone(); let mut ui = Self { + args, app_data, docker_sx, gui_state, @@ -86,19 +91,17 @@ impl Ui { /// Setup the terminal for full-screen drawing mode, with mouse capture fn setup_terminal() -> Result>> { - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen)?; - Self::enable_mouse_capture()?; + let stdout = Self::init_terminal()?; let backend = CrosstermBackend::new(stdout); Ok(Terminal::new(backend)?) } - /// This is a fix for mouse-events being printed to screen, read an event and do nothing with it - fn nullify_event_read(&self) { - if crossterm::event::poll(self.input_poll_rate).unwrap_or(true) { - event::read().ok(); - } + fn init_terminal() -> Result { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + Self::enable_mouse_capture()?; + Ok(stdout) } /// reset the terminal back to default settings @@ -137,12 +140,48 @@ impl Ui { Ok(()) } + /// Use exeternal docker cli to exec into a container + fn exec(&mut self) { + let id = self.app_data.lock().get_selected_container_id(); + + if let Some(id) = id { + // if Self::can_exec(&id).is_some() { + if let Ok(mut child) = std::process::Command::new(DOCKER_COMMAND) + .args(["exec", "-it", id.get(), "sh"]) + .stdin(std::process::Stdio::inherit()) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .spawn() + { + self.reset_terminal().ok(); + child.wait().ok(); + if child.kill().is_err() { + std::process::exit(1) + } + // } + } + } + + self.terminal.clear().ok(); + self.reset_terminal().ok(); + Self::init_terminal().ok(); + self.gui_state.lock().status_del(Status::Exec); + } + /// The loop for drawing the main UI to the terminal async fn gui_loop(&mut self) -> Result<(), AppError> { let update_duration = - std::time::Duration::from_millis(u64::from(self.app_data.lock().args.docker_interval)); + std::time::Duration::from_millis(u64::from(self.args.docker_interval)); while self.is_running.load(Ordering::SeqCst) { + let exec = self.gui_state.lock().status_contains(&[Status::Exec]); + + if exec { + self.exec(); + self.docker_sx.send(DockerMessage::Update).await.ok(); + continue; + } + if self .terminal .draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state)) @@ -150,6 +189,7 @@ impl Ui { { return Err(AppError::Terminal); } + if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) { if let Ok(event) = event::read() { if let Event::Key(key) = event { @@ -173,6 +213,7 @@ impl Ui { } } + // Should this be done in the docker thread instead? if self.now.elapsed() >= update_duration { self.docker_sx.send(DockerMessage::Update).await.ok(); self.now = Instant::now(); @@ -192,7 +233,6 @@ impl Ui { } else { self.gui_loop().await?; } - self.nullify_event_read(); Ok(()) } } @@ -208,11 +248,7 @@ macro_rules! value_capture { /// Draw the main ui to a frame of the terminal /// TODO add a single line area for debug message - if not in release mode? -fn draw_frame( - f: &mut Frame, - app_data: &Arc>, - gui_state: &Arc>, -) { +fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>) { value_capture!(height, app_data.lock().get_container_len()); value_capture!(column_widths, app_data.lock().get_width()); value_capture!(has_containers, app_data.lock().get_container_len() > 0);