Merge branch 'feat/tty' into dev
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -45,32 +45,17 @@ 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
|
||||||
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 ##
|
## 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
@@ -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
@@ -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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
@@ -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,
|
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)
|
self.app_data.lock().set_error(
|
||||||
.args(["exec", id.get(), "pwd"])
|
AppError::DockerExec,
|
||||||
.output()
|
&self.gui_state,
|
||||||
{
|
Status::Error,
|
||||||
if let Ok(output) = String::from_utf8(output.stdout) {
|
);
|
||||||
if !output.starts_with(OCI_ERROR) {
|
},
|
||||||
exec_err = None;
|
|mode| {
|
||||||
}
|
self.gui_state.lock().set_exec_mode(mode);
|
||||||
}
|
},
|
||||||
}
|
);
|
||||||
|
|
||||||
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);
|
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 => {
|
||||||
|
|||||||
+27
-22
@@ -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,33 +146,33 @@ 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
|
||||||
while is_running.load(Ordering::SeqCst) {
|
while is_running.load(Ordering::SeqCst) {
|
||||||
if let Some(err) = app_data.lock().get_error() {
|
if let Some(err) = app_data.lock().get_error() {
|
||||||
error!("{}", err);
|
error!("{}", err);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
|
tokio::time::sleep(std::time::Duration::from_millis(u64::from(
|
||||||
args.docker_interval,
|
args.docker_interval,
|
||||||
)))
|
)))
|
||||||
.await;
|
.await;
|
||||||
let containers = app_data
|
let containers = app_data
|
||||||
.lock()
|
.lock()
|
||||||
.get_container_items()
|
.get_container_items()
|
||||||
.clone()
|
.clone()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| format!("{i}"))
|
.map(|i| format!("{i}"))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if !containers.is_empty() {
|
if !containers.is_empty() {
|
||||||
for item in containers {
|
for item in containers {
|
||||||
info!("{item}");
|
info!("{item}");
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-5
@@ -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(),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+33
-4
@@ -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,16 +269,41 @@ 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 {
|
||||||
self.status.remove(&Status::DeleteConfirm);
|
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) {
|
||||||
self.status.insert(status);
|
match status {
|
||||||
|
Status::Exec => (),
|
||||||
|
_ => {
|
||||||
|
self.status.insert(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Change to next selectable panel
|
/// Change to next selectable panel
|
||||||
|
|||||||
+23
-31
@@ -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() {
|
self.reset_terminal().ok();
|
||||||
if let Ok(mut child) = std::process::Command::new(DOCKER_COMMAND)
|
self.terminal.clear().ok();
|
||||||
.args(["exec", "-it", id.get(), "sh"])
|
if let Err(e) = mode.run(&self.app_data, &self.gui_state).await {
|
||||||
.stdin(std::process::Stdio::inherit())
|
self.app_data
|
||||||
.stdout(std::process::Stdio::inherit())
|
.lock()
|
||||||
.stderr(std::process::Stdio::inherit())
|
.set_error(e, &self.gui_state, Status::Error);
|
||||||
.spawn()
|
};
|
||||||
{
|
|
||||||
self.reset_terminal().ok();
|
|
||||||
child.wait().ok();
|
|
||||||
if child.kill().is_err() {
|
|
||||||
std::process::exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user