mirror of https://github.com/chipsenkbeil/distant
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
470 lines
16 KiB
Rust
470 lines
16 KiB
Rust
use crate::{
|
|
cli::{Cache, Client, Manager},
|
|
config::{AccessControl, ManagerConfig, NetworkConfig},
|
|
paths::user::CACHE_FILE_PATH_STR,
|
|
CliResult,
|
|
};
|
|
use anyhow::Context;
|
|
use clap::{Subcommand, ValueHint};
|
|
use distant_core::{net::ServerRef, ConnectionId, DistantManagerConfig};
|
|
use log::*;
|
|
use once_cell::sync::Lazy;
|
|
use service_manager::{
|
|
ServiceInstallCtx, ServiceLabel, ServiceLevel, ServiceManager, ServiceManagerKind,
|
|
ServiceStartCtx, ServiceStopCtx, ServiceUninstallCtx,
|
|
};
|
|
use std::{ffi::OsString, path::PathBuf};
|
|
use tabled::{Table, Tabled};
|
|
|
|
/// [`ServiceLabel`] for our manager in the form `rocks.distant.manager`
|
|
static SERVICE_LABEL: Lazy<ServiceLabel> = Lazy::new(|| ServiceLabel {
|
|
qualifier: String::from("rocks"),
|
|
organization: String::from("distant"),
|
|
application: String::from("manager"),
|
|
});
|
|
|
|
mod handlers;
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum ManagerSubcommand {
|
|
/// Interact with a manager being run by a service management platform
|
|
#[clap(subcommand)]
|
|
Service(ManagerServiceSubcommand),
|
|
|
|
/// Listen for incoming requests as a manager
|
|
Listen {
|
|
/// Type of access to apply to created unix socket or windows pipe
|
|
#[clap(long, value_enum)]
|
|
access: Option<AccessControl>,
|
|
|
|
/// If specified, will fork the process to run as a standalone daemon
|
|
#[clap(long)]
|
|
daemon: bool,
|
|
|
|
/// If specified, will listen on a user-local unix socket or local windows named pipe
|
|
#[clap(long)]
|
|
user: bool,
|
|
|
|
#[clap(flatten)]
|
|
network: NetworkConfig,
|
|
},
|
|
|
|
/// Retrieve a list of capabilities that the manager supports
|
|
Capabilities {
|
|
#[clap(flatten)]
|
|
network: NetworkConfig,
|
|
},
|
|
|
|
/// Retrieve information about a specific connection
|
|
Info {
|
|
id: ConnectionId,
|
|
#[clap(flatten)]
|
|
network: NetworkConfig,
|
|
},
|
|
|
|
/// List information about all connections
|
|
List {
|
|
#[clap(flatten)]
|
|
network: NetworkConfig,
|
|
|
|
/// Location to store cached data
|
|
#[clap(
|
|
long,
|
|
value_hint = ValueHint::FilePath,
|
|
value_parser,
|
|
default_value = CACHE_FILE_PATH_STR.as_str()
|
|
)]
|
|
cache: PathBuf,
|
|
},
|
|
|
|
/// Kill a specific connection
|
|
Kill {
|
|
#[clap(flatten)]
|
|
network: NetworkConfig,
|
|
id: ConnectionId,
|
|
},
|
|
|
|
/// Send a shutdown request to the manager
|
|
Shutdown {
|
|
#[clap(flatten)]
|
|
network: NetworkConfig,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum ManagerServiceSubcommand {
|
|
/// Start the manager as a service
|
|
Start {
|
|
/// Type of service manager used to run this service, defaulting to platform native
|
|
#[clap(long, value_enum)]
|
|
kind: Option<ServiceManagerKind>,
|
|
|
|
/// If specified, starts as a user-level service
|
|
#[clap(long)]
|
|
user: bool,
|
|
},
|
|
|
|
/// Stop the manager as a service
|
|
Stop {
|
|
#[clap(long, value_enum)]
|
|
kind: Option<ServiceManagerKind>,
|
|
|
|
/// If specified, stops a user-level service
|
|
#[clap(long)]
|
|
user: bool,
|
|
},
|
|
|
|
/// Install the manager as a service
|
|
Install {
|
|
#[clap(long, value_enum)]
|
|
kind: Option<ServiceManagerKind>,
|
|
|
|
/// If specified, installs as a user-level service
|
|
#[clap(long)]
|
|
user: bool,
|
|
},
|
|
|
|
/// Uninstall the manager as a service
|
|
Uninstall {
|
|
#[clap(long, value_enum)]
|
|
kind: Option<ServiceManagerKind>,
|
|
|
|
/// If specified, uninstalls a user-level service
|
|
#[clap(long)]
|
|
user: bool,
|
|
},
|
|
}
|
|
|
|
impl ManagerSubcommand {
|
|
/// Returns true if the manager subcommand is listen
|
|
pub fn is_listen(&self) -> bool {
|
|
matches!(self, Self::Listen { .. })
|
|
}
|
|
|
|
pub fn run(self, config: ManagerConfig) -> CliResult {
|
|
match &self {
|
|
Self::Listen { daemon, .. } if *daemon => Self::run_daemon(self, config),
|
|
_ => {
|
|
let rt = tokio::runtime::Runtime::new().context("Failed to start up runtime")?;
|
|
rt.block_on(Self::async_run(self, config))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn run_daemon(self, _config: ManagerConfig) -> CliResult {
|
|
use crate::cli::Spawner;
|
|
let pid = Spawner::spawn_running_background(Vec::new())
|
|
.context("Failed to spawn background process")?;
|
|
println!("[distant manager detached, pid = {}]", pid);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn run_daemon(self, config: ManagerConfig) -> CliResult {
|
|
use crate::CliError;
|
|
use fork::{daemon, Fork};
|
|
|
|
debug!("Forking process");
|
|
match daemon(true, true) {
|
|
Ok(Fork::Child) => {
|
|
let rt = tokio::runtime::Runtime::new().context("Failed to start up runtime")?;
|
|
rt.block_on(async { Self::async_run(self, config).await })?;
|
|
Ok(())
|
|
}
|
|
Ok(Fork::Parent(pid)) => {
|
|
println!("[distant manager detached, pid = {}]", pid);
|
|
if fork::close_fd().is_err() {
|
|
Err(CliError::Error(anyhow::anyhow!("Fork failed to close fd")))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
Err(_) => Err(CliError::Error(anyhow::anyhow!("Fork failed"))),
|
|
}
|
|
}
|
|
|
|
async fn async_run(self, config: ManagerConfig) -> CliResult {
|
|
match self {
|
|
Self::Service(ManagerServiceSubcommand::Start { kind, user }) => {
|
|
debug!("Starting manager service via {:?}", kind);
|
|
let mut manager = <dyn ServiceManager>::target_or_native(kind)
|
|
.context("Failed to detect native service manager")?;
|
|
|
|
if user {
|
|
manager
|
|
.set_level(ServiceLevel::User)
|
|
.context("Failed to set service manager to user level")?;
|
|
}
|
|
|
|
manager
|
|
.start(ServiceStartCtx {
|
|
label: SERVICE_LABEL.clone(),
|
|
})
|
|
.context("Failed to start service")?;
|
|
Ok(())
|
|
}
|
|
Self::Service(ManagerServiceSubcommand::Stop { kind, user }) => {
|
|
debug!("Stopping manager service via {:?}", kind);
|
|
let mut manager = <dyn ServiceManager>::target_or_native(kind)
|
|
.context("Failed to detect native service manager")?;
|
|
|
|
if user {
|
|
manager
|
|
.set_level(ServiceLevel::User)
|
|
.context("Failed to set service manager to user level")?;
|
|
}
|
|
|
|
manager
|
|
.stop(ServiceStopCtx {
|
|
label: SERVICE_LABEL.clone(),
|
|
})
|
|
.context("Failed to stop service")?;
|
|
Ok(())
|
|
}
|
|
Self::Service(ManagerServiceSubcommand::Install { kind, user }) => {
|
|
debug!("Installing manager service via {:?}", kind);
|
|
let mut manager = <dyn ServiceManager>::target_or_native(kind)
|
|
.context("Failed to detect native service manager")?;
|
|
let mut args = vec![OsString::from("manager"), OsString::from("listen")];
|
|
|
|
if user {
|
|
args.push(OsString::from("--user"));
|
|
manager
|
|
.set_level(ServiceLevel::User)
|
|
.context("Failed to set service manager to user level")?;
|
|
}
|
|
|
|
manager
|
|
.install(ServiceInstallCtx {
|
|
label: SERVICE_LABEL.clone(),
|
|
|
|
// distant manager listen
|
|
program: std::env::current_exe()
|
|
.ok()
|
|
.unwrap_or_else(|| PathBuf::from("distant")),
|
|
args,
|
|
})
|
|
.context("Failed to install service")?;
|
|
|
|
Ok(())
|
|
}
|
|
Self::Service(ManagerServiceSubcommand::Uninstall { kind, user }) => {
|
|
debug!("Uninstalling manager service via {:?}", kind);
|
|
let mut manager = <dyn ServiceManager>::target_or_native(kind)
|
|
.context("Failed to detect native service manager")?;
|
|
if user {
|
|
manager
|
|
.set_level(ServiceLevel::User)
|
|
.context("Failed to set service manager to user level")?;
|
|
}
|
|
manager
|
|
.uninstall(ServiceUninstallCtx {
|
|
label: SERVICE_LABEL.clone(),
|
|
})
|
|
.context("Failed to uninstall service")?;
|
|
|
|
Ok(())
|
|
}
|
|
Self::Listen {
|
|
access,
|
|
network,
|
|
user,
|
|
..
|
|
} => {
|
|
let access = access.or(config.access).unwrap_or_default();
|
|
let network = network.merge(config.network);
|
|
|
|
info!(
|
|
"Starting manager (network = {})",
|
|
if (cfg!(windows) && network.windows_pipe.is_some())
|
|
|| (cfg!(unix) && network.unix_socket.is_some())
|
|
{
|
|
"custom"
|
|
} else if user {
|
|
"user"
|
|
} else {
|
|
"global"
|
|
}
|
|
);
|
|
let manager_ref = Manager {
|
|
access,
|
|
config: DistantManagerConfig {
|
|
user,
|
|
..Default::default()
|
|
},
|
|
network,
|
|
}
|
|
.listen()
|
|
.await
|
|
.context("Failed to start manager")?;
|
|
|
|
// Register our handlers for different schemes
|
|
debug!("Registering handlers with manager");
|
|
manager_ref
|
|
.register_launch_handler("manager", handlers::ManagerLaunchHandler::new())
|
|
.await
|
|
.context("Failed to register launch handler for \"manager://\"")?;
|
|
manager_ref
|
|
.register_connect_handler("distant", handlers::DistantConnectHandler)
|
|
.await
|
|
.context("Failed to register connect handler for \"distant://\"")?;
|
|
|
|
#[cfg(any(feature = "libssh", feature = "ssh2"))]
|
|
// Register ssh-specific handlers if either feature flag is enabled
|
|
{
|
|
manager_ref
|
|
.register_launch_handler("ssh", handlers::SshLaunchHandler)
|
|
.await
|
|
.context("Failed to register launch handler for \"ssh://\"")?;
|
|
manager_ref
|
|
.register_connect_handler("ssh", handlers::SshConnectHandler)
|
|
.await
|
|
.context("Failed to register connect handler for \"ssh://\"")?;
|
|
}
|
|
|
|
// Let our server run to completion
|
|
manager_ref
|
|
.wait()
|
|
.await
|
|
.context("Failed to wait on manager")?;
|
|
info!("Manager is shutting down");
|
|
|
|
Ok(())
|
|
}
|
|
Self::Capabilities { network } => {
|
|
let network = network.merge(config.network);
|
|
debug!("Getting list of capabilities");
|
|
let caps = Client::new(network)
|
|
.connect()
|
|
.await
|
|
.context("Failed to connect to manager")?
|
|
.capabilities()
|
|
.await
|
|
.context("Failed to get list of capabilities")?;
|
|
|
|
#[derive(Tabled)]
|
|
struct CapabilityRow {
|
|
kind: String,
|
|
description: String,
|
|
}
|
|
|
|
println!(
|
|
"{}",
|
|
Table::new(caps.into_sorted_vec().into_iter().map(|cap| {
|
|
CapabilityRow {
|
|
kind: cap.kind,
|
|
description: cap.description,
|
|
}
|
|
}))
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
Self::Info { network, id } => {
|
|
let network = network.merge(config.network);
|
|
debug!("Getting info about connection {}", id);
|
|
let info = Client::new(network)
|
|
.connect()
|
|
.await
|
|
.context("Failed to connect to manager")?
|
|
.info(id)
|
|
.await
|
|
.context("Failed to get info about connection")?;
|
|
|
|
#[derive(Tabled)]
|
|
struct InfoRow {
|
|
id: ConnectionId,
|
|
scheme: String,
|
|
host: String,
|
|
port: String,
|
|
options: String,
|
|
}
|
|
|
|
println!(
|
|
"{}",
|
|
Table::new(vec![InfoRow {
|
|
id: info.id,
|
|
scheme: info.destination.scheme.unwrap_or_default(),
|
|
host: info.destination.host.to_string(),
|
|
port: info
|
|
.destination
|
|
.port
|
|
.map(|x| x.to_string())
|
|
.unwrap_or_default(),
|
|
options: info.options.to_string()
|
|
}])
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
Self::List { network, cache } => {
|
|
let network = network.merge(config.network);
|
|
debug!("Getting list of connections");
|
|
let list = Client::new(network)
|
|
.connect()
|
|
.await
|
|
.context("Failed to connect to manager")?
|
|
.list()
|
|
.await
|
|
.context("Failed to get list of connections")?;
|
|
|
|
debug!("Looking up selected connection");
|
|
let selected = Cache::read_from_disk_or_default(cache)
|
|
.await
|
|
.context("Failed to look up selected connection")?
|
|
.data
|
|
.selected;
|
|
|
|
#[derive(Tabled)]
|
|
struct ListRow {
|
|
selected: bool,
|
|
id: ConnectionId,
|
|
scheme: String,
|
|
host: String,
|
|
port: String,
|
|
}
|
|
|
|
println!(
|
|
"{}",
|
|
Table::new(list.into_iter().map(|(id, destination)| {
|
|
ListRow {
|
|
selected: *selected == id,
|
|
id,
|
|
scheme: destination.scheme.unwrap_or_default(),
|
|
host: destination.host.to_string(),
|
|
port: destination.port.map(|x| x.to_string()).unwrap_or_default(),
|
|
}
|
|
}))
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
Self::Kill { network, id } => {
|
|
let network = network.merge(config.network);
|
|
debug!("Killing connection {}", id);
|
|
Client::new(network)
|
|
.connect()
|
|
.await
|
|
.context("Failed to connect to manager")?
|
|
.kill(id)
|
|
.await
|
|
.with_context(|| format!("Failed to kill connection to server {id}"))?;
|
|
Ok(())
|
|
}
|
|
Self::Shutdown { network } => {
|
|
let network = network.merge(config.network);
|
|
debug!("Shutting down manager");
|
|
Client::new(network)
|
|
.connect()
|
|
.await
|
|
.context("Failed to connect to manager")?
|
|
.shutdown()
|
|
.await
|
|
.context("Failed to shutdown manager")?;
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|