1
1
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:
Wez Furlong 2022-01-11 22:39:19 -07:00
parent e314d84711
commit 9b9bd0ae8c
9 changed files with 226 additions and 2 deletions

4
Cargo.lock generated
View File

@ -533,6 +533,7 @@ dependencies = [
"num-integer",
"num-traits",
"pure-rust-locales",
"serde",
"time",
"winapi 0.3.9",
]
@ -2290,10 +2291,12 @@ dependencies = [
"async-trait",
"base64",
"bintree",
"chrono",
"config",
"crossbeam",
"downcast-rs",
"filedescriptor",
"hostname",
"k9",
"lazy_static",
"libc",
@ -4614,6 +4617,7 @@ name = "wezterm"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"codec",
"config",
"env-bootstrap",

View File

@ -12,7 +12,7 @@
#![cfg_attr(feature = "cargo-clippy", allow(clippy::range_plus_one))]
use anyhow::{bail, Context as _, Error};
use leb128;
use mux::client::{ClientId, ClientInfo};
use mux::domain::DomainId;
use mux::pane::PaneId;
use mux::renderable::{RenderableDimensions, StableCursorPosition};
@ -406,7 +406,7 @@ macro_rules! pdu {
/// The overall version of the codec.
/// This must be bumped when backwards incompatible changes
/// are made to the types and protocol.
pub const CODEC_VERSION: usize = 13;
pub const CODEC_VERSION: usize = 14;
// Defines the Pdu enum.
// Each struct has an explicit identifying number.
@ -445,6 +445,9 @@ pdu! {
PaneRemoved: 37,
SetPalette: 38,
NotifyAlert: 39,
SetClientId: 40,
GetClientList: 41,
GetClientListResponse: 42,
}
impl Pdu {
@ -697,6 +700,19 @@ pub struct NotifyAlert {
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)]
pub struct Resize {
pub containing_tab_id: TabId,

View File

@ -11,10 +11,12 @@ anyhow = "1.0"
async-trait = "0.1"
base64 = "0.13"
bintree = { path = "../bintree" }
chrono = { version = "0.4", features = ["serde"] }
config = { path = "../config" }
crossbeam = "0.8"
downcast-rs = "1.0"
filedescriptor = { version="0.8", path = "../filedescriptor" }
hostname = "0.3"
lazy_static = "1.4"
libc = "0.2"
log = "0.4"

64
mux/src/client.rs Normal file
View 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();
}
}

View File

@ -1,3 +1,4 @@
use crate::client::{ClientId, ClientInfo};
use crate::pane::{Pane, PaneId};
use crate::tab::{Tab, TabId};
use crate::window::{Window, WindowId};
@ -25,6 +26,7 @@ use thiserror::*;
use winapi::um::winsock2::{SOL_SOCKET, SO_RCVBUF, SO_SNDBUF};
pub mod activity;
pub mod client;
pub mod connui;
pub mod domain;
pub mod localpane;
@ -64,6 +66,7 @@ pub struct Mux {
domains_by_name: RefCell<HashMap<String, Arc<dyn Domain>>>,
subscribers: RefCell<HashMap<usize, Box<dyn Fn(MuxNotification) -> bool>>>,
banner: RefCell<Option<String>>,
clients: RefCell<HashMap<ClientId, ClientInfo>>,
}
const BUFSIZE: usize = 1024 * 1024;
@ -317,9 +320,34 @@ impl Mux {
domains: RefCell::new(domains),
subscribers: RefCell::new(HashMap::new()),
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)
where
F: Fn(MuxNotification) -> bool + 'static,

View File

@ -8,6 +8,7 @@ use codec::*;
use config::{configuration, SshDomain, TlsDomainClient, UnixDomain, UnixTarget};
use filedescriptor::FileDescriptor;
use futures::FutureExt;
use mux::client::ClientId;
use mux::connui::ConnectionUI;
use mux::domain::DomainId;
use mux::pane::PaneId;
@ -41,6 +42,7 @@ enum ReaderMessage {
pub struct Client {
sender: Sender<ReaderMessage>,
local_domain_id: Option<DomainId>,
client_id: ClientId,
pub is_reconnectable: bool,
pub is_local: bool,
}
@ -865,6 +867,7 @@ impl Client {
let is_reconnectable = reconnectable.reconnectable();
let is_local = reconnectable.is_local();
let (sender, mut receiver) = unbounded();
let client_id = ClientId::new();
thread::spawn(move || {
const BASE_INTERVAL: Duration = Duration::from_secs(1);
@ -957,6 +960,7 @@ impl Client {
local_domain_id,
is_reconnectable,
is_local,
client_id,
}
}
@ -971,6 +975,10 @@ impl Client {
info.version_string,
info.codec_vers
);
self.set_client_id(SetClientId {
client_id: self.client_id.clone(),
})
.await?;
Ok(info)
}
Ok(info) => {
@ -1117,4 +1125,6 @@ impl Client {
SearchScrollbackResponse
);
rpc!(kill_pane, KillPane, UnitResponse);
rpc!(set_client_id, SetClientId, UnitResponse);
rpc!(list_clients, GetClientList, GetClientListResponse);
}

View File

@ -2,6 +2,7 @@ use crate::PKI;
use anyhow::{anyhow, Context};
use codec::*;
use config::keyassignment::SpawnTabDomain;
use mux::client::ClientId;
use mux::pane::{Pane, PaneId};
use mux::renderable::{RenderableDimensions, StableCursorPosition};
use mux::tab::TabId;
@ -196,6 +197,16 @@ fn maybe_push_pane_changes(
pub struct SessionHandler {
to_write_tx: PduSender,
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 {
@ -214,6 +225,7 @@ impl SessionHandler {
Self {
to_write_tx,
per_pane: HashMap::new(),
client_id: None,
}
}
@ -244,6 +256,10 @@ impl SessionHandler {
let sender = self.to_write_tx.clone();
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 pdu = match result {
Ok(pdu) => pdu,
@ -265,6 +281,30 @@ impl SessionHandler {
match decoded.pdu {
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 {}) => {
spawn_into_main_thread(async move {
catch(
@ -593,6 +633,7 @@ impl SessionHandler {
| Pdu::GetLinesResponse { .. }
| Pdu::GetCodecVersionResponse { .. }
| Pdu::GetTlsCredsResponse { .. }
| Pdu::GetClientListResponse { .. }
| Pdu::PaneRemoved { .. }
| Pdu::ErrorResponse { .. } => {
send_response(Err(anyhow!("expected a request, got {:?}", decoded.pdu)))

View File

@ -11,6 +11,7 @@ anyhow = "1.0"
[dependencies]
anyhow = "1.0"
chrono = "0.4"
codec = { path = "../codec" }
config = { path = "../config" }
env-bootstrap = { path = "../env-bootstrap" }

View File

@ -1,4 +1,5 @@
use anyhow::{anyhow, Context};
use chrono::{DateTime, Utc};
use config::keyassignment::SpawnTabDomain;
use config::wezterm_version;
use mux::activity::Activity;
@ -112,6 +113,9 @@ enum CliSubCommand {
#[structopt(name = "list", about = "list windows, tabs and panes")]
List,
#[structopt(name = "list-clients", about = "list clients")]
ListClients,
#[structopt(name = "proxy", about = "start rpc proxy pipe")]
Proxy,
@ -400,6 +404,60 @@ async fn run_cli_async(config: config::ConfigHandle, cli: CliCommand) -> anyhow:
)?;
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 => {
let cols = vec![
Column {