Merge branch 'feat/tty' into dev

This commit is contained in:
Jack Wills
2023-11-18 22:45:20 +00:00
15 changed files with 477 additions and 142 deletions
@@ -87,6 +87,7 @@ jobs:
artifacts: | artifacts: |
**/oxker_*.zip **/oxker_*.zip
**/oxker_*.tar.gz **/oxker_*.tar.gz
######################### #########################
## Publish to crates.io # ## Publish to crates.io #
######################### #########################
@@ -106,6 +107,7 @@ jobs:
######################################### #########################################
## Build images for Dockerhub & ghcr.io # ## Build images for Dockerhub & ghcr.io #
######################################### #########################################
image_build: image_build:
needs: [cargo_publish] needs: [cargo_publish]
runs-on: ubuntu-latest runs-on: ubuntu-latest
+8 -7
View File
@@ -106,13 +106,14 @@ In application controls
Available command line arguments Available command line arguments
| argument|result| | argument|result|
|--|--| |--|--|
|```-d [number > 0]```| set the minimum update interval for docker information, in ms, defaults to 1000 (1 second) | |```-d [number > 0]```| Set the minimum update interval for docker information in milliseconds. Defaults to 1000 (1 second).|
|```--host [hostname]```| connect to Docker with a custom hostname, defaults to `/var/run/docker.sock`, will use `$DOCKER_HOST` env if set | |```--host [hostname]```| Connect to Docker with a custom hostname. Defaults to `/var/run/docker.sock`. Will use `$DOCKER_HOST` environment variable if set.|
|```-r```| show raw logs, by default oxker will remove ANSI formatting (conflicts with -c) | |```--use-cli```| When executing into a container, use the external Docker CLI application.|
|```-c```| attempt to color the logs (conflicts with -r) | |```-r```| Show raw logs. By default, removes ANSI formatting (conflicts with `-c`).|
|```-t```| remove timestamps from each log entry | |```-c```| Attempt to color the logs (conflicts with `-r`).|
|```-s```| if running via docker, will show the oxker container | |```-t```| Remove timestamps from each log entry.|
|```-g```| no tui, basically a pointless debugging mode, for now | |```-s```| If running via Docker, will display the oxker container.|
|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.|
## Build step ## Build step
+1 -16
View File
@@ -45,31 +45,16 @@ 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 alpine:3.18 as RUNTIME FROM scratch as RUNTIME
# Set an ENV to indicate that we're running in a container # Set an ENV to indicate that we're running in a container
ENV OXKER_RUNTIME=container ENV OXKER_RUNTIME=container
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 so DO NOT EDIT # this is used in the application itself so DO NOT EDIT
+7 -19
View File
@@ -1,28 +1,12 @@
################
## 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
FROM alpine:3.18 as RUNTIME # Set env that we're running in a container, so that the application can sleep for 250ms at start
# 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 --from=MUSL_SETUP /usr/bin/docker /usr/bin/ # Copy application binary from builder image
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
@@ -44,3 +28,7 @@ ENTRYPOINT [ "/app/oxker"]
# Buildx command to build musl version for all three platforms, should probably be executed in create_release # Buildx command to build musl version for all three platforms, should probably be executed in create_release
# docker buildx create --use # docker buildx create --use
# docker buildx build --platform linux/arm/v6,linux/arm64,linux/amd64 -t oxker_dev_all -o type=tar,dest=/tmp/oxker_dev_all.tar -f containerised/Dockerfile . # docker buildx build --platform linux/arm/v6,linux/arm64,linux/amd64 -t oxker_dev_all -o type=tar,dest=/tmp/oxker_dev_all.tar -f containerised/Dockerfile .
# Build production version for x86 only, then run
# docker build --platform linux/amd64 -t oxker_dev -f containerised/Dockerfile . && docker run --rm -it --volume /var/run/docker.sock:/var/run/docker.sock:ro oxker_dev
+7 -1
View File
@@ -83,7 +83,7 @@ impl AppData {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
#[allow(unused)] #[allow(unused)]
pub fn set_debug_string(&mut self, x: &str) { pub fn push_debug_string(&mut self, x: &str) {
self.debug_string.push_str(x); self.debug_string.push_str(x);
} }
@@ -506,6 +506,12 @@ impl AppData {
self.get_selected_container().map(|i| i.id.clone()) self.get_selected_container().map(|i| i.id.clone())
} }
/// Get the Id and State for the currently selected container - used by the exec check method
pub fn get_selected_container_id_state(&self) -> Option<(ContainerId, State)> {
self.get_selected_container()
.map(|i| (i.id.clone(), i.state))
}
/// Update container mem, cpu, & network stats, in single function so only need to call .lock() once /// Update container mem, cpu, & network stats, in single function so only need to call .lock() once
/// Will also, if a sort is set, sort the containers /// Will also, if a sort is set, sort the containers
pub fn update_stats( pub fn update_stats(
+1 -1
View File
@@ -27,7 +27,7 @@ impl fmt::Display for AppError {
let reason = if *x { "en" } else { "dis" }; let reason = if *x { "en" } else { "dis" };
write!(f, "Unable to {reason}able mouse capture") write!(f, "Unable to {reason}able mouse capture")
} }
Self::Terminal => write!(f, "Unable to draw to terminal"), Self::Terminal => write!(f, "Unable to fully render to terminal"),
} }
} }
} }
+8 -3
View File
@@ -1,9 +1,14 @@
use crate::app_data::ContainerId; use std::sync::Arc;
#[derive(Debug, Clone)] use crate::app_data::ContainerId;
use bollard::Docker;
use tokio::sync::oneshot::Sender;
#[derive(Debug)]
pub enum DockerMessage { pub enum DockerMessage {
Delete(ContainerId),
ConfirmDelete(ContainerId), ConfirmDelete(ContainerId),
Delete(ContainerId),
Exec(Sender<Arc<Docker>>),
Pause(ContainerId), Pause(ContainerId),
Quit, Quit,
Restart(ContainerId), Restart(ContainerId),
+4
View File
@@ -330,6 +330,7 @@ impl DockerData {
/// Handle incoming messages, container controls & all container information update /// Handle incoming messages, container controls & all container information update
/// Spawn Docker commands off into own thread /// Spawn Docker commands off into own thread
#[allow(clippy::too_many_lines)]
async fn message_handler(&mut self) { async fn message_handler(&mut self) {
while let Some(message) = self.receiver.recv().await { while let Some(message) = self.receiver.recv().await {
let docker = Arc::clone(&self.docker); let docker = Arc::clone(&self.docker);
@@ -338,6 +339,9 @@ impl DockerData {
let uuid = Uuid::new_v4(); let uuid = Uuid::new_v4();
// TODO need to refactor these // TODO need to refactor these
match message { match message {
DockerMessage::Exec(sender) => {
sender.send(Arc::clone(&self.docker)).ok();
}
DockerMessage::Pause(id) => { DockerMessage::Pause(id) => {
tokio::spawn(async move { tokio::spawn(async move {
let handle = GuiState::start_loading_animation(&gui_state, uuid); let handle = GuiState::start_loading_animation(&gui_state, uuid);
+319
View File
@@ -0,0 +1,319 @@
use std::{
fmt,
hash::{Hash, Hasher},
io::{Read, Write},
sync::{atomic::AtomicBool, Arc},
};
use bollard::{
container,
exec::{CreateExecOptions, StartExecOptions, StartExecResults},
Docker,
};
use crossterm::terminal::enable_raw_mode;
use futures_util::StreamExt;
use parking_lot::Mutex;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::{
app_data::{AppData, ContainerId, State},
app_error::AppError,
parse_args::CliArgs,
ui::{GuiState, Status},
};
/// TTY location
const TTY: &str = "/dev/tty";
/// This will be the start of a docker exec emssage if one is unable to actually exec into the container
const OCI_ERROR: &str = "OCI runtime exec failed";
/// Set the cursor position on the screen to (0,0)
pub const CURSOR_POS: &str = "\x1B[J\x1B[H";
/// This needs to be written to stdout when exiting the exec mode, else the input handler thread gets confused,
/// see https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
const KEYBOARD_PROTO: &str = "\x1B[?u\x1B[c";
mod command {
pub const PWD: &str = "pwd";
pub const DOCKER: &str = "docker";
pub const EXEC: &str = "exec";
pub const SH: &str = "sh";
pub const IT: &str = "-it";
}
/// Currently known byte output after writing KEYBOARD_PROTO to stdout
/// valid arm: [91, 63, 54, 49, 59, 54, 59, 55, 59, 50, 50, 59, 50, 51, 59, 50, 52, 59, 50, 56, 59, 51, 50, 59,52, 50] => [?61;6;7;22;23;24;28;32;2
/// valid x86: [91, 63, 49, 59, 50, 99] => [?1;2c
/// invalid x86: [91, 63, 49, 59, 48, 99] => [?1;0c
enum ByteOutput {
Arm,
X86,
}
impl ByteOutput {
const fn len(&self) -> usize {
match self {
Self::Arm => 26,
Self::X86 => 6,
}
}
const fn last(&self) -> &[u8] {
match self {
Self::Arm => &[50],
Self::X86 => &[99],
}
}
}
/// Check the output from tty to see if it matches known sequence.
/// At the moment we only need to check the length and end digit, as x86 valid and invalid match in these two regards
fn byte_sequence_valid(bytes: &[u8]) -> bool {
[ByteOutput::Arm, ByteOutput::X86]
.iter()
.any(|i| i.len() == bytes.len() && bytes.ends_with(i.last()))
}
/// Check if tty is able to be written to, aka not windows
pub fn tty_readable() -> bool {
std::fs::OpenOptions::new()
.read(true)
.write(false)
.open(TTY)
.is_ok()
}
/// Async tty reading, spawned into its own tokio thread
fn tty(run: Arc<AtomicBool>) -> Option<AsyncTTY> {
if tty_readable() {
let (tx, rx) = std::sync::mpsc::channel();
tokio::spawn(async move {
if let Ok(mut f) = tokio::fs::File::open(TTY).await {
while run.load(std::sync::atomic::Ordering::SeqCst) {
let mut buf = [0];
if tokio::time::timeout(
std::time::Duration::from_millis(10),
f.read_exact(&mut buf),
)
.await
.is_ok()
&& tx.send(buf[0]).is_err()
{
run.store(false, std::sync::atomic::Ordering::SeqCst);
}
}
}
});
Some(AsyncTTY { rx })
} else {
None
}
}
struct AsyncTTY {
rx: std::sync::mpsc::Receiver<u8>,
}
#[derive(Debug, Clone)]
pub enum ExecMode {
// use Bollard Rust library
Internal((ContainerId, Arc<Docker>)),
// use the external `docker-cli`
External(ContainerId),
}
impl ExecMode {
/// Test if we can exec into the selected container, first via the Internal methods, then by the External
/// If the container is oxker, it will always return None
pub async fn new(app_data: &Arc<Mutex<AppData>>, docker: &Arc<Docker>) -> Option<Self> {
let is_oxker = app_data.lock().is_oxker();
if is_oxker {
return None;
}
let use_cli = app_data.lock().args.use_cli;
let container = app_data.lock().get_selected_container_id_state();
if let Some((id, state)) = container {
if state == State::Running {
if tty_readable() && !use_cli {
if let Ok(exec) = docker
.create_exec(
id.get(),
CreateExecOptions {
attach_stdout: Some(true),
attach_stderr: Some(true),
cmd: Some(vec![command::PWD]),
..Default::default()
},
)
.await
{
if let Ok(StartExecResults::Attached { mut output, .. }) =
docker.start_exec(&exec.id, None).await
{
if let Some(Ok(msg)) = output.next().await {
if !msg.to_string().starts_with(OCI_ERROR) {
return Some(Self::Internal((id.clone(), Arc::clone(docker))));
}
}
}
}
}
if let Ok(output) = std::process::Command::new(command::DOCKER)
.args([command::EXEC, id.get(), command::PWD])
.output()
{
if let Ok(output) = String::from_utf8(output.stdout) {
if !output.starts_with(OCI_ERROR) {
return Some(Self::External(id.clone()));
}
}
}
}
}
None
}
/// exec into the container using the external docker cli, the result it just piped into oxker
fn exec_external(id: &ContainerId) {
let mut stdout = std::io::stdout();
stdout.write_all(CURSOR_POS.as_bytes()).ok();
if let Ok(mut child) = std::process::Command::new(command::DOCKER)
.args([command::EXEC, command::IT, id.get(), command::SH])
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()
{
child.wait().ok();
if child.kill().is_err() {
std::process::exit(1)
}
}
}
/// Exec into the container via the Bollard library, stdout & stdin on different threads
/// Have to deal with strange output once dropped, hence the use of internal_cleanup() method
async fn exec_internal(&self, id: &ContainerId, docker: &Arc<Docker>) -> Result<(), AppError> {
let run = Arc::new(AtomicBool::new(true));
if let Ok(exec_result) = docker
.create_exec(
id.get(),
CreateExecOptions {
attach_stdout: Some(true),
attach_stderr: Some(false),
attach_stdin: Some(true),
tty: Some(true),
cmd: Some(vec![command::SH]),
..Default::default()
},
)
.await
{
if let Ok(StartExecResults::Attached {
mut output,
mut input,
}) = docker
.start_exec(
&exec_result.id,
Some(StartExecOptions {
detach: false,
..Default::default()
}),
)
.await
{
if let Some(async_tty) = tty(Arc::clone(&run)) {
let run_thread = Arc::clone(&run);
tokio::spawn(async move {
enable_raw_mode().ok();
let mut stdout = std::io::stdout();
stdout.write_all(CURSOR_POS.as_bytes()).ok();
stdout.flush().ok();
while run_thread.load(std::sync::atomic::Ordering::SeqCst) {
while let Some(Ok(x)) = output.next().await {
stdout.write_all(&x.into_bytes()).ok();
stdout.flush().ok();
}
run_thread.store(false, std::sync::atomic::Ordering::SeqCst);
}
});
while let Ok(x) = async_tty.rx.recv() {
input.write(&[x]).await.ok();
}
self.internal_cleanup()?;
}
} else {
return Err(AppError::Terminal);
}
}
Ok(())
}
// This is the fix for key pressed not being handled correctly on quit
// It writes a special message to the stdout, and then listens out for a valid response
// afterwhich it's assumes that we're completely done with TTY
fn internal_cleanup(&self) -> Result<(), AppError> {
match self {
Self::External(_) => Ok(()),
Self::Internal(_) => {
let waiting = Arc::new(AtomicBool::new(true));
let waiting_thread = Arc::clone(&waiting);
std::thread::spawn(move || {
// At the moment the known max length is 26
let mut bytes = Vec::with_capacity(26);
while waiting_thread.load(std::sync::atomic::Ordering::SeqCst) {
let mut buf = [0];
if let Ok(mut f) = std::fs::File::open(TTY) {
if f.read_exact(&mut buf).is_err() {
waiting_thread.store(false, std::sync::atomic::Ordering::SeqCst);
}
bytes.push(buf[0]);
if byte_sequence_valid(&bytes) {
waiting_thread.store(false, std::sync::atomic::Ordering::SeqCst);
}
};
}
});
let mut stdout = std::io::stdout();
stdout.write_all(KEYBOARD_PROTO.as_bytes()).ok();
stdout.flush().ok();
let start = std::time::Instant::now();
while waiting.load(std::sync::atomic::Ordering::SeqCst) {
if start.elapsed().as_millis() > 1500 {
waiting.store(false, std::sync::atomic::Ordering::SeqCst);
return Err(AppError::Terminal);
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
Ok(())
}
}
}
// RESET TERMINAL BEFROEHAND
pub async fn run(
&self,
app_data: &Arc<Mutex<AppData>>,
gui_state: &Arc<Mutex<GuiState>>,
) -> Result<(), AppError> {
match self {
Self::External(id) => {
Self::exec_external(id);
Ok(())
}
Self::Internal((id, docker)) => self.exec_internal(id, docker).await,
}
}
}
+17 -25
View File
@@ -3,6 +3,7 @@ use std::sync::{
Arc, Arc,
}; };
use bollard::Docker;
use crossterm::{ use crossterm::{
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind}, event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
execute, execute,
@@ -20,12 +21,11 @@ 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, DOCKER_COMMAND}, exec::{tty_readable, ExecMode},
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
}; };
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 {
@@ -164,36 +164,28 @@ impl InputHandler {
} }
/// Validate that one can exec into a Docker container /// Validate that one can exec into a Docker container
fn e_key(&self) { async fn e_key(&self) {
let is_oxker = self.app_data.lock().is_oxker(); let is_oxker = self.app_data.lock().is_oxker();
if !is_oxker { let mut exec_err = Some(());
if !is_oxker && tty_readable() {
let uuid = Uuid::new_v4(); let uuid = Uuid::new_v4();
let handle = GuiState::start_loading_animation(&self.gui_state, uuid); let handle = GuiState::start_loading_animation(&self.gui_state, uuid);
let mut exec_err = Some(()); let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>();
self.docker_sender.send(DockerMessage::Exec(sx)).await.ok();
let id = self.app_data.lock().get_selected_container_id(); if let Ok(docker) = rx.await {
(ExecMode::new(&self.app_data, &docker).await).map_or_else(
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( self.app_data.lock().set_error(
AppError::DockerExec, AppError::DockerExec,
&self.gui_state, &self.gui_state,
Status::Error, Status::Error,
); );
} else { },
self.gui_state.lock().status_push(Status::Exec); |mode| {
} self.gui_state.lock().set_exec_mode(mode);
},
);
} }
self.gui_state.lock().stop_loading_animation(&handle, uuid); self.gui_state.lock().stop_loading_animation(&handle, uuid);
} }
@@ -251,7 +243,7 @@ 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('e' | 'E') => self.e_key().await,
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 => {
+7 -2
View File
@@ -13,7 +13,7 @@
clippy::similar_names clippy::similar_names
)] )]
// Only allow when debugging // Only allow when debugging
// #![allow(unused)] #![allow(unused)]
use app_data::AppData; use app_data::AppData;
use app_error::AppError; use app_error::AppError;
@@ -35,6 +35,7 @@ use tracing::{error, info, Level};
mod app_data; mod app_data;
mod app_error; mod app_error;
mod docker_data; mod docker_data;
mod exec;
mod input_handler; mod input_handler;
mod parse_args; mod parse_args;
mod ui; mod ui;
@@ -121,6 +122,10 @@ async fn main() {
setup_tracing(); setup_tracing();
let args = CliArgs::new(); let args = CliArgs::new();
if args.in_container {
std::thread::sleep(std::time::Duration::from_millis(250));
}
let host = read_docker_host(&args); let host = read_docker_host(&args);
let app_data = Arc::new(Mutex::new(AppData::default(args.clone()))); let app_data = Arc::new(Mutex::new(AppData::default(args.clone())));
@@ -141,7 +146,7 @@ async fn main() {
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);
handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running); handler_init(&app_data, &docker_tx, &gui_state, input_rx, &is_running);
Ui::create(app_data, gui_state, is_running, input_sx).await; Ui::create(app_data, docker_tx.clone(), gui_state, is_running, input_sx).await;
} else { } else {
info!("in debug mode\n"); info!("in debug mode\n");
// Debug mode for testing, less pointless now, will display some basic information // Debug mode for testing, less pointless now, will display some basic information
+11 -5
View File
@@ -21,10 +21,6 @@ pub struct Args {
#[clap(short = 'c', conflicts_with = "raw")] #[clap(short = 'c', conflicts_with = "raw")]
pub color: bool, pub color: bool,
/// Docker host, defaults to `/var/run/docker.sock`
#[clap(long, short = None)]
pub host: Option<String>,
/// Show raw logs, default is to remove ansi formatting, conflicts with "-c" /// Show raw logs, default is to remove ansi formatting, conflicts with "-c"
#[clap(short = 'r', conflicts_with = "color")] #[clap(short = 'r', conflicts_with = "color")]
pub raw: bool, pub raw: bool,
@@ -36,16 +32,25 @@ pub struct Args {
/// Don't draw gui - for debugging - mostly pointless /// Don't draw gui - for debugging - mostly pointless
#[clap(short = 'g')] #[clap(short = 'g')]
pub gui: bool, pub gui: bool,
/// Docker host, defaults to `/var/run/docker.sock`
#[clap(long, short = None)]
pub host: Option<String>,
/// Use "docker" cli for execing
#[clap(long="use-cli", short = None)]
pub use_cli: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
pub struct CliArgs { pub struct CliArgs {
pub in_container: bool,
pub color: bool, pub color: bool,
pub docker_interval: u32, pub docker_interval: u32,
pub use_cli: bool,
pub gui: bool, pub gui: bool,
pub host: Option<String>, pub host: Option<String>,
pub in_container: bool,
pub raw: bool, pub raw: bool,
pub show_self: bool, pub show_self: bool,
pub timestamp: bool, pub timestamp: bool,
@@ -76,6 +81,7 @@ impl CliArgs {
Self { Self {
color: args.color, color: args.color,
docker_interval: args.docker_interval, docker_interval: args.docker_interval,
use_cli: args.use_cli,
gui: !args.gui, gui: !args.gui,
host: args.host, host: args.host,
in_container: Self::check_if_in_container(), in_container: Self::check_if_in_container(),
+3 -2
View File
@@ -279,6 +279,7 @@ pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc<Mutex<AppData>>) {
.data(&mem.0)]; .data(&mem.0)];
let cpu_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1)); let cpu_stats = CpuStats::new(cpu.0.last().map_or(0.00, |f| f.1));
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let mem_stats = ByteStats::new(mem.0.last().map_or(0, |f| f.1 as u64)); let mem_stats = ByteStats::new(mem.0.last().map_or(0, |f| f.1 as u64));
let cpu_chart = make_chart(cpu.2, "cpu", cpu_dataset, &cpu_stats, &cpu.1); let cpu_chart = make_chart(cpu.2, "cpu", cpu_dataset, &cpu_stats, &cpu.1);
let mem_chart = make_chart(mem.2, "memory", mem_dataset, &mem_stats, &mem.1); let mem_chart = make_chart(mem.2, "memory", mem_dataset, &mem_stats, &mem.1);
@@ -359,8 +360,8 @@ pub fn heading_bar(
if let Some((a, b)) = data.sorted_by.as_ref() { if let Some((a, b)) = data.sorted_by.as_ref() {
if x == a { if x == a {
match b { match b {
SortedOrder::Asc => suffix = " ", SortedOrder::Asc => suffix = " ",
SortedOrder::Desc => suffix = " ", SortedOrder::Desc => suffix = " ",
} }
suffix_margin = 2; suffix_margin = 2;
color = Color::White; color = Color::White;
+31 -2
View File
@@ -7,7 +7,10 @@ use std::{
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use uuid::Uuid; use uuid::Uuid;
use crate::app_data::{ContainerId, Header}; use crate::{
app_data::{ContainerId, Header},
exec::ExecMode,
};
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)] #[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
pub enum SelectablePanel { pub enum SelectablePanel {
@@ -174,6 +177,7 @@ pub struct GuiState {
panel_map: HashMap<SelectablePanel, Rect>, panel_map: HashMap<SelectablePanel, Rect>,
selected_panel: SelectablePanel, selected_panel: SelectablePanel,
status: HashSet<Status>, status: HashSet<Status>,
exec_mode: Option<ExecMode>,
pub info_box_text: Option<String>, pub info_box_text: Option<String>,
} }
impl GuiState { impl GuiState {
@@ -265,17 +269,42 @@ impl GuiState {
} }
/// Remove a gui_status into the current gui_status HashSet /// Remove a gui_status into the current gui_status HashSet
/// Remove exec mode & deleteConfirm is required
pub fn status_del(&mut self, status: Status) { pub fn status_del(&mut self, status: Status) {
self.status.remove(&status); self.status.remove(&status);
if status == Status::DeleteConfirm { match status {
Status::DeleteConfirm => {
self.status.remove(&Status::DeleteConfirm); self.status.remove(&Status::DeleteConfirm);
} }
Status::Exec => {
self.exec_mode = None;
}
_ => (),
}
}
/// Inset the ExecMode into self, and set the Status as exec
/// Using StatusPush with Status::Exec won't insert into the hash map
/// To force self.exec_mode to be set
pub fn set_exec_mode(&mut self, mode: ExecMode) {
self.exec_mode = Some(mode);
self.status.insert(Status::Exec);
}
pub fn get_exec_mode(&mut self) -> Option<ExecMode> {
self.exec_mode.clone()
} }
/// Insert a gui_status into the current gui_status HashSet /// Insert a gui_status into the current gui_status HashSet
/// If the status is Exec, it won't get inserted, set_exec_mode() should be used instead
pub fn status_push(&mut self, status: Status) { pub fn status_push(&mut self, status: Status) {
match status {
Status::Exec => (),
_ => {
self.status.insert(status); self.status.insert(status);
} }
}
}
/// Change to next selectable panel /// Change to next selectable panel
pub fn next_panel(&mut self) { pub fn next_panel(&mut self) {
+22 -30
View File
@@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use bollard::Docker;
use crossterm::{ use crossterm::{
event::{self, DisableMouseCapture, Event}, event::{self, DisableMouseCapture, Event},
execute, execute,
@@ -28,20 +29,20 @@ pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
use crate::{ use crate::{
app_data::{AppData, Columns, ContainerId, Header, SortedOrder}, app_data::{AppData, Columns, ContainerId, Header, SortedOrder},
app_error::AppError, app_error::AppError,
docker_data::DockerMessage,
input_handler::InputMessages, input_handler::InputMessages,
}; };
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>,
gui_state: Arc<Mutex<GuiState>>, gui_state: Arc<Mutex<GuiState>>,
input_poll_rate: Duration, input_poll_rate: Duration,
is_running: Arc<AtomicBool>, is_running: Arc<AtomicBool>,
now: Instant, now: Instant,
sender: Sender<InputMessages>, sender: Sender<InputMessages>,
terminal: Terminal<CrosstermBackend<Stdout>>, terminal: Terminal<CrosstermBackend<Stdout>>,
cursor_position: (u16, u16),
} }
impl Ui { impl Ui {
@@ -60,20 +61,24 @@ impl Ui {
/// Create a new Ui struct, and execute the drawing loop /// Create a new Ui struct, and execute the drawing loop
pub async fn create( pub async fn create(
app_data: Arc<Mutex<AppData>>, app_data: Arc<Mutex<AppData>>,
docker_sx: Sender<DockerMessage>,
gui_state: Arc<Mutex<GuiState>>, gui_state: Arc<Mutex<GuiState>>,
is_running: Arc<AtomicBool>, is_running: Arc<AtomicBool>,
sender: Sender<InputMessages>, sender: Sender<InputMessages>,
) { ) {
if let Ok(terminal) = Self::setup_terminal() { if let Ok(mut terminal) = Self::setup_terminal() {
// let args = app_data.lock().args.clone(); // let args = app_data.lock().args.clone();
let cursor_position = terminal.get_cursor().unwrap_or_default();
let mut ui = Self { let mut ui = Self {
app_data, app_data,
docker_sx,
gui_state, gui_state,
input_poll_rate: std::time::Duration::from_millis(100), input_poll_rate: std::time::Duration::from_millis(100),
is_running, is_running,
now: Instant::now(), now: Instant::now(),
sender, sender,
terminal, terminal,
cursor_position,
}; };
if let Err(e) = ui.draw_ui().await { if let Err(e) = ui.draw_ui().await {
error!("{e}"); error!("{e}");
@@ -111,6 +116,9 @@ impl Ui {
DisableMouseCapture DisableMouseCapture
)?; )?;
disable_raw_mode()?; disable_raw_mode()?;
self.terminal.clear().ok();
self.terminal
.set_cursor(self.cursor_position.0, self.cursor_position.1)?;
Ok(self.terminal.show_cursor()?) Ok(self.terminal.show_cursor()?)
} }
@@ -138,24 +146,17 @@ impl Ui {
} }
/// Use exeternal docker cli to exec into a container /// Use exeternal docker cli to exec into a container
fn exec(&mut self) { async fn exec(&mut self) {
let id = self.app_data.lock().get_selected_container_id(); let mut exec_mode = self.gui_state.lock().get_exec_mode();
if let Some(id) = id { if let Some(mode) = exec_mode {
// 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(); self.reset_terminal().ok();
child.wait().ok(); self.terminal.clear().ok();
if child.kill().is_err() { if let Err(e) = mode.run(&self.app_data, &self.gui_state).await {
std::process::exit(1) self.app_data
} .lock()
} .set_error(e, &self.gui_state, Status::Error);
};
} }
self.terminal.clear().ok(); self.terminal.clear().ok();
self.reset_terminal().ok(); self.reset_terminal().ok();
@@ -168,7 +169,7 @@ impl Ui {
while self.is_running.load(Ordering::SeqCst) { while self.is_running.load(Ordering::SeqCst) {
let exec = self.gui_state.lock().status_contains(&[Status::Exec]); let exec = self.gui_state.lock().status_contains(&[Status::Exec]);
if exec { if exec {
self.exec(); self.exec().await;
} }
if self if self
@@ -220,15 +221,6 @@ impl Ui {
} }
} }
// #[macro_export]
// /// This macro simplifies the definition and evaluation of variables by capturing and immediately evaluating an expression.
// macro_rules! value_capture {
// ($name:ident, $lock_expr:expr) => {
// let $name = || $lock_expr;
// let $name = $name();
// };
// }
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> { fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> {
Layout::default() Layout::default()