use std::io;
use clap::{CommandFactory, Parser, Subcommand};
use term_session::auto_spawn::connect_or_spawn_server;
use term_session_client::run_session;
use term_session_muxio_service_definitions::ChannelName;
#[derive(Parser, Debug)]
#[command(
name = env!("CARGO_PKG_NAME"),
version = env!("CARGO_PKG_VERSION"),
about = env!("CARGO_PKG_DESCRIPTION"),
long_about = concat!(env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION"), ": ", env!("CARGO_PKG_DESCRIPTION")),
)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[arg(long, hide = true)]
daemon: bool,
#[arg(long, hide = true)]
daemon_selfcheck: Option<std::path::PathBuf>,
#[arg(long)]
channel: Option<String>,
#[arg(value_name = "CMD", num_args = 0.., trailing_var_arg = true, allow_hyphen_values = true)]
cmd: Vec<String>,
#[arg(long)]
gateway: Option<String>,
}
#[derive(Subcommand, Debug)]
enum Command {
#[command(name = "ls", alias = "list")]
List,
Kill {
channel: String,
#[arg(long)]
kill_session: bool,
},
#[command(name = "kill-client")]
KillClient {
channel: String,
client_id: usize,
},
#[command(name = "stop")]
Stop {
#[arg(long)]
force: bool,
},
}
fn main() {
if let Err(e) = run() {
eprintln!("error: {e}");
std::process::exit(1);
}
}
fn run() -> io::Result<()> {
let cli = Cli::parse();
if let Some(ref gw) = cli.gateway {
unsafe {
std::env::set_var("TERM_WM_GATEWAY", gw);
}
}
if cli.daemon {
return term_session::run_daemon(cli.daemon_selfcheck);
}
match cli.command {
Some(Command::List) => list(),
Some(Command::Kill {
channel,
kill_session: _,
}) => kill(&channel),
Some(Command::KillClient { channel, client_id }) => {
term_session::kill_client(&channel, client_id)?;
println!("Detached client {client_id} from channel {channel}");
Ok(())
}
Some(Command::Stop { force }) => stop(force),
None => {
if cli.channel.is_some() || !cli.cmd.is_empty() {
attach(cli.channel, &cli.cmd)
} else {
let mut stderr = io::stderr();
let _ = Cli::command().write_long_help(&mut stderr);
std::process::exit(2);
}
}
}
}
fn attach(channel: Option<String>, cmd: &[String]) -> io::Result<()> {
let channel_str = term_session::resolve_channel(channel);
let channel = ChannelName::parse(&channel_str).map_err(|e| {
io::Error::new(io::ErrorKind::InvalidInput, format!("Invalid channel: {e}"))
})?;
let socket_name = connect_or_spawn_server(None)?;
run_session(&socket_name, &channel.to_string(), cmd)
}
fn list() -> io::Result<()> {
let resp = term_session::list_channels()?;
println!(
"Gateway Daemon PID: {} | Socket: {}",
resp.gateway_pid, resp.socket
);
if resp.channels.is_empty() {
println!("\nNo channels.");
return Ok(());
}
for ch in &resp.channels {
let session = ch
.session
.as_ref()
.map(|s| format!("session size: {}x{}", s.cols, s.rows))
.unwrap_or_else(|| "none".to_string());
let nclients = ch.clients.len();
println!();
println!("channel: {}", ch.name);
println!(
" created: {}",
term_session::format_unix_relative(ch.created_at_unix)
);
println!(" session: {}", session);
println!(
" clients: {}",
if nclients == 0 {
"none".to_string()
} else {
format!("{nclients} connected")
}
);
for c in &ch.clients {
println!(" - conn: {} (pid {})", c.conn_id, c.pid);
println!(" user: {}", c.user);
println!(" version: {}", c.version);
if let Some(ip) = &c.ssh_ip {
println!(" ssh ip from: {}", ip);
}
println!(" host: {}", c.hostname);
println!(" size: {}x{}", c.cols, c.rows);
println!(
" connected: {}",
term_session::format_unix_relative(c.connected_at_unix)
);
}
}
Ok(())
}
fn kill(channel: &str) -> io::Result<()> {
term_session::kill_channel(channel)?;
println!("Killed channel {channel}");
Ok(())
}
fn stop(force: bool) -> io::Result<()> {
term_session::stop_gateway(force)?;
println!("Gateway shutdown initiated.");
Ok(())
}