mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-06-23 00:34:48 +03:00
hickory_resolver's TokioResolver only consults /etc/resolv.conf and queries upstream DNS servers — which means it can't see /etc/hosts, mDNS (Avahi/Bonjour), NetBIOS, or anything else in the system's full name-resolution stack. On a typical home LAN there's no DNS server that knows about peer machine names, so users had to fall back to typing IP addresses, which broke the moment they moved their setup to a different network. Swap to tokio::net::lookup_host, which calls getaddrinfo (or GetAddrInfoEx on Windows). That walks /etc/nsswitch.conf on Linux (picking up Avahi-resolved .local names, /etc/hosts, and DNS), uses Bonjour for .local on macOS, and the full Windows resolver on Windows. A Bonjour hostname like "JKMBP-M4-Max.local" now resolves on every modern network without explicit configuration; the user can carry their two machines between LANs and the connection still finds them. Drop the hickory-resolver dependency entirely — it's no longer needed. ServiceError::Dns also goes away; lookup failures surface as io::Error which is already covered by ServiceError::Io. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
611 lines
22 KiB
Rust
611 lines
22 KiB
Rust
use crate::{
|
|
capture::{Capture, CaptureType, ICaptureEvent},
|
|
client::ClientManager,
|
|
config::{Config, ConfigClient},
|
|
connect::LanMouseConnection,
|
|
crypto,
|
|
dns::{DnsEvent, DnsResolver},
|
|
emulation::{Emulation, EmulationEvent},
|
|
listen::{LanMouseListener, ListenerCreationError},
|
|
};
|
|
use futures::StreamExt;
|
|
use lan_mouse_ipc::{
|
|
AsyncFrontendListener, ClientHandle, FrontendEvent, FrontendRequest, IpcError,
|
|
IpcListenerCreationError, Position, Status,
|
|
};
|
|
use log;
|
|
use std::{
|
|
collections::{HashMap, HashSet, VecDeque},
|
|
io,
|
|
net::{IpAddr, SocketAddr},
|
|
sync::{Arc, RwLock},
|
|
};
|
|
use thiserror::Error;
|
|
use tokio::{process::Command, signal, sync::Notify};
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum ServiceError {
|
|
#[error(transparent)]
|
|
IpcListen(#[from] IpcListenerCreationError),
|
|
#[error(transparent)]
|
|
Io(#[from] io::Error),
|
|
#[error(transparent)]
|
|
ListenError(#[from] ListenerCreationError),
|
|
#[error("failed to load certificate: `{0}`")]
|
|
Certificate(#[from] crypto::Error),
|
|
}
|
|
|
|
pub struct Service {
|
|
/// configuration
|
|
config: Config,
|
|
/// input capture
|
|
capture: Capture,
|
|
/// input emulation
|
|
emulation: Emulation,
|
|
/// dns resolver
|
|
resolver: DnsResolver,
|
|
/// frontend listener
|
|
frontend_listener: AsyncFrontendListener,
|
|
/// authorized public key sha256 fingerprints
|
|
authorized_keys: Arc<RwLock<HashMap<String, String>>>,
|
|
/// (outgoing) client information
|
|
client_manager: ClientManager,
|
|
/// current port
|
|
port: u16,
|
|
/// the public key fingerprint for (D)TLS
|
|
public_key_fingerprint: String,
|
|
/// notify for pending frontend events
|
|
frontend_event_pending: Notify,
|
|
/// frontend events queued for sending
|
|
pending_frontend_events: VecDeque<FrontendEvent>,
|
|
/// status of input capture (enabled / disabled)
|
|
capture_status: Status,
|
|
/// status of input emulation (enabled / disabled)
|
|
emulation_status: Status,
|
|
/// keep track of registered connections to avoid duplicate barriers
|
|
incoming_conns: HashSet<SocketAddr>,
|
|
/// map from capture handle to connection info
|
|
incoming_conn_info: HashMap<ClientHandle, Incoming>,
|
|
next_trigger_handle: u64,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Incoming {
|
|
fingerprint: String,
|
|
addr: SocketAddr,
|
|
pos: Position,
|
|
}
|
|
|
|
impl Service {
|
|
pub async fn new(config: Config) -> Result<Self, ServiceError> {
|
|
let client_manager = ClientManager::default();
|
|
for client in config.clients() {
|
|
client_manager.add_with_config(client);
|
|
}
|
|
|
|
// load certificate
|
|
let cert = crypto::load_or_generate_key_and_cert(config.cert_path())?;
|
|
let public_key_fingerprint = crypto::certificate_fingerprint(&cert);
|
|
|
|
// create frontend communication adapter, exit if already running
|
|
let frontend_listener = AsyncFrontendListener::new().await?;
|
|
|
|
let authorized_keys = Arc::new(RwLock::new(config.authorized_fingerprints()));
|
|
// listener + connection
|
|
let listener =
|
|
LanMouseListener::new(config.port(), cert.clone(), authorized_keys.clone()).await?;
|
|
let conn = LanMouseConnection::new(cert.clone(), client_manager.clone());
|
|
|
|
// input capture + emulation
|
|
let capture_backend = config.capture_backend().map(|b| b.into());
|
|
let capture = Capture::new(capture_backend, conn, config.release_bind());
|
|
let emulation_backend = config.emulation_backend().map(|b| b.into());
|
|
let emulation = Emulation::new(emulation_backend, listener);
|
|
|
|
// create dns resolver
|
|
let resolver = DnsResolver::new()?;
|
|
|
|
let port = config.port();
|
|
let service = Self {
|
|
config,
|
|
capture,
|
|
emulation,
|
|
frontend_listener,
|
|
resolver,
|
|
authorized_keys,
|
|
public_key_fingerprint,
|
|
client_manager,
|
|
frontend_event_pending: Default::default(),
|
|
port,
|
|
pending_frontend_events: Default::default(),
|
|
capture_status: Default::default(),
|
|
emulation_status: Default::default(),
|
|
incoming_conn_info: Default::default(),
|
|
incoming_conns: Default::default(),
|
|
next_trigger_handle: 0,
|
|
};
|
|
Ok(service)
|
|
}
|
|
|
|
pub async fn run(&mut self) -> Result<(), ServiceError> {
|
|
let active = self.client_manager.active_clients();
|
|
for handle in active.iter() {
|
|
// small hack: `activate_client()` checks, if the client
|
|
// is already active in client_manager and does not create a
|
|
// capture barrier in that case so we have to deactivate it first
|
|
self.client_manager.deactivate_client(*handle);
|
|
}
|
|
|
|
for handle in active {
|
|
self.activate_client(handle);
|
|
}
|
|
|
|
loop {
|
|
tokio::select! {
|
|
request = self.frontend_listener.next() => self.handle_frontend_request(request),
|
|
_ = self.frontend_event_pending.notified() => self.handle_frontend_pending().await,
|
|
event = self.emulation.event() => self.handle_emulation_event(event),
|
|
event = self.capture.event() => self.handle_capture_event(event),
|
|
event = self.resolver.event() => self.handle_resolver_event(event),
|
|
_ = self.config.changed() => self.handle_config_change(),
|
|
r = signal::ctrl_c() => break r.expect("failed to wait for CTRL+C"),
|
|
}
|
|
}
|
|
|
|
log::info!("terminating service ...");
|
|
log::debug!("terminating capture ...");
|
|
self.capture.terminate().await;
|
|
log::debug!("terminating emulation ...");
|
|
self.emulation.terminate().await;
|
|
log::debug!("terminating dns resolver ...");
|
|
self.resolver.terminate().await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_frontend_request(&mut self, request: Option<Result<FrontendRequest, IpcError>>) {
|
|
let request = match request.expect("frontend listener closed") {
|
|
Ok(r) => r,
|
|
Err(e) => return log::error!("error receiving request: {e}"),
|
|
};
|
|
match request {
|
|
FrontendRequest::Activate(handle, active) => {
|
|
self.set_client_active(handle, active);
|
|
self.save_config();
|
|
}
|
|
FrontendRequest::AuthorizeKey(desc, fp) => {
|
|
self.add_authorized_key(desc, fp);
|
|
self.save_config();
|
|
}
|
|
FrontendRequest::ChangePort(port) => self.change_port(port),
|
|
FrontendRequest::Create => {
|
|
self.add_client();
|
|
self.save_config();
|
|
}
|
|
FrontendRequest::Delete(handle) => {
|
|
self.remove_client(handle);
|
|
self.save_config();
|
|
}
|
|
FrontendRequest::EnableCapture => self.capture.reenable(),
|
|
FrontendRequest::EnableEmulation => self.emulation.reenable(),
|
|
FrontendRequest::Enumerate() => self.enumerate(),
|
|
FrontendRequest::UpdateFixIps(handle, fix_ips) => {
|
|
self.update_fix_ips(handle, fix_ips);
|
|
self.save_config();
|
|
}
|
|
FrontendRequest::UpdateHostname(handle, host) => {
|
|
self.update_hostname(handle, host);
|
|
self.save_config();
|
|
}
|
|
FrontendRequest::UpdatePort(handle, port) => {
|
|
self.update_port(handle, port);
|
|
self.save_config();
|
|
}
|
|
FrontendRequest::UpdatePosition(handle, pos) => {
|
|
self.update_pos(handle, pos);
|
|
self.save_config();
|
|
}
|
|
FrontendRequest::ResolveDns(handle) => self.resolve(handle),
|
|
FrontendRequest::Sync => self.sync_frontend(),
|
|
FrontendRequest::RemoveAuthorizedKey(key) => {
|
|
self.remove_authorized_key(key);
|
|
self.save_config();
|
|
}
|
|
FrontendRequest::UpdateEnterHook(handle, enter_hook) => {
|
|
self.update_enter_hook(handle, enter_hook)
|
|
}
|
|
FrontendRequest::SaveConfiguration => self.save_config(),
|
|
}
|
|
}
|
|
|
|
fn save_config(&mut self) {
|
|
let clients = self.client_manager.clients();
|
|
let clients = clients
|
|
.into_iter()
|
|
.map(|(c, s)| ConfigClient {
|
|
ips: HashSet::from_iter(c.fix_ips),
|
|
hostname: c.hostname,
|
|
port: c.port,
|
|
pos: c.pos,
|
|
active: s.active,
|
|
enter_hook: c.cmd,
|
|
})
|
|
.collect();
|
|
self.config.set_clients(clients);
|
|
let authorized_keys = self.authorized_keys.read().expect("lock").clone();
|
|
self.config.set_authorized_keys(authorized_keys);
|
|
if let Err(e) = self.config.write_back() {
|
|
log::warn!("failed to write config: {e}");
|
|
}
|
|
}
|
|
|
|
fn handle_config_change(&mut self) {
|
|
for h in self.client_manager.registered_clients() {
|
|
self.remove_client(h);
|
|
}
|
|
for c in self.config.clients() {
|
|
let handle = self.client_manager.add_with_config(c);
|
|
log::info!("added client {handle}");
|
|
let (c, s) = self.client_manager.get_state(handle).unwrap();
|
|
if s.active {
|
|
self.client_manager.deactivate_client(handle);
|
|
self.activate_client(handle);
|
|
}
|
|
self.notify_frontend(FrontendEvent::Created(handle, c, s));
|
|
}
|
|
let release_bind = self.config.release_bind();
|
|
self.capture.set_release_bind(release_bind);
|
|
let authorized_keys = self.config.authorized_fingerprints();
|
|
self.authorized_keys
|
|
.write()
|
|
.unwrap()
|
|
.clone_from(&authorized_keys);
|
|
self.sync_frontend();
|
|
}
|
|
|
|
async fn handle_frontend_pending(&mut self) {
|
|
while let Some(event) = self.pending_frontend_events.pop_front() {
|
|
self.frontend_listener.broadcast(event).await;
|
|
}
|
|
}
|
|
|
|
fn handle_emulation_event(&mut self, event: EmulationEvent) {
|
|
match event {
|
|
EmulationEvent::ConnectionAttempt { fingerprint } => {
|
|
self.notify_frontend(FrontendEvent::ConnectionAttempt { fingerprint });
|
|
}
|
|
EmulationEvent::Entered {
|
|
addr,
|
|
pos,
|
|
fingerprint,
|
|
} => {
|
|
// check if already registered
|
|
if !self.incoming_conns.contains(&addr) {
|
|
self.add_incoming(addr, pos, fingerprint.clone());
|
|
self.notify_frontend(FrontendEvent::DeviceEntered {
|
|
fingerprint,
|
|
addr,
|
|
pos,
|
|
});
|
|
} else {
|
|
self.update_incoming(addr, pos, fingerprint);
|
|
}
|
|
}
|
|
EmulationEvent::Disconnected { addr } => {
|
|
if let Some(addr) = self.remove_incoming(addr) {
|
|
self.notify_frontend(FrontendEvent::IncomingDisconnected(addr));
|
|
}
|
|
}
|
|
EmulationEvent::PortChanged(port) => match port {
|
|
Ok(port) => {
|
|
self.port = port;
|
|
self.notify_frontend(FrontendEvent::PortChanged(port, None));
|
|
}
|
|
Err(e) => self
|
|
.notify_frontend(FrontendEvent::PortChanged(self.port, Some(format!("{e}")))),
|
|
},
|
|
EmulationEvent::EmulationDisabled => {
|
|
self.emulation_status = Status::Disabled;
|
|
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status));
|
|
}
|
|
EmulationEvent::EmulationEnabled => {
|
|
self.emulation_status = Status::Enabled;
|
|
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status));
|
|
}
|
|
EmulationEvent::ReleaseNotify => self.capture.release(),
|
|
EmulationEvent::Connected { addr, fingerprint } => {
|
|
self.notify_frontend(FrontendEvent::DeviceConnected { addr, fingerprint });
|
|
}
|
|
EmulationEvent::PeerHello { addr, commit } => {
|
|
// Map the peer's source addr back to its client handle
|
|
// and stamp the commit. Skip if we don't have an
|
|
// outgoing client configured for this peer (incoming-
|
|
// only setup) — there's nowhere to display the version
|
|
// in that case anyway.
|
|
if let Some(handle) = self.client_manager.get_client(addr) {
|
|
self.client_manager.set_peer_commit(handle, Some(commit));
|
|
self.broadcast_client(handle);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_capture_event(&mut self, event: ICaptureEvent) {
|
|
match event {
|
|
ICaptureEvent::CaptureBegin(handle) => {
|
|
// we entered the capture zone for an incoming connection
|
|
// => notify it that its capture should be released
|
|
if let Some(incoming) = self.incoming_conn_info.get(&handle) {
|
|
self.emulation.send_leave_event(incoming.addr);
|
|
}
|
|
}
|
|
ICaptureEvent::CaptureDisabled => {
|
|
self.capture_status = Status::Disabled;
|
|
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status));
|
|
}
|
|
ICaptureEvent::CaptureEnabled => {
|
|
self.capture_status = Status::Enabled;
|
|
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status));
|
|
}
|
|
ICaptureEvent::ClientEntered(handle) => {
|
|
log::info!("entering client {handle} ...");
|
|
self.spawn_hook_command(handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_resolver_event(&mut self, event: DnsEvent) {
|
|
let handle = match event {
|
|
DnsEvent::Resolving(handle) => {
|
|
self.client_manager.set_resolving(handle, true);
|
|
handle
|
|
}
|
|
DnsEvent::Resolved(handle, hostname, ips) => {
|
|
self.client_manager.set_resolving(handle, false);
|
|
if let Err(e) = &ips {
|
|
log::warn!("could not resolve {hostname}: {e}");
|
|
}
|
|
let ips = ips.unwrap_or_default();
|
|
self.client_manager.set_dns_ips(handle, ips);
|
|
handle
|
|
}
|
|
};
|
|
self.broadcast_client(handle);
|
|
}
|
|
|
|
fn resolve(&self, handle: ClientHandle) {
|
|
if let Some(hostname) = self.client_manager.get_hostname(handle) {
|
|
self.resolver.resolve(handle, hostname);
|
|
}
|
|
}
|
|
|
|
fn sync_frontend(&mut self) {
|
|
self.enumerate();
|
|
self.notify_frontend(FrontendEvent::EmulationStatus(self.emulation_status));
|
|
self.notify_frontend(FrontendEvent::CaptureStatus(self.capture_status));
|
|
self.notify_frontend(FrontendEvent::PortChanged(self.port, None));
|
|
self.notify_frontend(FrontendEvent::PublicKeyFingerprint(
|
|
self.public_key_fingerprint.clone(),
|
|
));
|
|
let keys = self.authorized_keys.read().expect("lock").clone();
|
|
self.notify_frontend(FrontendEvent::AuthorizedUpdated(keys));
|
|
}
|
|
|
|
const ENTER_HANDLE_BEGIN: u64 = u64::MAX / 2 + 1;
|
|
|
|
fn add_incoming(&mut self, addr: SocketAddr, pos: Position, fingerprint: String) {
|
|
let handle = Self::ENTER_HANDLE_BEGIN + self.next_trigger_handle;
|
|
self.next_trigger_handle += 1;
|
|
self.capture.create(handle, pos, CaptureType::EnterOnly);
|
|
self.incoming_conns.insert(addr);
|
|
self.incoming_conn_info.insert(
|
|
handle,
|
|
Incoming {
|
|
fingerprint,
|
|
addr,
|
|
pos,
|
|
},
|
|
);
|
|
}
|
|
|
|
fn update_incoming(&mut self, addr: SocketAddr, pos: Position, fingerprint: String) {
|
|
let incoming = self
|
|
.incoming_conn_info
|
|
.iter_mut()
|
|
.find(|(_, i)| i.addr == addr)
|
|
.map(|(_, i)| i)
|
|
.expect("no such client");
|
|
let mut changed = false;
|
|
if incoming.fingerprint != fingerprint {
|
|
incoming.fingerprint = fingerprint.clone();
|
|
changed = true;
|
|
}
|
|
if incoming.pos != pos {
|
|
incoming.pos = pos;
|
|
changed = true;
|
|
}
|
|
if changed {
|
|
self.remove_incoming(addr);
|
|
self.add_incoming(addr, pos, fingerprint.clone());
|
|
self.notify_frontend(FrontendEvent::IncomingDisconnected(addr));
|
|
self.notify_frontend(FrontendEvent::DeviceEntered {
|
|
fingerprint,
|
|
addr,
|
|
pos,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn remove_incoming(&mut self, addr: SocketAddr) -> Option<SocketAddr> {
|
|
let handle = self
|
|
.incoming_conn_info
|
|
.iter()
|
|
.find(|(_, incoming)| incoming.addr == addr)
|
|
.map(|(k, _)| *k)?;
|
|
self.capture.destroy(handle);
|
|
self.incoming_conns.remove(&addr);
|
|
self.incoming_conn_info
|
|
.remove(&handle)
|
|
.map(|incoming| incoming.addr)
|
|
}
|
|
|
|
fn notify_frontend(&mut self, event: FrontendEvent) {
|
|
self.pending_frontend_events.push_back(event);
|
|
self.frontend_event_pending.notify_one();
|
|
}
|
|
|
|
fn add_authorized_key(&mut self, desc: String, fp: String) {
|
|
self.authorized_keys.write().expect("lock").insert(fp, desc);
|
|
let keys = self.authorized_keys.read().expect("lock").clone();
|
|
self.notify_frontend(FrontendEvent::AuthorizedUpdated(keys));
|
|
}
|
|
|
|
fn remove_authorized_key(&mut self, fp: String) {
|
|
self.authorized_keys.write().expect("lock").remove(&fp);
|
|
let keys = self.authorized_keys.read().expect("lock").clone();
|
|
self.notify_frontend(FrontendEvent::AuthorizedUpdated(keys));
|
|
}
|
|
|
|
fn enumerate(&mut self) {
|
|
let clients = self.client_manager.get_client_states();
|
|
self.notify_frontend(FrontendEvent::Enumerate(clients));
|
|
}
|
|
|
|
fn add_client(&mut self) {
|
|
let handle = self.client_manager.add_client();
|
|
log::info!("added client {handle}");
|
|
let (c, s) = self.client_manager.get_state(handle).unwrap();
|
|
self.notify_frontend(FrontendEvent::Created(handle, c, s));
|
|
}
|
|
|
|
fn set_client_active(&mut self, handle: ClientHandle, active: bool) {
|
|
if active {
|
|
self.activate_client(handle);
|
|
} else {
|
|
self.deactivate_client(handle);
|
|
}
|
|
}
|
|
|
|
fn deactivate_client(&mut self, handle: ClientHandle) {
|
|
log::debug!("deactivating client {handle}");
|
|
if self.client_manager.deactivate_client(handle) {
|
|
self.capture.destroy(handle);
|
|
self.broadcast_client(handle);
|
|
log::info!("deactivated client {handle}");
|
|
}
|
|
}
|
|
|
|
fn activate_client(&mut self, handle: ClientHandle) {
|
|
log::debug!("activating client {handle}");
|
|
|
|
/* resolve dns on activate */
|
|
self.resolve(handle);
|
|
|
|
/* deactivate potential other client at this position */
|
|
let Some(pos) = self.client_manager.get_pos(handle) else {
|
|
return;
|
|
};
|
|
|
|
if let Some(other) = self.client_manager.client_at(pos) {
|
|
if other != handle {
|
|
self.deactivate_client(other);
|
|
}
|
|
}
|
|
|
|
/* activate the client */
|
|
if self.client_manager.activate_client(handle) {
|
|
/* notify capture and frontends */
|
|
self.capture.create(handle, pos, CaptureType::Default);
|
|
self.broadcast_client(handle);
|
|
log::info!("activated client {handle} ({pos})");
|
|
}
|
|
}
|
|
|
|
fn change_port(&mut self, port: u16) {
|
|
if self.port != port {
|
|
self.emulation.request_port_change(port);
|
|
} else {
|
|
self.notify_frontend(FrontendEvent::PortChanged(self.port, None));
|
|
}
|
|
}
|
|
|
|
fn remove_client(&mut self, handle: ClientHandle) {
|
|
if self
|
|
.client_manager
|
|
.remove_client(handle)
|
|
.map(|(_, s)| s.active)
|
|
.unwrap_or(false)
|
|
{
|
|
self.capture.destroy(handle);
|
|
}
|
|
self.notify_frontend(FrontendEvent::Deleted(handle));
|
|
}
|
|
|
|
fn update_fix_ips(&mut self, handle: ClientHandle, fix_ips: Vec<IpAddr>) {
|
|
self.client_manager.set_fix_ips(handle, fix_ips);
|
|
self.broadcast_client(handle);
|
|
}
|
|
|
|
fn update_hostname(&mut self, handle: ClientHandle, hostname: Option<String>) {
|
|
log::info!("hostname changed: {hostname:?}");
|
|
if self.client_manager.set_hostname(handle, hostname.clone()) {
|
|
self.resolve(handle);
|
|
}
|
|
self.broadcast_client(handle);
|
|
}
|
|
|
|
fn update_port(&mut self, handle: ClientHandle, port: u16) {
|
|
self.client_manager.set_port(handle, port);
|
|
self.broadcast_client(handle);
|
|
}
|
|
|
|
fn update_pos(&mut self, handle: ClientHandle, pos: Position) {
|
|
// update state in event input emulator & input capture
|
|
if self.client_manager.set_pos(handle, pos) {
|
|
self.deactivate_client(handle);
|
|
self.activate_client(handle);
|
|
}
|
|
self.broadcast_client(handle);
|
|
}
|
|
|
|
fn update_enter_hook(&mut self, handle: ClientHandle, enter_hook: Option<String>) {
|
|
self.client_manager.set_enter_hook(handle, enter_hook);
|
|
self.broadcast_client(handle);
|
|
}
|
|
|
|
fn broadcast_client(&mut self, handle: ClientHandle) {
|
|
let event = self
|
|
.client_manager
|
|
.get_state(handle)
|
|
.map(|(c, s)| FrontendEvent::State(handle, c, s))
|
|
.unwrap_or(FrontendEvent::NoSuchClient(handle));
|
|
self.notify_frontend(event);
|
|
}
|
|
|
|
fn spawn_hook_command(&self, handle: ClientHandle) {
|
|
let Some(cmd) = self.client_manager.get_enter_cmd(handle) else {
|
|
return;
|
|
};
|
|
tokio::task::spawn_local(async move {
|
|
log::info!("spawning command!");
|
|
let mut child = match Command::new("sh").arg("-c").arg(cmd.as_str()).spawn() {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
log::warn!("could not execute cmd: {e}");
|
|
return;
|
|
}
|
|
};
|
|
match child.wait().await {
|
|
Ok(s) => {
|
|
if s.success() {
|
|
log::info!("{cmd} exited successfully");
|
|
} else {
|
|
log::warn!("{cmd} exited with {s}");
|
|
}
|
|
}
|
|
Err(e) => log::warn!("{cmd}: {e}"),
|
|
}
|
|
});
|
|
}
|
|
}
|