From 33f9374908942f4a3b90be227fad94ca353cf351 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 18 Jan 2023 01:58:12 +0000 Subject: [PATCH 01/10] chore: dependencies updated --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe0cded..8ec59a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -667,9 +667,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" dependencies = [ "unicode-ident", ] @@ -896,9 +896,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] @@ -949,9 +949,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.24.1" +version = "1.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" +checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" dependencies = [ "autocfg", "bytes", From 9a27d46a044452080144ee1367dc95886b10abf8 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 18 Jan 2023 01:58:34 +0000 Subject: [PATCH 02/10] chore: create_release.sh typos --- create_release.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/create_release.sh b/create_release.sh index 64e5193..f04c721 100755 --- a/create_release.sh +++ b/create_release.sh @@ -105,11 +105,11 @@ update_release_body_and_changelog () { # Update changelog to add links to commits [hex:8](url_with_full_commit) # "[aaaaaaaaaabbbbbbbbbbccccccccccddddddddd]" -> "[aaaaaaaa](https:/www.../commit/aaaaaaaaaabbbbbbbbbbccccccccccddddddddd)" - sed -i -E "s=(\s)\[([0-9a-f]{8})([0-9a-f]{32})\]= [\2](${GIT_REPO_URL}/commit/\2\3)=g" ./CHANGELOG.md + sed -i -E "s=(\s)\[([0-9a-f]{8})([0-9a-f]{32})\]= [\2](${GIT_REPO_URL}/commit/\2\3)=g" CHANGELOG.md - # Update changelog to add links to closed issues - comma included! - # "closes #1" -> "closes [#1](https:/www.../issues/1),"" - sed -i -r -E "s=closes \#([0-9]+)=closes [#\1](${GIT_REPO_URL}/issues/\1)=g" ./CHANGELOG.md + # Update changelog to add links to closed issues + # "closes #1" -> "closes [#1](https:/www.../issues/1)"" + sed -i -r -E "s=closes \#([0-9]+)=closes [#\1](${GIT_REPO_URL}/issues/\1)=g" CHANGELOG.md } # update version in cargo.toml, to match selected current version From 2d253f034182741d434e4bac12317f24221d0d4a Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 18 Jan 2023 01:58:53 +0000 Subject: [PATCH 03/10] chore: dev container post create install cross --- .devcontainer/devcontainer.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d4f7835..0efedc9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,6 +16,8 @@ "seccomp=unconfined" ], + "postCreateCommand": "cargo install cross", + "mounts": [ "source=/etc/timezone,target=/etc/timezone,type=bind,readonly" ], From 9dcd0509efeb464f58fb53d813bd78de2447949d Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 18 Jan 2023 02:01:01 +0000 Subject: [PATCH 04/10] refactor: derive Default for GuiState --- src/ui/gui_state.rs | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/ui/gui_state.rs b/src/ui/gui_state.rs index bc83f81..1c197ef 100644 --- a/src/ui/gui_state.rs +++ b/src/ui/gui_state.rs @@ -7,8 +7,9 @@ use uuid::Uuid; use crate::app_data::Header; -#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] +#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)] pub enum SelectablePanel { + #[default] Containers, Commands, Logs, @@ -124,8 +125,9 @@ impl BoxLocation { } /// State for the loading animation -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy)] pub enum Loading { + #[default] One, Two, Three, @@ -184,7 +186,7 @@ pub enum Status { } /// Global gui_state, stored in an Arc -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct GuiState { panel_map: HashMap, heading_map: HashMap, @@ -195,19 +197,6 @@ pub struct GuiState { pub info_box_text: Option, } impl GuiState { - /// Generate a default gui_state - pub fn default() -> Self { - Self { - panel_map: HashMap::new(), - heading_map: HashMap::new(), - loading_icon: Loading::One, - selected_panel: SelectablePanel::Containers, - is_loading: HashSet::new(), - info_box_text: None, - status: HashSet::new(), - } - } - /// Clear panels hash map, so on resize can fix the sizes for mouse clicks pub fn clear_area_map(&mut self) { self.panel_map.clear(); @@ -257,12 +246,12 @@ impl GuiState { status.iter().any(|i| self.status.contains(i)) } - /// Remove a gui_status into the current gui_status hashset + /// Remove a gui_status into the current gui_status HashSet pub fn status_del(&mut self, status: Status) { self.status.remove(&status); } - /// Insert a gui_status into the current gui_status hashset + /// Insert a gui_status into the current gui_status HashSet pub fn status_push(&mut self, status: Status) { self.status.insert(status); } @@ -277,7 +266,7 @@ impl GuiState { self.selected_panel = self.selected_panel.prev(); } - /// Insert a new loading_uuid into hashset, and advance the animation by one frame + /// Insert a new loading_uuid into HashSet, and advance the animation by one frame pub fn next_loading(&mut self, uuid: Uuid) { self.loading_icon = self.loading_icon.next(); self.is_loading.insert(uuid); @@ -292,7 +281,7 @@ impl GuiState { } } - /// Remove a loading_uuid from the is_loading hashset + /// Remove a loading_uuid from the is_loading HashSet pub fn remove_loading(&mut self, uuid: Uuid) { self.is_loading.remove(&uuid); } From 9ec43e124a62a80f4e78acba85fc3af5980ce260 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 18 Jan 2023 02:01:56 +0000 Subject: [PATCH 05/10] feat: spawn docker exec commands into own thread --- src/docker_data/mod.rs | 169 +++++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 83 deletions(-) diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 5daa6c7..c700654 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -3,7 +3,7 @@ use bollard::{ service::ContainerSummary, Docker, }; -use futures_util::{Future, StreamExt}; +use futures_util::StreamExt; use parking_lot::Mutex; use std::{ collections::HashMap, @@ -33,7 +33,7 @@ enum SpawnId { /// Cpu & Mem stats take twice as long as the update interval to get a value, so will have two being executed at the same time /// SpawnId::Stats takes container_id and binate value to enable both cycles of the same container_id to be inserted into the hashmap -/// Binate value is toggled when all join handles have been spawned off +/// Binate value is toggled when all handles have been spawned off /// Also effectively means that if the docker_update interval minimum will be 1000ms #[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] enum Binate { @@ -56,7 +56,6 @@ pub struct DockerData { binate: Binate, docker: Arc, gui_state: Arc>, - initialised: bool, is_running: Arc, receiver: Receiver, spawns: Arc>>>, @@ -94,12 +93,12 @@ impl DockerData { /// don't take &self, so that can tokio::spawn into it's own thread /// remove if from spawns hashmap when complete async fn update_container_stat( + app_data: Arc>, docker: Arc, id: ContainerId, - app_data: Arc>, is_running: bool, - spawns: Arc>>>, spawn_id: SpawnId, + spawns: Arc>>>, ) { let mut stream = docker .stats( @@ -156,18 +155,18 @@ impl DockerData { let docker = Arc::clone(&self.docker); let app_data = Arc::clone(&self.app_data); let spawns = Arc::clone(&self.spawns); - let spawn_key = SpawnId::Stats((id.clone(), self.binate)); + let spawn_id = SpawnId::Stats((id.clone(), self.binate)); self.spawns .lock() - .entry(spawn_key.clone()) + .entry(spawn_id.clone()) .or_insert_with(|| { tokio::spawn(Self::update_container_stat( + app_data, docker, id.clone(), - app_data, *is_running, + spawn_id, spawns, - spawn_key, )) }); } @@ -223,19 +222,17 @@ impl DockerData { } /// Update single container logs - /// don't take &self, so that can tokio::spawn into it's own thread - /// remove if from spawns hashmap when complete + /// remove it from spawns hashmap when complete async fn update_log( + app_data: Arc>, docker: Arc, id: ContainerId, - timestamps: bool, since: u64, - app_data: Arc>, spawns: Arc>>>, ) { let options = Some(LogsOptions:: { stdout: true, - timestamps, + timestamps: true, since: i64::try_from(since).unwrap_or_default(), ..Default::default() }); @@ -243,16 +240,14 @@ impl DockerData { let mut logs = docker.logs(id.get(), options); let mut output = vec![]; - while let Some(value) = logs.next().await { - if let Ok(data) = value { - let log_string = data.to_string(); - if !log_string.trim().is_empty() { - output.push(log_string); - } + while let Some(Ok(value)) = logs.next().await { + let data = value.to_string(); + if !data.trim().is_empty() { + output.push(data); } } spawns.lock().remove(&SpawnId::Log(id.clone())); - app_data.lock().update_log_by_id(&output, &id); + app_data.lock().update_log_by_id(output, &id); } /// Update all logs, spawn each container into own tokio::spawn thread @@ -264,14 +259,7 @@ impl DockerData { let key = SpawnId::Log(id.clone()); self.spawns.lock().insert( key, - tokio::spawn(Self::update_log( - docker, - id.clone(), - self.args.timestamp, - 0, - app_data, - spawns, - )), + tokio::spawn(Self::update_log(app_data, docker, id.clone(), 0, spawns)), ); } } @@ -290,11 +278,10 @@ impl DockerData { let app_data = Arc::clone(&self.app_data); let spawns = Arc::clone(&self.spawns); tokio::spawn(Self::update_log( + app_data, docker, container.id.clone(), - self.args.timestamp, container.last_updated, - app_data, spawns, )) }); @@ -305,8 +292,8 @@ impl DockerData { } /// Animate the loading icon - async fn loading_spin(&mut self, loading_uuid: Uuid) -> JoinHandle<()> { - let gui_state = Arc::clone(&self.gui_state); + async 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; @@ -316,89 +303,106 @@ impl DockerData { } /// Stop the loading_spin function, and reset gui loading status - fn stop_loading_spin(&mut self, handle: &JoinHandle<()>, loading_uuid: Uuid) { + fn stop_loading_spin( + gui_state: &Arc>, + handle: &JoinHandle<()>, + loading_uuid: Uuid, + ) { handle.abort(); - self.gui_state.lock().remove_loading(loading_uuid); + 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).await; + let loading_spin = Self::loading_spin(loading_uuid, &Arc::clone(&self.gui_state)).await; let all_ids = self.update_all_containers().await; self.update_all_container_stats(&all_ids); - // Maybe only do a single one at first? self.init_all_logs(&all_ids); - if all_ids.is_empty() { - self.initialised = true; - } - // wait until all logs have initialised - while !self.initialised { + while !self.app_data.lock().initialised(&all_ids) { tokio::time::sleep(std::time::Duration::from_millis(100)).await; - self.initialised = self.app_data.lock().initialised(&all_ids); } self.gui_state.lock().status_del(Status::Init); - self.stop_loading_spin(&loading_spin, loading_uuid); + Self::stop_loading_spin(&self.gui_state, &loading_spin, loading_uuid); } /// Set the global error as the docker error, and set gui_state to error - fn set_error(&mut self, error: DockerControls) { - self.app_data - .lock() - .set_error(AppError::DockerCommand(error)); - self.gui_state.lock().status_push(Status::Error); - } - - /// Execute a docker command, will start and stop the loading spinner, and set correct error - async fn exec_docker( - &mut self, - docker_fn: impl Future> + Send, - control: DockerControls, + fn set_error( + app_data: &Arc>, + error: DockerControls, + gui_state: &Arc>, ) { - let uuid = Uuid::new_v4(); - let loading_spin = self.loading_spin(uuid).await; - if docker_fn.await.is_err() { - self.set_error(control); - }; - self.stop_loading_spin(&loading_spin, uuid); + app_data.lock().set_error(AppError::DockerCommand(error)); + gui_state.lock().status_push(Status::Error); } /// Handle incoming messages, container controls & all container information update + /// Spawn dowcker commands off into own thread async fn message_handler(&mut self) { while let Some(message) = self.receiver.recv().await { let docker = Arc::clone(&self.docker); + let gui_state = Arc::clone(&self.gui_state); + let app_data = Arc::clone(&self.app_data); + let uuid = Uuid::new_v4(); match message { DockerMessage::Pause(id) => { - self.exec_docker(docker.pause_container(id.get()), DockerControls::Pause) - .await; + tokio::spawn(async move { + let loading_spin = Self::loading_spin(uuid, &gui_state).await; + 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); + }); + self.update_everything().await; } DockerMessage::Restart(id) => { - self.exec_docker( - docker.restart_container(id.get(), None), - DockerControls::Restart, - ) - .await; + tokio::spawn(async move { + let loading_spin = Self::loading_spin(uuid, &gui_state).await; + 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); + }); + self.update_everything().await; } DockerMessage::Start(id) => { - self.exec_docker( - docker.start_container(id.get(), None::>), - DockerControls::Start, - ) - .await; + tokio::spawn(async move { + let loading_spin = Self::loading_spin(uuid, &gui_state).await; + if docker + .start_container(id.get(), None::>) + .await + .is_err() + { + Self::set_error(&app_data, DockerControls::Start, &gui_state); + } + Self::stop_loading_spin(&gui_state, &loading_spin, uuid); + }); + self.update_everything().await; } DockerMessage::Stop(id) => { - self.exec_docker(docker.stop_container(id.get(), None), DockerControls::Stop) - .await; + tokio::spawn(async move { + let loading_spin = Self::loading_spin(uuid, &gui_state).await; + 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); + }); + self.update_everything().await; } DockerMessage::Unpause(id) => { - self.exec_docker(docker.unpause_container(id.get()), DockerControls::Unpause) - .await; + tokio::spawn(async move { + let loading_spin = Self::loading_spin(uuid, &gui_state).await; + 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); + }); self.update_everything().await; } DockerMessage::Update => self.update_everything().await, @@ -418,8 +422,8 @@ impl DockerData { pub async fn init( app_data: Arc>, docker: Docker, + docker_rx: Receiver, gui_state: Arc>, - receiver: Receiver, is_running: Arc, ) { let args = app_data.lock().args; @@ -427,13 +431,12 @@ impl DockerData { let mut inner = Self { app_data, args, + binate: Binate::One, docker: Arc::new(docker), gui_state, - initialised: false, - receiver, - spawns: Arc::new(Mutex::new(HashMap::new())), is_running, - binate: Binate::One, + receiver: docker_rx, + spawns: Arc::new(Mutex::new(HashMap::new())), }; inner.initialise_container_data().await; From 657ea2d751a71f05b17547b47c492d5676817336 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 18 Jan 2023 02:04:44 +0000 Subject: [PATCH 06/10] feat: Logs in own struct Store the logs, and timestamp into a hashset, so that won't push data into the vec if it's already in the hashset, close #11 --- src/app_data/container_state.rs | 100 +++++++++++++++++++++++++++++--- src/app_data/mod.rs | 51 +++++++++------- 2 files changed, 121 insertions(+), 30 deletions(-) diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index f759867..efa2d81 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -1,4 +1,8 @@ -use std::{cmp::Ordering, collections::VecDeque, fmt}; +use std::{ + cmp::Ordering, + collections::{HashSet, VecDeque}, + fmt, +}; use tui::{ style::Color, @@ -352,6 +356,87 @@ impl fmt::Display for ByteStats { pub type MemTuple = (Vec<(f64, f64)>, ByteStats, State); pub type CpuTuple = (Vec<(f64, f64)>, CpuStats, State); +/// Used to make sure that each log entry, for each container, is unique, +/// will only push a log entry into the logs vec if timetstamp of said log entry isn't in the hashset +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct LogsTz(String); + +/// The docker log, which should always contain a timestamp, is in the format `2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet` +/// So just split at the inclusive index of the first space, needs to be inclusive, hence the use of format to at the space, so that we can remove the whole thing when the `-t` flag is set +/// Need to make sure that this isn't an empty string?! +impl From<&String> for LogsTz { + fn from(value: &String) -> Self { + Self(value.split_inclusive(' ').take(1).collect::()) + } +} + +impl fmt::Display for LogsTz { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Store the logs alongside a HashSet, each log *should* generate a unique timestamp, +/// so if we store the timestamp seperately in a HashSet, we can then check if we should insert a log line into the +/// stateful list dependant on whethere the timestamp is in the HashSet or not +#[derive(Debug, Clone)] +pub struct Logs { + logs: StatefulList>, + tz: HashSet, +} + +impl Default for Logs { + fn default() -> Self { + let mut logs = StatefulList::new(vec![]); + logs.end(); + Self { + logs, + tz: HashSet::new(), + } + } +} + +impl Logs { + /// Only allow a new log line to be inserted if the log timestamp isn't in the tz HashSet + pub fn insert(&mut self, line: ListItem<'static>, tz: LogsTz) { + if self.tz.insert(tz) { + self.logs.items.push(line); + }; + } + + pub fn to_vec(&self) -> Vec> { + self.logs.items.clone() + } + + /// The rest of the methods are basically forwarding from the underlying StatefulList + pub fn get_state_title(&self) -> String { + self.logs.get_state_title() + } + + pub fn next(&mut self) { + self.logs.next(); + } + + pub fn previous(&mut self) { + self.logs.previous(); + } + + pub fn end(&mut self) { + self.logs.end(); + } + pub fn start(&mut self) { + self.logs.start(); + } + + pub fn len(&self) -> usize { + self.logs.items.len() + } + + pub fn state(&mut self) -> &mut ListState { + &mut self.logs.state + } +} + /// Info for each container #[derive(Debug, Clone)] pub struct ContainerItem { @@ -361,7 +446,7 @@ pub struct ContainerItem { pub id: ContainerId, pub image: String, pub last_updated: u64, - pub logs: StatefulList>, + pub logs: Logs, pub mem_limit: ByteStats, pub mem_stats: VecDeque, pub name: String, @@ -385,8 +470,6 @@ impl ContainerItem { ) -> Self { let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state)); docker_controls.start(); - let mut logs = StatefulList::new(vec![]); - logs.end(); Self { created, cpu_stats: VecDeque::with_capacity(60), @@ -395,7 +478,7 @@ impl ContainerItem { image, is_oxker, last_updated: 0, - logs, + logs: Logs::default(), mem_limit: ByteStats::default(), mem_stats: VecDeque::with_capacity(60), name, @@ -479,14 +562,13 @@ impl Columns { Self { state: (Header::State, 11), status: (Header::Status, 16), - // 7 to allow for "100.00%" cpu: (Header::Cpu, 7), - mem: (Header::Memory, 6, 6), + mem: (Header::Memory, 7, 7), id: (Header::Id, 8), name: (Header::Name, 4), image: (Header::Image, 5), - net_rx: (Header::Rx, 5), - net_tx: (Header::Tx, 5), + net_rx: (Header::Rx, 7), + net_tx: (Header::Tx, 7), } } } diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index 94e5850..fc106f4 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -12,7 +12,6 @@ pub use container_state::*; #[derive(Debug, Clone)] pub struct AppData { error: Option, - logs_parsed: bool, sorted_by: Option<(Header, SortedOrder)>, pub args: CliArgs, pub containers: StatefulList, @@ -62,7 +61,6 @@ impl AppData { args, containers: StatefulList::new(vec![]), error: None, - logs_parsed: false, sorted_by: None, } } @@ -193,7 +191,7 @@ impl AppData { /// Check if the selected container is a dockerised version of oxker /// So that can disallow commands to be send - /// Is a poor way of implementing this + /// Is a shabby way of implementing this pub fn selected_container_is_oxker(&self) -> bool { if let Some(index) = self.containers.state.selected() { if let Some(x) = self.containers.items.get(index) { @@ -352,7 +350,7 @@ impl AppData { .iter() .filter(|i| !i.cpu_stats.is_empty()) .count(); - self.logs_parsed && count_is_running == number_with_cpu_status + count_is_running == number_with_cpu_status } /// Just get the total number of containers @@ -382,8 +380,14 @@ impl AppData { let name_count = count(&container.name); let state_count = count(&container.state.to_string()); let status_count = count(&container.status); - let mem_current_count = count(&container.mem_stats.back().unwrap_or(&ByteStats::default()).to_string()); - let mem_limit_count= count(&container.mem_limit.to_string()); + let mem_current_count = count( + &container + .mem_stats + .back() + .unwrap_or(&ByteStats::default()) + .to_string(), + ); + let mem_limit_count = count(&container.mem_limit.to_string()); if cpu_count > output.cpu.1 { output.cpu.1 = cpu_count; @@ -394,7 +398,7 @@ impl AppData { if mem_current_count > output.mem.1 { output.mem.1 = mem_current_count; }; - if mem_limit_count > output.mem.2 { + if mem_limit_count > output.mem.2 { output.mem.2 = mem_limit_count; }; if name_count > output.name.1 { @@ -548,8 +552,8 @@ impl AppData { if item.image != image { item.image = image; }; - // else container not known, so make new ContainerItem and push into containers Vec } else { + // container not known, so make new ContainerItem and push into containers Vec let container = ContainerItem::new(created, id, image, is_oxker, name, state, status); self.containers.items.push(container); @@ -559,34 +563,39 @@ impl AppData { } /// update logs of a given container, based on id - pub fn update_log_by_id(&mut self, output: &[String], id: &ContainerId) { - let tz = Self::get_systemtime(); + pub fn update_log_by_id(&mut self, output: Vec, id: &ContainerId) { let color = self.args.color; let raw = self.args.raw; - if let Some(container) = self.get_container_by_id(id) { - container.last_updated = tz; - let current_len = container.logs.items.len(); + let timestamp = self.args.timestamp; - for i in output { + if let Some(container) = self.get_container_by_id(id) { + container.last_updated = Self::get_systemtime(); + let current_len = container.logs.len(); + + for mut i in output { + let tz = LogsTz::from(&i); + // Strip the timestamp if `-t` flag set + if !timestamp { + i = i.replace(&tz.to_string(), ""); + } let lines = if color { - log_sanitizer::colorize_logs(i) + log_sanitizer::colorize_logs(&i) } else if raw { - log_sanitizer::raw(i) + log_sanitizer::raw(&i) } else { - log_sanitizer::remove_ansi(i) + log_sanitizer::remove_ansi(&i) }; - container.logs.items.push(ListItem::new(lines)); + 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 + if container.logs.state().selected().is_none() + || container.logs.state().selected().map_or(1, |f| f + 1) == current_len { container.logs.end(); } } - self.logs_parsed = true; } } From 97b89349dc2de275ca514a1e6420255a63d775e8 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 18 Jan 2023 02:05:00 +0000 Subject: [PATCH 07/10] refactor: main.rs tidy up --- src/main.rs | 105 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 38 deletions(-) diff --git a/src/main.rs b/src/main.rs index 996b0f8..4e107cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,13 @@ #![forbid(unsafe_code)] #![warn(clippy::unused_async, clippy::unwrap_used, clippy::expect_used)] // Wanring - These are indeed pedantic -// #![warn(clippy::pedantic)] -// #![warn(clippy::nursery)] -// #![allow(clippy::module_name_repetitions, clippy::doc_markdown, clippy::similar_names)] - +#![warn(clippy::pedantic)] +#![warn(clippy::nursery)] +#![allow( + clippy::module_name_repetitions, + clippy::doc_markdown, + clippy::similar_names +)] // Only allow when debugging // #![allow(unused)] @@ -12,9 +15,11 @@ use app_data::AppData; use app_error::AppError; use bollard::Docker; use docker_data::DockerData; +use input_handler::InputMessages; use parking_lot::Mutex; use parse_args::CliArgs; use std::sync::{atomic::AtomicBool, Arc}; +use tokio::sync::mpsc::{Receiver, Sender}; use tracing::{info, Level}; mod app_data; @@ -26,11 +31,58 @@ mod ui; use ui::{create_ui, GuiState, Status}; +use crate::docker_data::DockerMessage; + const ENTRY_POINT: &str = "./start_oxker.sh"; +// write to file if `-g` is set? fn setup_tracing() { tracing_subscriber::fmt().with_max_level(Level::INFO).init(); - // TODO write to file? +} + +// 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>, + docker_rx: Receiver, + gui_state: &Arc>, + is_running: &Arc, +) { + if let Ok(docker) = Docker::connect_with_socket_defaults() { + if docker.ping().await.is_ok() { + let app_data = Arc::clone(&app_data); + let gui_state = Arc::clone(&gui_state); + let is_running = Arc::clone(&is_running); + tokio::spawn(DockerData::init( + app_data, docker, docker_rx, gui_state, is_running, + )); + } else { + app_data.lock().set_error(AppError::DockerConnect); + gui_state.lock().status_push(Status::DockerConnect); + } + } else { + app_data.lock().set_error(AppError::DockerConnect); + gui_state.lock().status_push(Status::DockerConnect); + } +} + +/// Create data for, and then spawn a tokio thread, for the input handler +async fn handler_init( + app_data: &Arc>, + docker_sx: &Sender, + gui_state: &Arc>, + input_rx: Receiver, + is_running: &Arc, +) { + let input_app_data = Arc::clone(&app_data); + let input_gui_state = Arc::clone(&gui_state); + let input_is_running = Arc::clone(&is_running); + tokio::spawn(input_handler::InputHandler::init( + input_app_data, + input_rx, + docker_sx.clone(), + input_gui_state, + input_is_running, + )); } #[tokio::main] @@ -43,46 +95,23 @@ async fn main() { let (docker_sx, docker_rx) = tokio::sync::mpsc::channel(16); let (input_sx, input_rx) = tokio::sync::mpsc::channel(16); - // Create docker daemon handler, and only spawn up the docker data handler if ping returns non-error - if let Ok(docker) = Docker::connect_with_socket_defaults() { - if docker.ping().await.is_ok() { - let app_data = Arc::clone(&app_data); - let gui_state = Arc::clone(&gui_state); - let is_running = Arc::clone(&is_running); - tokio::spawn(DockerData::init( - app_data, docker, gui_state, docker_rx, is_running, - )); - } else { - app_data.lock().set_error(AppError::DockerConnect); - gui_state.lock().status_push(Status::DockerConnect); - } - } else { - app_data.lock().set_error(AppError::DockerConnect); - gui_state.lock().status_push(Status::DockerConnect); - } + docker_init(&app_data, docker_rx, &gui_state, &is_running).await; - let input_app_data = Arc::clone(&app_data); - let input_gui_state = Arc::clone(&gui_state); - let input_is_running = Arc::clone(&is_running); - // Spawn input handling into own tokio thread - tokio::spawn(input_handler::InputHandler::init( - input_app_data, - input_rx, - docker_sx.clone(), - input_gui_state, - input_is_running, - )); + handler_init(&app_data, &docker_sx, &gui_state, input_rx, &is_running).await; if args.gui { - create_ui(app_data, input_sx, is_running, gui_state, docker_sx) + create_ui(app_data, docker_sx, gui_state, is_running, input_sx) .await .unwrap_or(()); } else { - // Debug mode for testing, mostly pointless, doesn't take terminal nor draw gui - // TODO this needs to be improved to display something actually useful + // Debug mode for testing, mostly pointless, doesn't take terminal + info!("in debug mode"); loop { - info!("in debug mode"); - tokio::time::sleep(std::time::Duration::from_millis(5000)).await; + docker_sx.send(DockerMessage::Update).await.unwrap_or(()); + tokio::time::sleep(std::time::Duration::from_millis(u64::from( + args.docker_interval, + ))) + .await; } } } From 51c580010a24de2427373795803936d498dc8cee Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 18 Jan 2023 02:06:05 +0000 Subject: [PATCH 08/10] fix: memory column aligned, closes #20 --- src/ui/draw_blocks.rs | 47 +++++++++++++++++++++------------------- src/ui/mod.rs | 50 +++++++++++++++++++------------------------ 2 files changed, 47 insertions(+), 50 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 14c060b..6f48b2e 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -55,19 +55,22 @@ fn generate_block<'a>( .lock() .update_heading_map(Region::Panel(panel), area); let current_selected_panel = gui_state.lock().selected_panel; - let title = match panel { + let mut title = match panel { SelectablePanel::Containers => { format!( - " {} {} ", + "{} {}", panel.title(), app_data.lock().containers.get_state_title() ) } SelectablePanel::Logs => { - format!(" {} {} ", panel.title(), app_data.lock().get_log_title()) + format!("{} {}", panel.title(), app_data.lock().get_log_title()) } SelectablePanel::Commands => String::new(), }; + if !title.is_empty() { + title = format!(" {title} "); + } let mut block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) @@ -136,19 +139,21 @@ pub fn containers( let state_style = Style::default().fg(i.state.get_color()); let blue = Style::default().fg(Color::Blue); - // let mems = format!( - // "{:>1} / {:>1}", - // i.mem_stats.back().unwrap_or(&ByteStats::default()), - // i.mem_limit - // ); - let lines = Spans::from(vec![ Span::styled( - format!("{:width$}", i.status, width = &widths.status.1.into()), + format!( + "{MARGIN}{:>width$}", + i.status, + width = &widths.status.1.into() + ), state_style, ), Span::styled( @@ -161,7 +166,13 @@ pub fn containers( state_style, ), Span::styled( - format!("{MARGIN}{:>width_current$} / {:>width_limit$}", i.mem_stats.back().unwrap_or(&ByteStats::default()), i.mem_limit, width_current = &widths.mem.1.into(), width_limit = &widths.mem.2.into()), + format!( + "{MARGIN}{:>width_current$} / {:>width_limit$}", + i.mem_stats.back().unwrap_or(&ByteStats::default()), + i.mem_limit, + width_current = &widths.mem.1.into(), + width_limit = &widths.mem.2.into() + ), state_style, ), Span::styled( @@ -226,22 +237,14 @@ pub fn logs( .alignment(Alignment::Center); f.render_widget(paragraph, area); } else if let Some(index) = index { - let items = app_data.lock().containers.items[index] - .logs - .items - .iter() - .enumerate() - .map(|i| i.1.clone()) - .collect::>(); - - let items = List::new(items) + let items = List::new(app_data.lock().containers.items[index].logs.to_vec()) .block(block) .highlight_symbol(ARROW) .highlight_style(Style::default().add_modifier(Modifier::BOLD)); f.render_stateful_widget( items, area, - &mut app_data.lock().containers.items[index].logs.state, + app_data.lock().containers.items[index].logs.state(), ); } else { let paragraph = Paragraph::new("no logs found") diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 40d917d..d46ce7d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,7 +6,7 @@ use crossterm::{ }; use parking_lot::Mutex; use std::{ - io, + io::{self, Write}, sync::{atomic::Ordering, Arc}, }; use std::{sync::atomic::AtomicBool, time::Instant}; @@ -31,11 +31,10 @@ use crate::{ /// Take control of the terminal in order to draw gui pub async fn create_ui( app_data: Arc>, - sender: Sender, - is_running: Arc, - gui_state: Arc>, docker_sx: Sender, - // update_duration: Duration, + gui_state: Arc>, + is_running: Arc, + sender: Sender, ) -> Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); @@ -44,16 +43,14 @@ pub async fn create_ui( let mut terminal = Terminal::new(backend)?; let res = run_app( - &mut terminal, app_data, - sender, - is_running, - gui_state, docker_sx, + gui_state, + is_running, + sender, + &mut terminal, ) .await; - terminal.clear()?; - disable_raw_mode()?; execute!( terminal.backend_mut(), @@ -65,46 +62,43 @@ pub async fn create_ui( if let Err(err) = res { println!("{err}"); } + std::io::stdout().flush().unwrap_or(()); Ok(()) } /// Run a loop to draw the gui async fn run_app( - terminal: &mut Terminal, app_data: Arc>, - sender: Sender, - is_running: Arc, - gui_state: Arc>, docker_sx: Sender, + gui_state: Arc>, + is_running: Arc, + sender: Sender, + terminal: &mut Terminal, ) -> Result<(), AppError> { let update_duration = std::time::Duration::from_millis(u64::from(app_data.lock().args.docker_interval)); let input_poll_rate = std::time::Duration::from_millis(75); let status_dockerconnect = gui_state.lock().status_contains(&[Status::DockerConnect]); + let mut now = Instant::now(); if status_dockerconnect { let mut seconds = 5; loop { if seconds < 1 { - is_running.store(false, Ordering::Relaxed); break; } + if now.elapsed() >= std::time::Duration::from_secs(1) { + seconds -= 1; + now = Instant::now(); + } if terminal .draw(|f| draw_blocks::error(f, AppError::DockerConnect, Some(seconds))) .is_err() { return Err(AppError::Terminal); } - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - seconds -= 1; } } else { - let mut now = Instant::now(); - loop { - if terminal.draw(|f| ui(f, &app_data, &gui_state)).is_err() { - return Err(AppError::Terminal); - } - // TODO could only draw if in gui mode, that way all inputs & docker commands will run, and can just trace!("{event"}) all over the place - // refactor this into own function, so can be called without drawing to the terminal + while is_running.load(Ordering::Relaxed) { if crossterm::event::poll(input_poll_rate).unwrap_or(false) { if let Ok(event) = event::read() { if let Event::Key(key) = event { @@ -128,12 +122,12 @@ async fn run_app( docker_sx.send(DockerMessage::Update).await.unwrap_or(()); now = Instant::now(); } - - if !is_running.load(Ordering::Relaxed) { - break; + if terminal.draw(|f| ui(f, &app_data, &gui_state)).is_err() { + return Err(AppError::Terminal); } } } + terminal.clear().unwrap_or(()); Ok(()) } From fb400517a7c6281bcddda88b5c8c6570f02107c1 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 18 Jan 2023 02:14:02 +0000 Subject: [PATCH 09/10] docs: changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5219847..8152a5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +### Chores ++ dependencies updated, [8cd199db49186fad6ce432bb277e3a10f0a08d34], [d880b829c123dbe57deccadef97810e45c083737], [66d57c99558ca14d9593d6dbfd5b0e8e5d59055d], [33f9374908942f4a3b90be227fad94ca353cf351] ++ create_release.sh typos, [9a27d46a044452080144ee1367dc95886b10abf8] ++ dev container post create install cross, [2d253f034182741d434e4bac12317f24221d0d4a] + +### Features ++ Logs in own struct, [657ea2d751a71f05b17547b47c492d5676817336] ++ spawn docker exec commands into own thread, [9ec43e124a62a80f4e78acba85fc3af5980ce260] ++ align memory columns correctly, closes #20, [bd7dfcd2c512a527d66a1388f90006988a487186], [51c580010a24de2427373795803936d498dc8cee] + +### Refactors ++ main.rs tidy up, [97b89349dc2de275ca514a1e6420255a63d775e8] ++ derive Default for GuiState, [9dcd0509efeb464f58fb53d813bd78de2447949d] ++ param reduction, AtomicBool to Relaxed, [0350293de3c00c6e5e5d787b7596bb3413d1cda1] + # v0.1.11 ### 2023-01-03 From bb26734039226d1987aaabd222251a6eb04db871 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Wed, 18 Jan 2023 02:14:40 +0000 Subject: [PATCH 10/10] docs: changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8152a5a..bf03bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ### Features + Logs in own struct, [657ea2d751a71f05b17547b47c492d5676817336] + spawn docker exec commands into own thread, [9ec43e124a62a80f4e78acba85fc3af5980ce260] -+ align memory columns correctly, closes #20, [bd7dfcd2c512a527d66a1388f90006988a487186], [51c580010a24de2427373795803936d498dc8cee] ++ align memory columns correctly, minimum byte display value now `0.00 kB`, rather than `0 B`closes #20, [bd7dfcd2c512a527d66a1388f90006988a487186], [51c580010a24de2427373795803936d498dc8cee] ### Refactors + main.rs tidy up, [97b89349dc2de275ca514a1e6420255a63d775e8]