Merge branch 'feat/tty' into dev
This commit is contained in:
@@ -87,6 +87,7 @@ jobs:
|
||||
artifacts: |
|
||||
**/oxker_*.zip
|
||||
**/oxker_*.tar.gz
|
||||
|
||||
#########################
|
||||
## Publish to crates.io #
|
||||
#########################
|
||||
@@ -106,6 +107,7 @@ jobs:
|
||||
#########################################
|
||||
## Build images for Dockerhub & ghcr.io #
|
||||
#########################################
|
||||
|
||||
image_build:
|
||||
needs: [cargo_publish]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -106,13 +106,14 @@ In application controls
|
||||
Available command line arguments
|
||||
| argument|result|
|
||||
|--|--|
|
||||
|```-d [number > 0]```| set the minimum update interval for docker information, in ms, 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 |
|
||||
|```-r```| show raw logs, by default oxker will remove ANSI formatting (conflicts with -c) |
|
||||
|```-c```| attempt to color the logs (conflicts with -r) |
|
||||
|```-t```| remove timestamps from each log entry |
|
||||
|```-s```| if running via docker, will show the oxker container |
|
||||
|```-g```| no tui, basically a pointless debugging mode, for now |
|
||||
|```-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` environment variable if set.|
|
||||
|```--use-cli```| When executing into a container, use the external Docker CLI application.|
|
||||
|```-r```| Show raw logs. By default, removes ANSI formatting (conflicts with `-c`).|
|
||||
|```-c```| Attempt to color the logs (conflicts with `-r`).|
|
||||
|```-t```| Remove timestamps from each log entry.|
|
||||
|```-s```| If running via Docker, will display the oxker container.|
|
||||
|```-g```| No TUI, essentially a debugging mode with limited functionality, for now.|
|
||||
|
||||
## Build step
|
||||
|
||||
|
||||
@@ -45,32 +45,17 @@ RUN cargo build --release --target $(cat /.platform)
|
||||
|
||||
RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker /
|
||||
|
||||
################
|
||||
## MUSL SETUP ##
|
||||
################
|
||||
|
||||
FROM alpine:3.18 as MUSL_SETUP
|
||||
|
||||
RUN apk add --update --no-cache docker-cli upx
|
||||
|
||||
# Compress the docker executable, to reduce final image size
|
||||
RUN upx -9 /usr/bin/docker
|
||||
|
||||
#############
|
||||
## Runtime ##
|
||||
#############
|
||||
|
||||
FROM alpine:3.18 as RUNTIME
|
||||
FROM scratch as RUNTIME
|
||||
|
||||
# Set an ENV to indicate that we're running in a container
|
||||
ENV OXKER_RUNTIME=container
|
||||
|
||||
COPY --from=BUILDER /oxker /app/
|
||||
COPY --from=MUSL_SETUP /usr/bin/docker /usr/bin/
|
||||
|
||||
# remove sh and busybox, probably pointless
|
||||
RUN rm /bin/sh /bin/busybox
|
||||
|
||||
# Run the application
|
||||
# this is used in the application itself so DO NOT EDIT
|
||||
ENTRYPOINT [ "/app/oxker"]
|
||||
ENTRYPOINT [ "/app/oxker"]
|
||||
@@ -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 ##
|
||||
#############
|
||||
FROM scratch
|
||||
|
||||
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 env that we're running in a container, so that the application can sleep for 250ms at start
|
||||
ENV OXKER_RUNTIME=container
|
||||
|
||||
COPY --from=MUSL_SETUP /usr/bin/docker /usr/bin/
|
||||
|
||||
|
||||
RUN rm /bin/sh /bin/busybox
|
||||
|
||||
# Copy application binary from builder image
|
||||
COPY ./target/x86_64-unknown-linux-musl/release/oxker /app/
|
||||
|
||||
# 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
|
||||
# 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 .
|
||||
|
||||
|
||||
# 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
@@ -83,7 +83,7 @@ impl AppData {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[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);
|
||||
}
|
||||
|
||||
@@ -506,6 +506,12 @@ impl AppData {
|
||||
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
|
||||
/// Will also, if a sort is set, sort the containers
|
||||
pub fn update_stats(
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ impl fmt::Display for AppError {
|
||||
let reason = if *x { "en" } else { "dis" };
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
Delete(ContainerId),
|
||||
ConfirmDelete(ContainerId),
|
||||
Delete(ContainerId),
|
||||
Exec(Sender<Arc<Docker>>),
|
||||
Pause(ContainerId),
|
||||
Quit,
|
||||
Restart(ContainerId),
|
||||
|
||||
@@ -330,6 +330,7 @@ impl DockerData {
|
||||
|
||||
/// Handle incoming messages, container controls & all container information update
|
||||
/// Spawn Docker commands off into own thread
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn message_handler(&mut self) {
|
||||
while let Some(message) = self.receiver.recv().await {
|
||||
let docker = Arc::clone(&self.docker);
|
||||
@@ -338,6 +339,9 @@ impl DockerData {
|
||||
let uuid = Uuid::new_v4();
|
||||
// TODO need to refactor these
|
||||
match message {
|
||||
DockerMessage::Exec(sender) => {
|
||||
sender.send(Arc::clone(&self.docker)).ok();
|
||||
}
|
||||
DockerMessage::Pause(id) => {
|
||||
tokio::spawn(async move {
|
||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
||||
|
||||
+319
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-30
@@ -3,6 +3,7 @@ use std::sync::{
|
||||
Arc,
|
||||
};
|
||||
|
||||
use bollard::Docker;
|
||||
use crossterm::{
|
||||
event::{DisableMouseCapture, KeyCode, KeyModifiers, MouseButton, MouseEvent, MouseEventKind},
|
||||
execute,
|
||||
@@ -20,12 +21,11 @@ use crate::{
|
||||
app_data::{AppData, DockerControls, Header},
|
||||
app_error::AppError,
|
||||
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;
|
||||
|
||||
const OCI_ERROR: &str = "OCI runtime exec failed";
|
||||
|
||||
/// Handle all input events
|
||||
#[derive(Debug)]
|
||||
pub struct InputHandler {
|
||||
@@ -164,36 +164,28 @@ impl InputHandler {
|
||||
}
|
||||
|
||||
/// 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();
|
||||
if !is_oxker {
|
||||
let mut exec_err = Some(());
|
||||
if !is_oxker && tty_readable() {
|
||||
let uuid = Uuid::new_v4();
|
||||
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 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);
|
||||
}
|
||||
if let Ok(docker) = rx.await {
|
||||
(ExecMode::new(&self.app_data, &docker).await).map_or_else(
|
||||
|| {
|
||||
self.app_data.lock().set_error(
|
||||
AppError::DockerExec,
|
||||
&self.gui_state,
|
||||
Status::Error,
|
||||
);
|
||||
},
|
||||
|mode| {
|
||||
self.gui_state.lock().set_exec_mode(mode);
|
||||
},
|
||||
);
|
||||
}
|
||||
self.gui_state.lock().stop_loading_animation(&handle, uuid);
|
||||
}
|
||||
@@ -251,7 +243,7 @@ impl InputHandler {
|
||||
KeyCode::Char('7') => self.sort(Header::Image),
|
||||
KeyCode::Char('8') => self.sort(Header::Rx),
|
||||
KeyCode::Char('9') => self.sort(Header::Tx),
|
||||
KeyCode::Char('e' | 'E') => self.e_key(),
|
||||
KeyCode::Char('e' | 'E') => self.e_key().await,
|
||||
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
|
||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||
KeyCode::Tab => {
|
||||
|
||||
+27
-22
@@ -13,7 +13,7 @@
|
||||
clippy::similar_names
|
||||
)]
|
||||
// Only allow when debugging
|
||||
// #![allow(unused)]
|
||||
#![allow(unused)]
|
||||
|
||||
use app_data::AppData;
|
||||
use app_error::AppError;
|
||||
@@ -35,6 +35,7 @@ use tracing::{error, info, Level};
|
||||
mod app_data;
|
||||
mod app_error;
|
||||
mod docker_data;
|
||||
mod exec;
|
||||
mod input_handler;
|
||||
mod parse_args;
|
||||
mod ui;
|
||||
@@ -121,6 +122,10 @@ async fn main() {
|
||||
setup_tracing();
|
||||
|
||||
let args = CliArgs::new();
|
||||
|
||||
if args.in_container {
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
}
|
||||
let host = read_docker_host(&args);
|
||||
|
||||
let app_data = Arc::new(Mutex::new(AppData::default(args.clone())));
|
||||
@@ -141,33 +146,33 @@ async fn main() {
|
||||
if args.gui {
|
||||
let (input_sx, input_rx) = tokio::sync::mpsc::channel(32);
|
||||
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 {
|
||||
info!("in debug mode\n");
|
||||
// Debug mode for testing, less pointless now, will display some basic information
|
||||
while is_running.load(Ordering::SeqCst) {
|
||||
if let Some(err) = app_data.lock().get_error() {
|
||||
error!("{}", err);
|
||||
process::exit(1);
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
|
||||
args.docker_interval,
|
||||
)))
|
||||
.await;
|
||||
let containers = app_data
|
||||
.lock()
|
||||
.get_container_items()
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|i| format!("{i}"))
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(err) = app_data.lock().get_error() {
|
||||
error!("{}", err);
|
||||
process::exit(1);
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
|
||||
args.docker_interval,
|
||||
)))
|
||||
.await;
|
||||
let containers = app_data
|
||||
.lock()
|
||||
.get_container_items()
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|i| format!("{i}"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !containers.is_empty() {
|
||||
for item in containers {
|
||||
info!("{item}");
|
||||
}
|
||||
println!();
|
||||
if !containers.is_empty() {
|
||||
for item in containers {
|
||||
info!("{item}");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -21,10 +21,6 @@ pub struct Args {
|
||||
#[clap(short = 'c', conflicts_with = "raw")]
|
||||
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"
|
||||
#[clap(short = 'r', conflicts_with = "color")]
|
||||
pub raw: bool,
|
||||
@@ -36,16 +32,25 @@ pub struct Args {
|
||||
/// Don't draw gui - for debugging - mostly pointless
|
||||
#[clap(short = 'g')]
|
||||
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)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct CliArgs {
|
||||
pub in_container: bool,
|
||||
pub color: bool,
|
||||
pub docker_interval: u32,
|
||||
pub use_cli: bool,
|
||||
pub gui: bool,
|
||||
pub host: Option<String>,
|
||||
pub in_container: bool,
|
||||
pub raw: bool,
|
||||
pub show_self: bool,
|
||||
pub timestamp: bool,
|
||||
@@ -76,6 +81,7 @@ impl CliArgs {
|
||||
Self {
|
||||
color: args.color,
|
||||
docker_interval: args.docker_interval,
|
||||
use_cli: args.use_cli,
|
||||
gui: !args.gui,
|
||||
host: args.host,
|
||||
in_container: Self::check_if_in_container(),
|
||||
|
||||
@@ -279,6 +279,7 @@ pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc<Mutex<AppData>>) {
|
||||
.data(&mem.0)];
|
||||
|
||||
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 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);
|
||||
@@ -359,8 +360,8 @@ pub fn heading_bar(
|
||||
if let Some((a, b)) = data.sorted_by.as_ref() {
|
||||
if x == a {
|
||||
match b {
|
||||
SortedOrder::Asc => suffix = " ⌃",
|
||||
SortedOrder::Desc => suffix = " ⌄",
|
||||
SortedOrder::Asc => suffix = " ▲",
|
||||
SortedOrder::Desc => suffix = " ▼",
|
||||
}
|
||||
suffix_margin = 2;
|
||||
color = Color::White;
|
||||
|
||||
+33
-4
@@ -7,7 +7,10 @@ use std::{
|
||||
use tokio::task::JoinHandle;
|
||||
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)]
|
||||
pub enum SelectablePanel {
|
||||
@@ -174,6 +177,7 @@ pub struct GuiState {
|
||||
panel_map: HashMap<SelectablePanel, Rect>,
|
||||
selected_panel: SelectablePanel,
|
||||
status: HashSet<Status>,
|
||||
exec_mode: Option<ExecMode>,
|
||||
pub info_box_text: Option<String>,
|
||||
}
|
||||
impl GuiState {
|
||||
@@ -265,16 +269,41 @@ impl GuiState {
|
||||
}
|
||||
|
||||
/// Remove a gui_status into the current gui_status HashSet
|
||||
/// Remove exec mode & deleteConfirm is required
|
||||
pub fn status_del(&mut self, status: Status) {
|
||||
self.status.remove(&status);
|
||||
if status == Status::DeleteConfirm {
|
||||
self.status.remove(&Status::DeleteConfirm);
|
||||
match status {
|
||||
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
|
||||
/// 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) {
|
||||
self.status.insert(status);
|
||||
match status {
|
||||
Status::Exec => (),
|
||||
_ => {
|
||||
self.status.insert(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Change to next selectable panel
|
||||
|
||||
+23
-31
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use bollard::Docker;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, Event},
|
||||
execute,
|
||||
@@ -28,20 +29,20 @@ pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
|
||||
use crate::{
|
||||
app_data::{AppData, Columns, ContainerId, Header, SortedOrder},
|
||||
app_error::AppError,
|
||||
docker_data::DockerMessage,
|
||||
input_handler::InputMessages,
|
||||
};
|
||||
|
||||
pub const DOCKER_COMMAND: &str = "docker";
|
||||
|
||||
pub struct Ui {
|
||||
// args: CliArgs,
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
docker_sx: Sender<DockerMessage>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
input_poll_rate: Duration,
|
||||
is_running: Arc<AtomicBool>,
|
||||
now: Instant,
|
||||
sender: Sender<InputMessages>,
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
cursor_position: (u16, u16),
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
@@ -60,20 +61,24 @@ impl Ui {
|
||||
/// Create a new Ui struct, and execute the drawing loop
|
||||
pub async fn create(
|
||||
app_data: Arc<Mutex<AppData>>,
|
||||
docker_sx: Sender<DockerMessage>,
|
||||
gui_state: Arc<Mutex<GuiState>>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
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 cursor_position = terminal.get_cursor().unwrap_or_default();
|
||||
let mut ui = Self {
|
||||
app_data,
|
||||
docker_sx,
|
||||
gui_state,
|
||||
input_poll_rate: std::time::Duration::from_millis(100),
|
||||
is_running,
|
||||
now: Instant::now(),
|
||||
sender,
|
||||
terminal,
|
||||
cursor_position,
|
||||
};
|
||||
if let Err(e) = ui.draw_ui().await {
|
||||
error!("{e}");
|
||||
@@ -111,6 +116,9 @@ impl Ui {
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
disable_raw_mode()?;
|
||||
self.terminal.clear().ok();
|
||||
self.terminal
|
||||
.set_cursor(self.cursor_position.0, self.cursor_position.1)?;
|
||||
Ok(self.terminal.show_cursor()?)
|
||||
}
|
||||
|
||||
@@ -138,24 +146,17 @@ impl Ui {
|
||||
}
|
||||
|
||||
/// Use exeternal docker cli to exec into a container
|
||||
fn exec(&mut self) {
|
||||
let id = self.app_data.lock().get_selected_container_id();
|
||||
async fn exec(&mut self) {
|
||||
let mut exec_mode = self.gui_state.lock().get_exec_mode();
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
if let Some(mode) = exec_mode {
|
||||
self.reset_terminal().ok();
|
||||
self.terminal.clear().ok();
|
||||
if let Err(e) = mode.run(&self.app_data, &self.gui_state).await {
|
||||
self.app_data
|
||||
.lock()
|
||||
.set_error(e, &self.gui_state, Status::Error);
|
||||
};
|
||||
}
|
||||
self.terminal.clear().ok();
|
||||
self.reset_terminal().ok();
|
||||
@@ -168,7 +169,7 @@ impl Ui {
|
||||
while self.is_running.load(Ordering::SeqCst) {
|
||||
let exec = self.gui_state.lock().status_contains(&[Status::Exec]);
|
||||
if exec {
|
||||
self.exec();
|
||||
self.exec().await;
|
||||
}
|
||||
|
||||
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))]
|
||||
fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> {
|
||||
Layout::default()
|
||||
|
||||
Reference in New Issue
Block a user