mirror of
https://github.com/wez/wezterm.git
synced 2024-12-23 05:12:40 +03:00
mux: track list of clients
Define a way to compute a client ID and pass that through to the mux server when verifying version compatibility. Once associated, the session handler will keep some metadata updated in the mux. A new cli subcommand exposes the info: ``` ; ./target/debug/wezterm cli list-clients USER HOST PID CONNECTED IDLE WORKSPACE wez mba.localdomain 52979 30.009225s 1.009225s ``` refs: #1531
This commit is contained in:
parent
e314d84711
commit
9b9bd0ae8c
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -533,6 +533,7 @@ dependencies = [
|
|||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"pure-rust-locales",
|
"pure-rust-locales",
|
||||||
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
@ -2290,10 +2291,12 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"base64",
|
"base64",
|
||||||
"bintree",
|
"bintree",
|
||||||
|
"chrono",
|
||||||
"config",
|
"config",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
"downcast-rs",
|
"downcast-rs",
|
||||||
"filedescriptor",
|
"filedescriptor",
|
||||||
|
"hostname",
|
||||||
"k9",
|
"k9",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
@ -4614,6 +4617,7 @@ name = "wezterm"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
"codec",
|
"codec",
|
||||||
"config",
|
"config",
|
||||||
"env-bootstrap",
|
"env-bootstrap",
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
#![cfg_attr(feature = "cargo-clippy", allow(clippy::range_plus_one))]
|
#![cfg_attr(feature = "cargo-clippy", allow(clippy::range_plus_one))]
|
||||||
|
|
||||||
use anyhow::{bail, Context as _, Error};
|
use anyhow::{bail, Context as _, Error};
|
||||||
use leb128;
|
use mux::client::{ClientId, ClientInfo};
|
||||||
use mux::domain::DomainId;
|
use mux::domain::DomainId;
|
||||||
use mux::pane::PaneId;
|
use mux::pane::PaneId;
|
||||||
use mux::renderable::{RenderableDimensions, StableCursorPosition};
|
use mux::renderable::{RenderableDimensions, StableCursorPosition};
|
||||||
@ -406,7 +406,7 @@ macro_rules! pdu {
|
|||||||
/// The overall version of the codec.
|
/// The overall version of the codec.
|
||||||
/// This must be bumped when backwards incompatible changes
|
/// This must be bumped when backwards incompatible changes
|
||||||
/// are made to the types and protocol.
|
/// are made to the types and protocol.
|
||||||
pub const CODEC_VERSION: usize = 13;
|
pub const CODEC_VERSION: usize = 14;
|
||||||
|
|
||||||
// Defines the Pdu enum.
|
// Defines the Pdu enum.
|
||||||
// Each struct has an explicit identifying number.
|
// Each struct has an explicit identifying number.
|
||||||
@ -445,6 +445,9 @@ pdu! {
|
|||||||
PaneRemoved: 37,
|
PaneRemoved: 37,
|
||||||
SetPalette: 38,
|
SetPalette: 38,
|
||||||
NotifyAlert: 39,
|
NotifyAlert: 39,
|
||||||
|
SetClientId: 40,
|
||||||
|
GetClientList: 41,
|
||||||
|
GetClientListResponse: 42,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pdu {
|
impl Pdu {
|
||||||
@ -697,6 +700,19 @@ pub struct NotifyAlert {
|
|||||||
pub alert: Alert,
|
pub alert: Alert,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||||
|
pub struct SetClientId {
|
||||||
|
pub client_id: ClientId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||||
|
pub struct GetClientList;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||||
|
pub struct GetClientListResponse {
|
||||||
|
pub clients: Vec<ClientInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
#[derive(Deserialize, Serialize, PartialEq, Debug)]
|
||||||
pub struct Resize {
|
pub struct Resize {
|
||||||
pub containing_tab_id: TabId,
|
pub containing_tab_id: TabId,
|
||||||
|
@ -11,10 +11,12 @@ anyhow = "1.0"
|
|||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
bintree = { path = "../bintree" }
|
bintree = { path = "../bintree" }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
config = { path = "../config" }
|
config = { path = "../config" }
|
||||||
crossbeam = "0.8"
|
crossbeam = "0.8"
|
||||||
downcast-rs = "1.0"
|
downcast-rs = "1.0"
|
||||||
filedescriptor = { version="0.8", path = "../filedescriptor" }
|
filedescriptor = { version="0.8", path = "../filedescriptor" }
|
||||||
|
hostname = "0.3"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
64
mux/src/client.rs
Normal file
64
mux/src/client.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use chrono::serde::ts_seconds;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::*;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
static CLIENT_ID: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref EPOCH: u64 = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap().as_secs();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ClientId {
|
||||||
|
pub hostname: String,
|
||||||
|
pub username: String,
|
||||||
|
pub pid: u32,
|
||||||
|
pub epoch: u64,
|
||||||
|
pub id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientId {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let id = CLIENT_ID.fetch_add(1, Ordering::Relaxed);
|
||||||
|
Self {
|
||||||
|
hostname: hostname::get()
|
||||||
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|_| "localhost".to_string()),
|
||||||
|
username: config::username_from_env().unwrap_or_else(|_| "somebody".to_string()),
|
||||||
|
pid: unsafe { libc::getpid() as u32 },
|
||||||
|
epoch: *EPOCH,
|
||||||
|
id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
|
||||||
|
pub struct ClientInfo {
|
||||||
|
pub client_id: ClientId,
|
||||||
|
/// The time this client last connected
|
||||||
|
#[serde(with = "ts_seconds")]
|
||||||
|
pub connected_at: DateTime<Utc>,
|
||||||
|
/// Which workspace is active
|
||||||
|
pub active_workspace: Option<String>,
|
||||||
|
/// The last time we received input from this client
|
||||||
|
#[serde(with = "ts_seconds")]
|
||||||
|
pub last_input: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientInfo {
|
||||||
|
pub fn new(client_id: &ClientId) -> Self {
|
||||||
|
Self {
|
||||||
|
client_id: client_id.clone(),
|
||||||
|
connected_at: Utc::now(),
|
||||||
|
active_workspace: None,
|
||||||
|
last_input: Utc::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_last_input(&mut self) {
|
||||||
|
self.last_input = Utc::now();
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::client::{ClientId, ClientInfo};
|
||||||
use crate::pane::{Pane, PaneId};
|
use crate::pane::{Pane, PaneId};
|
||||||
use crate::tab::{Tab, TabId};
|
use crate::tab::{Tab, TabId};
|
||||||
use crate::window::{Window, WindowId};
|
use crate::window::{Window, WindowId};
|
||||||
@ -25,6 +26,7 @@ use thiserror::*;
|
|||||||
use winapi::um::winsock2::{SOL_SOCKET, SO_RCVBUF, SO_SNDBUF};
|
use winapi::um::winsock2::{SOL_SOCKET, SO_RCVBUF, SO_SNDBUF};
|
||||||
|
|
||||||
pub mod activity;
|
pub mod activity;
|
||||||
|
pub mod client;
|
||||||
pub mod connui;
|
pub mod connui;
|
||||||
pub mod domain;
|
pub mod domain;
|
||||||
pub mod localpane;
|
pub mod localpane;
|
||||||
@ -64,6 +66,7 @@ pub struct Mux {
|
|||||||
domains_by_name: RefCell<HashMap<String, Arc<dyn Domain>>>,
|
domains_by_name: RefCell<HashMap<String, Arc<dyn Domain>>>,
|
||||||
subscribers: RefCell<HashMap<usize, Box<dyn Fn(MuxNotification) -> bool>>>,
|
subscribers: RefCell<HashMap<usize, Box<dyn Fn(MuxNotification) -> bool>>>,
|
||||||
banner: RefCell<Option<String>>,
|
banner: RefCell<Option<String>>,
|
||||||
|
clients: RefCell<HashMap<ClientId, ClientInfo>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const BUFSIZE: usize = 1024 * 1024;
|
const BUFSIZE: usize = 1024 * 1024;
|
||||||
@ -317,9 +320,34 @@ impl Mux {
|
|||||||
domains: RefCell::new(domains),
|
domains: RefCell::new(domains),
|
||||||
subscribers: RefCell::new(HashMap::new()),
|
subscribers: RefCell::new(HashMap::new()),
|
||||||
banner: RefCell::new(None),
|
banner: RefCell::new(None),
|
||||||
|
clients: RefCell::new(HashMap::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn client_had_input(&self, client_id: &ClientId) {
|
||||||
|
if let Some(info) = self.clients.borrow_mut().get_mut(client_id) {
|
||||||
|
info.update_last_input();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_client(&self, client_id: &ClientId) {
|
||||||
|
self.clients
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(client_id.clone(), ClientInfo::new(client_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter_clients(&self) -> Vec<ClientInfo> {
|
||||||
|
self.clients
|
||||||
|
.borrow()
|
||||||
|
.values()
|
||||||
|
.map(|info| info.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unregister_client(&self, client_id: &ClientId) {
|
||||||
|
self.clients.borrow_mut().remove(client_id);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn subscribe<F>(&self, subscriber: F)
|
pub fn subscribe<F>(&self, subscriber: F)
|
||||||
where
|
where
|
||||||
F: Fn(MuxNotification) -> bool + 'static,
|
F: Fn(MuxNotification) -> bool + 'static,
|
||||||
|
@ -8,6 +8,7 @@ use codec::*;
|
|||||||
use config::{configuration, SshDomain, TlsDomainClient, UnixDomain, UnixTarget};
|
use config::{configuration, SshDomain, TlsDomainClient, UnixDomain, UnixTarget};
|
||||||
use filedescriptor::FileDescriptor;
|
use filedescriptor::FileDescriptor;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
|
use mux::client::ClientId;
|
||||||
use mux::connui::ConnectionUI;
|
use mux::connui::ConnectionUI;
|
||||||
use mux::domain::DomainId;
|
use mux::domain::DomainId;
|
||||||
use mux::pane::PaneId;
|
use mux::pane::PaneId;
|
||||||
@ -41,6 +42,7 @@ enum ReaderMessage {
|
|||||||
pub struct Client {
|
pub struct Client {
|
||||||
sender: Sender<ReaderMessage>,
|
sender: Sender<ReaderMessage>,
|
||||||
local_domain_id: Option<DomainId>,
|
local_domain_id: Option<DomainId>,
|
||||||
|
client_id: ClientId,
|
||||||
pub is_reconnectable: bool,
|
pub is_reconnectable: bool,
|
||||||
pub is_local: bool,
|
pub is_local: bool,
|
||||||
}
|
}
|
||||||
@ -865,6 +867,7 @@ impl Client {
|
|||||||
let is_reconnectable = reconnectable.reconnectable();
|
let is_reconnectable = reconnectable.reconnectable();
|
||||||
let is_local = reconnectable.is_local();
|
let is_local = reconnectable.is_local();
|
||||||
let (sender, mut receiver) = unbounded();
|
let (sender, mut receiver) = unbounded();
|
||||||
|
let client_id = ClientId::new();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
const BASE_INTERVAL: Duration = Duration::from_secs(1);
|
const BASE_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
@ -957,6 +960,7 @@ impl Client {
|
|||||||
local_domain_id,
|
local_domain_id,
|
||||||
is_reconnectable,
|
is_reconnectable,
|
||||||
is_local,
|
is_local,
|
||||||
|
client_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -971,6 +975,10 @@ impl Client {
|
|||||||
info.version_string,
|
info.version_string,
|
||||||
info.codec_vers
|
info.codec_vers
|
||||||
);
|
);
|
||||||
|
self.set_client_id(SetClientId {
|
||||||
|
client_id: self.client_id.clone(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
Ok(info)
|
Ok(info)
|
||||||
}
|
}
|
||||||
Ok(info) => {
|
Ok(info) => {
|
||||||
@ -1117,4 +1125,6 @@ impl Client {
|
|||||||
SearchScrollbackResponse
|
SearchScrollbackResponse
|
||||||
);
|
);
|
||||||
rpc!(kill_pane, KillPane, UnitResponse);
|
rpc!(kill_pane, KillPane, UnitResponse);
|
||||||
|
rpc!(set_client_id, SetClientId, UnitResponse);
|
||||||
|
rpc!(list_clients, GetClientList, GetClientListResponse);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ use crate::PKI;
|
|||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use codec::*;
|
use codec::*;
|
||||||
use config::keyassignment::SpawnTabDomain;
|
use config::keyassignment::SpawnTabDomain;
|
||||||
|
use mux::client::ClientId;
|
||||||
use mux::pane::{Pane, PaneId};
|
use mux::pane::{Pane, PaneId};
|
||||||
use mux::renderable::{RenderableDimensions, StableCursorPosition};
|
use mux::renderable::{RenderableDimensions, StableCursorPosition};
|
||||||
use mux::tab::TabId;
|
use mux::tab::TabId;
|
||||||
@ -196,6 +197,16 @@ fn maybe_push_pane_changes(
|
|||||||
pub struct SessionHandler {
|
pub struct SessionHandler {
|
||||||
to_write_tx: PduSender,
|
to_write_tx: PduSender,
|
||||||
per_pane: HashMap<TabId, Arc<Mutex<PerPane>>>,
|
per_pane: HashMap<TabId, Arc<Mutex<PerPane>>>,
|
||||||
|
client_id: Option<ClientId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SessionHandler {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(client_id) = self.client_id.take() {
|
||||||
|
let mux = Mux::get().unwrap();
|
||||||
|
mux.unregister_client(&client_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionHandler {
|
impl SessionHandler {
|
||||||
@ -214,6 +225,7 @@ impl SessionHandler {
|
|||||||
Self {
|
Self {
|
||||||
to_write_tx,
|
to_write_tx,
|
||||||
per_pane: HashMap::new(),
|
per_pane: HashMap::new(),
|
||||||
|
client_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,6 +256,10 @@ impl SessionHandler {
|
|||||||
let sender = self.to_write_tx.clone();
|
let sender = self.to_write_tx.clone();
|
||||||
let serial = decoded.serial;
|
let serial = decoded.serial;
|
||||||
|
|
||||||
|
if let Some(client_id) = &self.client_id {
|
||||||
|
Mux::get().unwrap().client_had_input(client_id);
|
||||||
|
}
|
||||||
|
|
||||||
let send_response = move |result: anyhow::Result<Pdu>| {
|
let send_response = move |result: anyhow::Result<Pdu>| {
|
||||||
let pdu = match result {
|
let pdu = match result {
|
||||||
Ok(pdu) => pdu,
|
Ok(pdu) => pdu,
|
||||||
@ -265,6 +281,30 @@ impl SessionHandler {
|
|||||||
|
|
||||||
match decoded.pdu {
|
match decoded.pdu {
|
||||||
Pdu::Ping(Ping {}) => send_response(Ok(Pdu::Pong(Pong {}))),
|
Pdu::Ping(Ping {}) => send_response(Ok(Pdu::Pong(Pong {}))),
|
||||||
|
Pdu::SetClientId(SetClientId { client_id }) => {
|
||||||
|
self.client_id.replace(client_id.clone());
|
||||||
|
spawn_into_main_thread(async move {
|
||||||
|
let mux = Mux::get().unwrap();
|
||||||
|
mux.register_client(&client_id);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
send_response(Ok(Pdu::UnitResponse(UnitResponse {})))
|
||||||
|
}
|
||||||
|
Pdu::GetClientList(GetClientList) => {
|
||||||
|
spawn_into_main_thread(async move {
|
||||||
|
catch(
|
||||||
|
move || {
|
||||||
|
let mux = Mux::get().unwrap();
|
||||||
|
let clients = mux.iter_clients();
|
||||||
|
Ok(Pdu::GetClientListResponse(GetClientListResponse {
|
||||||
|
clients,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
send_response,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
Pdu::ListPanes(ListPanes {}) => {
|
Pdu::ListPanes(ListPanes {}) => {
|
||||||
spawn_into_main_thread(async move {
|
spawn_into_main_thread(async move {
|
||||||
catch(
|
catch(
|
||||||
@ -593,6 +633,7 @@ impl SessionHandler {
|
|||||||
| Pdu::GetLinesResponse { .. }
|
| Pdu::GetLinesResponse { .. }
|
||||||
| Pdu::GetCodecVersionResponse { .. }
|
| Pdu::GetCodecVersionResponse { .. }
|
||||||
| Pdu::GetTlsCredsResponse { .. }
|
| Pdu::GetTlsCredsResponse { .. }
|
||||||
|
| Pdu::GetClientListResponse { .. }
|
||||||
| Pdu::PaneRemoved { .. }
|
| Pdu::PaneRemoved { .. }
|
||||||
| Pdu::ErrorResponse { .. } => {
|
| Pdu::ErrorResponse { .. } => {
|
||||||
send_response(Err(anyhow!("expected a request, got {:?}", decoded.pdu)))
|
send_response(Err(anyhow!("expected a request, got {:?}", decoded.pdu)))
|
||||||
|
@ -11,6 +11,7 @@ anyhow = "1.0"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
chrono = "0.4"
|
||||||
codec = { path = "../codec" }
|
codec = { path = "../codec" }
|
||||||
config = { path = "../config" }
|
config = { path = "../config" }
|
||||||
env-bootstrap = { path = "../env-bootstrap" }
|
env-bootstrap = { path = "../env-bootstrap" }
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use config::keyassignment::SpawnTabDomain;
|
use config::keyassignment::SpawnTabDomain;
|
||||||
use config::wezterm_version;
|
use config::wezterm_version;
|
||||||
use mux::activity::Activity;
|
use mux::activity::Activity;
|
||||||
@ -112,6 +113,9 @@ enum CliSubCommand {
|
|||||||
#[structopt(name = "list", about = "list windows, tabs and panes")]
|
#[structopt(name = "list", about = "list windows, tabs and panes")]
|
||||||
List,
|
List,
|
||||||
|
|
||||||
|
#[structopt(name = "list-clients", about = "list clients")]
|
||||||
|
ListClients,
|
||||||
|
|
||||||
#[structopt(name = "proxy", about = "start rpc proxy pipe")]
|
#[structopt(name = "proxy", about = "start rpc proxy pipe")]
|
||||||
Proxy,
|
Proxy,
|
||||||
|
|
||||||
@ -400,6 +404,60 @@ async fn run_cli_async(config: config::ConfigHandle, cli: CliCommand) -> anyhow:
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
match cli.sub {
|
match cli.sub {
|
||||||
|
CliSubCommand::ListClients => {
|
||||||
|
let cols = vec![
|
||||||
|
Column {
|
||||||
|
name: "USER".to_string(),
|
||||||
|
alignment: Alignment::Left,
|
||||||
|
},
|
||||||
|
Column {
|
||||||
|
name: "HOST".to_string(),
|
||||||
|
alignment: Alignment::Left,
|
||||||
|
},
|
||||||
|
Column {
|
||||||
|
name: "PID".to_string(),
|
||||||
|
alignment: Alignment::Right,
|
||||||
|
},
|
||||||
|
Column {
|
||||||
|
name: "CONNECTED".to_string(),
|
||||||
|
alignment: Alignment::Left,
|
||||||
|
},
|
||||||
|
Column {
|
||||||
|
name: "IDLE".to_string(),
|
||||||
|
alignment: Alignment::Left,
|
||||||
|
},
|
||||||
|
Column {
|
||||||
|
name: "WORKSPACE".to_string(),
|
||||||
|
alignment: Alignment::Left,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let mut data = vec![];
|
||||||
|
let clients = client.list_clients(codec::GetClientList).await?;
|
||||||
|
let now: DateTime<Utc> = Utc::now();
|
||||||
|
|
||||||
|
fn duration_string(d: chrono::Duration) -> String {
|
||||||
|
if let Ok(d) = d.to_std() {
|
||||||
|
format!("{:?}", d)
|
||||||
|
} else {
|
||||||
|
d.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for info in clients.clients {
|
||||||
|
let connected = now - info.connected_at;
|
||||||
|
let idle = now - info.last_input;
|
||||||
|
data.push(vec![
|
||||||
|
info.client_id.username.to_string(),
|
||||||
|
info.client_id.hostname.to_string(),
|
||||||
|
info.client_id.pid.to_string(),
|
||||||
|
duration_string(connected),
|
||||||
|
duration_string(idle),
|
||||||
|
info.active_workspace.as_deref().unwrap_or("").to_string(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
tabulate_output(&cols, &data, &mut std::io::stdout().lock())?;
|
||||||
|
}
|
||||||
CliSubCommand::List => {
|
CliSubCommand::List => {
|
||||||
let cols = vec![
|
let cols = vec![
|
||||||
Column {
|
Column {
|
||||||
|
Loading…
Reference in New Issue
Block a user