feat: Docker exec mode, closes #28

This commit is contained in:
Jack Wills
2023-11-14 12:38:15 +00:00
parent e1998c9fca
commit c8077bca0b
13 changed files with 397 additions and 272 deletions
+1
View File
@@ -97,6 +97,7 @@ In application controls
| ```( enter )```| execute selected docker command| | ```( enter )```| execute selected docker command|
| ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column | | ```( 1-9 )``` | sort containers by heading, clicking on headings also sorts the selected column |
| ```( 0 )``` | stop sorting | | ```( 0 )``` | stop sorting |
| ```( e )``` | (attempt) to exec into the selected container |
| ```( h )``` | toggle help menu | | ```( h )``` | toggle help menu |
| ```( m )``` | toggle mouse capture - if disabled, text on screen can be selected| | ```( m )``` | toggle mouse capture - if disabled, text on screen can be selected|
| ```( q )``` | to quit at any time | | ```( q )``` | to quit at any time |
+18 -4
View File
@@ -45,18 +45,32 @@ RUN cargo build --release --target $(cat /.platform)
RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker / 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 ## ## 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 ENV OXKER_RUNTIME=container
# Copy application binary from builder image
COPY --from=BUILDER /oxker /app/ 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 # 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"] ENTRYPOINT [ "/app/oxker"]
+19 -3
View File
@@ -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 ## ## 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 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/ COPY ./target/x86_64-unknown-linux-musl/release/oxker /app/
# Run the application # Run the application
+3 -3
View File
@@ -16,7 +16,7 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
memory: 128M memory: 1024M
redis: redis:
image: redis:alpine3.18 image: redis:alpine3.18
container_name: redis container_name: redis
@@ -27,7 +27,7 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
memory: 16M memory: 384M
rabbitmq: rabbitmq:
image: rabbitmq:3 image: rabbitmq:3
container_name: rabbitmq container_name: rabbitmq
@@ -38,6 +38,6 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
memory: 256M memory: 512M
+4 -2
View File
@@ -563,6 +563,8 @@ impl AppData {
}) })
}); });
let id = ContainerId::from(id.as_str());
let is_oxker = i let is_oxker = i
.command .command
.as_ref() .as_ref()
@@ -579,8 +581,6 @@ impl AppData {
.as_ref() .as_ref()
.map_or(String::new(), std::clone::Clone::clone); .map_or(String::new(), std::clone::Clone::clone);
let id = ContainerId::from(id.as_str());
let created = i let created = i
.created .created
.map_or(0, |i| u64::try_from(i).unwrap_or_default()); .map_or(0, |i| u64::try_from(i).unwrap_or_default());
@@ -624,6 +624,7 @@ impl AppData {
let timestamp = self.args.timestamp; let timestamp = self.args.timestamp;
if let Some(container) = self.get_container_by_id(id) { if let Some(container) = self.get_container_by_id(id) {
if !container.is_oxker {
container.last_updated = Self::get_systemtime(); container.last_updated = Self::get_systemtime();
let current_len = container.logs.len(); let current_len = container.logs.len();
@@ -653,3 +654,4 @@ impl AppData {
} }
} }
} }
}
+2
View File
@@ -6,6 +6,7 @@ use std::fmt;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum AppError { pub enum AppError {
DockerCommand(DockerControls), DockerCommand(DockerControls),
DockerExec,
DockerConnect, DockerConnect,
DockerInterval, DockerInterval,
InputPoll, InputPoll,
@@ -18,6 +19,7 @@ impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
Self::DockerCommand(s) => write!(f, "Unable to {s} container"), 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::DockerConnect => write!(f, "Unable to access docker daemon"),
Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"), Self::DockerInterval => write!(f, "Docker update interval needs to be greater than 0"),
Self::InputPoll => write!(f, "Unable to poll user input"), Self::InputPoll => write!(f, "Unable to poll user input"),
+19 -39
View File
@@ -54,7 +54,6 @@ pub struct DockerData {
app_data: Arc<Mutex<AppData>>, app_data: Arc<Mutex<AppData>>,
args: CliArgs, args: CliArgs,
binate: Binate, binate: Binate,
containerised: bool,
docker: Arc<Docker>, docker: Arc<Docker>,
gui_state: Arc<Mutex<GuiState>>, gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>, is_running: Arc<AtomicBool>,
@@ -101,6 +100,7 @@ impl DockerData {
spawn_id: SpawnId, spawn_id: SpawnId,
spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>, spawns: Arc<Mutex<HashMap<SpawnId, JoinHandle<()>>>>,
) { ) {
let mut stream = docker let mut stream = docker
.stats( .stats(
id.get(), id.get(),
@@ -191,7 +191,7 @@ impl DockerData {
.into_iter() .into_iter()
.filter_map(|f| match f.id { .filter_map(|f| match f.id {
Some(_) => { Some(_) => {
if self.containerised if self.args.in_container
&& f.command && f.command
.as_ref() .as_ref()
.map_or(false, |c| c.starts_with(ENTRY_POINT)) .map_or(false, |c| c.starts_with(ENTRY_POINT))
@@ -286,32 +286,12 @@ impl DockerData {
self.app_data.lock().sort_containers(); self.app_data.lock().sort_containers();
} }
/// Animate the loading icon
fn loading_spin(loading_uuid: Uuid, gui_state: &Arc<Mutex<GuiState>>) -> 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<Mutex<GuiState>>,
handle: &JoinHandle<()>,
loading_uuid: Uuid,
) {
handle.abort();
gui_state.lock().remove_loading(loading_uuid);
}
/// Initialize docker container data, before any messages are received /// Initialize docker container data, before any messages are received
async fn initialise_container_data(&mut self) { async fn initialise_container_data(&mut self) {
self.gui_state.lock().status_push(Status::Init); self.gui_state.lock().status_push(Status::Init);
let loading_uuid = Uuid::new_v4(); 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; let all_ids = self.update_all_containers().await;
@@ -323,7 +303,9 @@ impl DockerData {
while !self.app_data.lock().initialised(&all_ids) { while !self.app_data.lock().initialised(&all_ids) {
tokio::time::sleep(std::time::Duration::from_millis(100)).await; 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); self.gui_state.lock().status_del(Status::Init);
} }
@@ -350,27 +332,27 @@ impl DockerData {
match message { match message {
DockerMessage::Pause(id) => { DockerMessage::Pause(id) => {
tokio::spawn(async move { 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() { if docker.pause_container(id.get()).await.is_err() {
Self::set_error(&app_data, DockerControls::Pause, &gui_state); 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; self.update_everything().await;
} }
DockerMessage::Restart(id) => { DockerMessage::Restart(id) => {
tokio::spawn(async move { 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() { if docker.restart_container(id.get(), None).await.is_err() {
Self::set_error(&app_data, DockerControls::Restart, &gui_state); 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; self.update_everything().await;
} }
DockerMessage::Start(id) => { DockerMessage::Start(id) => {
tokio::spawn(async move { tokio::spawn(async move {
let loading_spin = Self::loading_spin(uuid, &gui_state); let handle = GuiState::start_loading_animation(&gui_state, uuid);
if docker if docker
.start_container(id.get(), None::<StartContainerOptions<String>>) .start_container(id.get(), None::<StartContainerOptions<String>>)
.await .await
@@ -378,33 +360,33 @@ impl DockerData {
{ {
Self::set_error(&app_data, DockerControls::Start, &gui_state); 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; self.update_everything().await;
} }
DockerMessage::Stop(id) => { DockerMessage::Stop(id) => {
tokio::spawn(async move { 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() { if docker.stop_container(id.get(), None).await.is_err() {
Self::set_error(&app_data, DockerControls::Stop, &gui_state); 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.update_everything().await;
} }
DockerMessage::Unpause(id) => { DockerMessage::Unpause(id) => {
tokio::spawn(async move { 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() { if docker.unpause_container(id.get()).await.is_err() {
Self::set_error(&app_data, DockerControls::Unpause, &gui_state); 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; self.update_everything().await;
} }
DockerMessage::Delete(id) => { DockerMessage::Delete(id) => {
tokio::spawn(async move { tokio::spawn(async move {
let loading_spin = Self::loading_spin(uuid, &gui_state); let handle = GuiState::start_loading_animation(&gui_state, uuid);
if docker if docker
.remove_container( .remove_container(
id.get(), id.get(),
@@ -419,7 +401,7 @@ impl DockerData {
{ {
Self::set_error(&app_data, DockerControls::Stop, &gui_state); 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.update_everything().await;
self.gui_state.lock().set_delete_container(None); self.gui_state.lock().set_delete_container(None);
@@ -443,7 +425,6 @@ impl DockerData {
/// Initialise self, and start the message receiving loop /// Initialise self, and start the message receiving loop
pub async fn init( pub async fn init(
app_data: Arc<Mutex<AppData>>, app_data: Arc<Mutex<AppData>>,
containerised: bool,
docker: Docker, docker: Docker,
docker_rx: Receiver<DockerMessage>, docker_rx: Receiver<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>, gui_state: Arc<Mutex<GuiState>>,
@@ -453,7 +434,6 @@ impl DockerData {
if app_data.lock().get_error().is_none() { if app_data.lock().get_error().is_none() {
let mut inner = Self { let mut inner = Self {
app_data, app_data,
containerised,
args, args,
binate: Binate::One, binate: Binate::One,
docker: Arc::new(docker), docker: Arc::new(docker),
+68 -21
View File
@@ -13,17 +13,20 @@ use tokio::{
sync::mpsc::{Receiver, Sender}, sync::mpsc::{Receiver, Sender},
task::JoinHandle, task::JoinHandle,
}; };
use uuid::Uuid;
mod message; mod message;
use crate::{ use crate::{
app_data::{AppData, DockerControls, Header}, app_data::{AppData, DockerControls, Header},
app_error::AppError, app_error::AppError,
docker_data::DockerMessage, docker_data::DockerMessage,
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui}, ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui, DOCKER_COMMAND},
value_capture, value_capture,
}; };
pub use message::InputMessages; pub use message::InputMessages;
const OCI_ERROR: &str = "OCI runtime exec failed";
/// Handle all input events /// Handle all input events
#[derive(Debug)] #[derive(Debug)]
pub struct InputHandler { pub struct InputHandler {
@@ -161,6 +164,42 @@ impl InputHandler {
self.gui_state.lock().set_delete_container(None); 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 /// Handle any keyboard button events
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) { async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) {
@@ -171,15 +210,13 @@ impl InputHandler {
.status_contains(&[Status::DeleteConfirm]) .status_contains(&[Status::DeleteConfirm])
); );
value_capture!( let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
contains_error,
self.gui_state.lock().status_contains(&[Status::Error])
);
value_capture!(
contains_help,
self.gui_state.lock().status_contains(&[Status::Help])
);
let contains_error = contains(Status::Error);
let contains_help = contains(Status::Help);
let contains_exec = contains(Status::Exec);
if !contains_exec {
// Always just quit on Ctrl + c/C or q/Q // 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_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q'); let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
@@ -216,13 +253,15 @@ impl InputHandler {
KeyCode::Char('7') => self.sort(Header::Image), KeyCode::Char('7') => self.sort(Header::Image),
KeyCode::Char('8') => self.sort(Header::Rx), KeyCode::Char('8') => self.sort(Header::Rx),
KeyCode::Char('9') => self.sort(Header::Tx), 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('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
KeyCode::Char('m' | 'M') => self.m_key(), KeyCode::Char('m' | 'M') => self.m_key(),
KeyCode::Tab => { KeyCode::Tab => {
// Skip control panel if no containers, could be refactored // Skip control panel if no containers, could be refactored
let is_containers = let is_containers =
self.gui_state.lock().selected_panel == SelectablePanel::Containers; self.gui_state.lock().selected_panel == SelectablePanel::Containers;
let count = if self.app_data.lock().get_container_len() == 0 && is_containers { let count =
if self.app_data.lock().get_container_len() == 0 && is_containers {
2 2
} else { } else {
1 1
@@ -235,7 +274,8 @@ impl InputHandler {
// Skip control panel if no containers, could be refactored // Skip control panel if no containers, could be refactored
let is_containers = let is_containers =
self.gui_state.lock().selected_panel == SelectablePanel::Logs; self.gui_state.lock().selected_panel == SelectablePanel::Logs;
let count = if self.app_data.lock().get_container_len() == 0 && is_containers { let count =
if self.app_data.lock().get_container_len() == 0 && is_containers {
2 2
} else { } else {
1 1
@@ -281,11 +321,11 @@ impl InputHandler {
let option_command = self.app_data.lock().selected_docker_command(); let option_command = self.app_data.lock().selected_docker_command();
if let Some(command) = option_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 // Poor way of disallowing commands to be sent to a containerised okxer
if self.app_data.lock().is_oxker() { if self.app_data.lock().is_oxker() {
return; return;
}; };
let option_id = self.app_data.lock().get_selected_container_id();
if let Some(id) = option_id { if let Some(id) = option_id {
match command { match command {
DockerControls::Delete => self DockerControls::Delete => self
@@ -293,20 +333,26 @@ impl InputHandler {
.send(DockerMessage::ConfirmDelete(id)) .send(DockerMessage::ConfirmDelete(id))
.await .await
.ok(), .ok(),
DockerControls::Pause => { DockerControls::Pause => self
self.docker_sender.send(DockerMessage::Pause(id)).await.ok() .docker_sender
} .send(DockerMessage::Pause(id))
.await
.ok(),
DockerControls::Unpause => self DockerControls::Unpause => self
.docker_sender .docker_sender
.send(DockerMessage::Unpause(id)) .send(DockerMessage::Unpause(id))
.await .await
.ok(), .ok(),
DockerControls::Start => { DockerControls::Start => self
self.docker_sender.send(DockerMessage::Start(id)).await.ok() .docker_sender
} .send(DockerMessage::Start(id))
DockerControls::Stop => { .await
self.docker_sender.send(DockerMessage::Stop(id)).await.ok() .ok(),
} DockerControls::Stop => self
.docker_sender
.send(DockerMessage::Stop(id))
.await
.ok(),
DockerControls::Restart => self DockerControls::Restart => self
.docker_sender .docker_sender
.send(DockerMessage::Restart(id)) .send(DockerMessage::Restart(id))
@@ -321,6 +367,7 @@ impl InputHandler {
} }
} }
} }
}
/// Check if a button press interacts with either the yes or no buttons in the delete container confirm window /// Check if a button press interacts with either the yes or no buttons in the delete container confirm window
async fn button_intersect(&mut self, mouse_event: MouseEvent) { async fn button_intersect(&mut self, mouse_event: MouseEvent) {
+2 -30
View File
@@ -55,18 +55,6 @@ fn setup_tracing() {
tracing_subscriber::fmt().with_max_level(Level::INFO).init(); 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 /// Read the optional docker_host path, the cli args take priority over the DOCKER_HOST env
fn read_docker_host(args: &CliArgs) -> Option<String> { fn read_docker_host(args: &CliArgs) -> Option<String> {
args.host args.host
@@ -77,7 +65,6 @@ fn read_docker_host(args: &CliArgs) -> Option<String> {
/// Create docker daemon handler, and only spawn up the docker data handler if a ping returns non-error /// Create docker daemon handler, and only spawn up the docker data handler if a ping returns non-error
async fn docker_init( async fn docker_init(
app_data: &Arc<Mutex<AppData>>, app_data: &Arc<Mutex<AppData>>,
containerised: bool,
docker_rx: Receiver<DockerMessage>, docker_rx: Receiver<DockerMessage>,
gui_state: &Arc<Mutex<GuiState>>, gui_state: &Arc<Mutex<GuiState>>,
is_running: &Arc<AtomicBool>, is_running: &Arc<AtomicBool>,
@@ -93,12 +80,7 @@ async fn docker_init(
let gui_state = Arc::clone(gui_state); let gui_state = Arc::clone(gui_state);
let is_running = Arc::clone(is_running); let is_running = Arc::clone(is_running);
tokio::spawn(DockerData::init( tokio::spawn(DockerData::init(
app_data, app_data, docker, docker_rx, gui_state, is_running,
containerised,
docker,
docker_rx,
gui_state,
is_running,
)); ));
} else { } else {
app_data app_data
@@ -134,8 +116,6 @@ fn handler_init(
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let containerised = check_if_containerised();
setup_tracing(); setup_tracing();
let args = CliArgs::new(); let args = CliArgs::new();
@@ -146,15 +126,7 @@ async fn main() {
let is_running = Arc::new(AtomicBool::new(true)); let is_running = Arc::new(AtomicBool::new(true));
let (docker_sx, docker_rx) = tokio::sync::mpsc::channel(32); let (docker_sx, docker_rx) = tokio::sync::mpsc::channel(32);
docker_init( docker_init(&app_data, docker_rx, &gui_state, &is_running, host).await;
&app_data,
containerised,
docker_rx,
&gui_state,
&is_running,
host,
)
.await;
if args.gui { if args.gui {
let (input_sx, input_rx) = tokio::sync::mpsc::channel(32); let (input_sx, input_rx) = tokio::sync::mpsc::channel(32);
+31 -4
View File
@@ -3,10 +3,12 @@ use std::process;
use clap::Parser; use clap::Parser;
use tracing::error; use tracing::error;
use crate::{ENV_KEY, ENV_VALUE};
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
#[command(version, about)] #[command(version, about)]
pub struct CliArgs { pub struct Args {
/// Docker update interval in ms, minimum effectively 1000 /// Docker update interval in ms, minimum effectively 1000
#[clap(short = 'd', value_name = "ms", default_value_t = 1000)] #[clap(short = 'd', value_name = "ms", default_value_t = 1000)]
pub docker_interval: u32, pub docker_interval: u32,
@@ -36,10 +38,34 @@ pub struct CliArgs {
pub gui: bool, 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<String>,
pub raw: bool,
pub show_self: bool,
pub timestamp: bool,
}
impl CliArgs { 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 /// Parse cli arguments
pub fn new() -> Self { pub fn new() -> Self {
let args = Self::parse(); let args = Args::parse();
// Quit the program if the docker update argument is 0 // Quit the program if the docker update argument is 0
// Should maybe change it to check if less than 100 // Should maybe change it to check if less than 100
@@ -50,10 +76,11 @@ impl CliArgs {
Self { Self {
color: args.color, color: args.color,
docker_interval: args.docker_interval, docker_interval: args.docker_interval,
host: args.host,
gui: !args.gui, gui: !args.gui,
show_self: !args.show_self, host: args.host,
in_container: Self::check_if_in_container(),
raw: args.raw, raw: args.raw,
show_self: !args.show_self,
timestamp: !args.timestamp, timestamp: !args.timestamp,
} }
} }
+6 -5
View File
@@ -585,6 +585,11 @@ impl HelpInfo {
space(), space(),
button_item("enter"), button_item("enter"),
button_desc("to send docker container command"), button_desc("to send docker container command"),
]),
Line::from(vec![
space(),
button_item("e"),
button_desc("exec into a container"),
]), ]),
Line::from(vec![ Line::from(vec![
space(), space(),
@@ -724,11 +729,7 @@ pub fn help_box(f: &mut Frame) {
/// Draw the delete confirm box in the centre of the screen /// Draw the delete confirm box in the centre of the screen
/// take in container id and container name here? /// take in container id and container name here?
pub fn delete_confirm( pub fn delete_confirm(f: &mut Frame, gui_state: &Arc<Mutex<GuiState>>, name: &str) {
f: &mut Frame,
gui_state: &Arc<Mutex<GuiState>>,
name: &str,
) {
let block = Block::default() let block = Block::default()
.title(" Confirm Delete ") .title(" Confirm Delete ")
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
+32 -5
View File
@@ -1,5 +1,10 @@
use parking_lot::Mutex;
use ratatui::layout::{Constraint, Rect}; 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 uuid::Uuid;
use crate::app_data::{ContainerId, Header}; 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 /// Various functions (e.g input handler), operate differently depending upon current Status
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum Status { pub enum Status {
Init, Exec,
Help,
DockerConnect,
DeleteConfirm, DeleteConfirm,
DockerConnect,
Error, Error,
Help,
Init,
} }
/// Global gui_state, stored in an Arc<Mutex> /// Global gui_state, stored in an Arc<Mutex>
@@ -296,13 +302,34 @@ impl GuiState {
} }
/// Remove a loading_uuid from the is_loading HashSet, if empty, reset loading_index to 0 /// 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); self.is_loading.remove(&uuid);
if self.is_loading.is_empty() { if self.is_loading.is_empty() {
self.loading_index = 0; self.loading_index = 0;
} }
} }
/// Animate the loading icon in its own Tokio thread
pub fn start_loading_animation(
gui_state: &Arc<Mutex<Self>>,
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 /// Set info box content
pub fn set_info_box(&mut self, text: &str) { pub fn set_info_box(&mut self, text: &str) {
self.info_box_text = Some(text.to_owned()); self.info_box_text = Some(text.to_owned());
+53 -17
View File
@@ -27,10 +27,13 @@ pub use self::color_match::*;
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status}; pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
use crate::{ use crate::{
app_data::AppData, app_error::AppError, docker_data::DockerMessage, 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 { pub struct Ui {
args: CliArgs,
app_data: Arc<Mutex<AppData>>, app_data: Arc<Mutex<AppData>>,
docker_sx: Sender<DockerMessage>, docker_sx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>, gui_state: Arc<Mutex<GuiState>>,
@@ -63,7 +66,9 @@ impl Ui {
sender: Sender<InputMessages>, sender: Sender<InputMessages>,
) { ) {
if let Ok(terminal) = Self::setup_terminal() { if let Ok(terminal) = Self::setup_terminal() {
let args = app_data.lock().args.clone();
let mut ui = Self { let mut ui = Self {
args,
app_data, app_data,
docker_sx, docker_sx,
gui_state, gui_state,
@@ -86,19 +91,17 @@ impl Ui {
/// Setup the terminal for full-screen drawing mode, with mouse capture /// Setup the terminal for full-screen drawing mode, with mouse capture
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> { fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?; let stdout = Self::init_terminal()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
Self::enable_mouse_capture()?;
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
Ok(Terminal::new(backend)?) Ok(Terminal::new(backend)?)
} }
/// This is a fix for mouse-events being printed to screen, read an event and do nothing with it fn init_terminal() -> Result<Stdout> {
fn nullify_event_read(&self) { enable_raw_mode()?;
if crossterm::event::poll(self.input_poll_rate).unwrap_or(true) { let mut stdout = io::stdout();
event::read().ok(); execute!(stdout, EnterAlternateScreen)?;
} Self::enable_mouse_capture()?;
Ok(stdout)
} }
/// reset the terminal back to default settings /// reset the terminal back to default settings
@@ -137,12 +140,48 @@ impl Ui {
Ok(()) 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 /// The loop for drawing the main UI to the terminal
async fn gui_loop(&mut self) -> Result<(), AppError> { async fn gui_loop(&mut self) -> Result<(), AppError> {
let update_duration = 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) { 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 if self
.terminal .terminal
.draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state)) .draw(|frame| draw_frame(frame, &self.app_data, &self.gui_state))
@@ -150,6 +189,7 @@ impl Ui {
{ {
return Err(AppError::Terminal); return Err(AppError::Terminal);
} }
if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) { if crossterm::event::poll(self.input_poll_rate).unwrap_or(false) {
if let Ok(event) = event::read() { if let Ok(event) = event::read() {
if let Event::Key(key) = event { 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 { if self.now.elapsed() >= update_duration {
self.docker_sx.send(DockerMessage::Update).await.ok(); self.docker_sx.send(DockerMessage::Update).await.ok();
self.now = Instant::now(); self.now = Instant::now();
@@ -192,7 +233,6 @@ impl Ui {
} else { } else {
self.gui_loop().await?; self.gui_loop().await?;
} }
self.nullify_event_read();
Ok(()) Ok(())
} }
} }
@@ -208,11 +248,7 @@ macro_rules! value_capture {
/// Draw the main ui to a frame of the terminal /// Draw the main ui to a frame of the terminal
/// TODO add a single line area for debug message - if not in release mode? /// TODO add a single line area for debug message - if not in release mode?
fn draw_frame( fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
f: &mut Frame,
app_data: &Arc<Mutex<AppData>>,
gui_state: &Arc<Mutex<GuiState>>,
) {
value_capture!(height, app_data.lock().get_container_len()); value_capture!(height, app_data.lock().get_container_len());
value_capture!(column_widths, app_data.lock().get_width()); value_capture!(column_widths, app_data.lock().get_width());
value_capture!(has_containers, app_data.lock().get_container_len() > 0); value_capture!(has_containers, app_data.lock().get_container_len() > 0);