veilid/veilid-cli/src/command_processor.rs

570 lines
19 KiB
Rust
Raw Normal View History

2021-11-22 16:28:30 +00:00
use crate::client_api_connection::*;
use crate::settings::Settings;
2023-06-08 18:07:09 +00:00
use crate::tools::*;
2021-11-22 16:28:30 +00:00
use crate::ui::*;
use std::net::SocketAddr;
2022-12-02 00:59:19 +00:00
use std::time::SystemTime;
2023-06-08 01:55:23 +00:00
use veilid_tools::*;
2022-07-01 16:13:52 +00:00
2023-06-08 18:07:09 +00:00
pub fn convert_loglevel(s: &str) -> Result<String, String> {
2022-07-01 16:13:52 +00:00
match s.to_ascii_lowercase().as_str() {
2023-06-08 18:07:09 +00:00
"off" => Ok("Off".to_owned()),
"error" => Ok("Error".to_owned()),
"warn" => Ok("Warn".to_owned()),
"info" => Ok("Info".to_owned()),
"debug" => Ok("Debug".to_owned()),
"trace" => Ok("Trace".to_owned()),
2022-07-01 16:13:52 +00:00
_ => Err(format!("Invalid log level: {}", s)),
}
}
2021-11-22 16:28:30 +00:00
#[derive(PartialEq, Clone)]
pub enum ConnectionState {
Disconnected,
Connected(SocketAddr, SystemTime),
Retrying(SocketAddr, SystemTime),
}
impl ConnectionState {
pub fn is_disconnected(&self) -> bool {
2021-11-29 01:08:50 +00:00
matches!(*self, Self::Disconnected)
2021-11-22 16:28:30 +00:00
}
pub fn is_connected(&self) -> bool {
2021-11-29 01:08:50 +00:00
matches!(*self, Self::Connected(_, _))
2021-11-22 16:28:30 +00:00
}
pub fn is_retrying(&self) -> bool {
2021-11-29 01:08:50 +00:00
matches!(*self, Self::Retrying(_, _))
2021-11-22 16:28:30 +00:00
}
}
struct CommandProcessorInner {
2023-06-08 18:07:09 +00:00
ui_sender: UISender,
2021-11-22 16:28:30 +00:00
capi: Option<ClientApiConnection>,
reconnect: bool,
finished: bool,
autoconnect: bool,
autoreconnect: bool,
server_addr: Option<SocketAddr>,
connection_waker: Eventual,
2023-06-08 18:07:09 +00:00
last_call_id: Option<u64>,
2021-11-22 16:28:30 +00:00
}
#[derive(Clone)]
pub struct CommandProcessor {
2023-06-08 18:07:09 +00:00
inner: Arc<Mutex<CommandProcessorInner>>,
2021-11-22 16:28:30 +00:00
}
impl CommandProcessor {
2023-06-08 18:07:09 +00:00
pub fn new(ui_sender: UISender, settings: &Settings) -> Self {
2021-11-22 16:28:30 +00:00
Self {
2023-06-08 18:07:09 +00:00
inner: Arc::new(Mutex::new(CommandProcessorInner {
ui_sender,
2021-11-22 16:28:30 +00:00
capi: None,
reconnect: settings.autoreconnect,
finished: false,
autoconnect: settings.autoconnect,
autoreconnect: settings.autoreconnect,
server_addr: None,
connection_waker: Eventual::new(),
2022-10-01 02:37:55 +00:00
last_call_id: None,
2021-11-22 16:28:30 +00:00
})),
}
}
2023-06-08 18:07:09 +00:00
pub fn set_client_api_connection(&self, capi: ClientApiConnection) {
self.inner.lock().capi = Some(capi);
2021-11-22 16:28:30 +00:00
}
2023-06-08 18:07:09 +00:00
fn inner(&self) -> MutexGuard<CommandProcessorInner> {
self.inner.lock()
2021-11-22 16:28:30 +00:00
}
2023-06-08 18:07:09 +00:00
fn inner_mut(&self) -> MutexGuard<CommandProcessorInner> {
self.inner.lock()
2021-11-22 16:28:30 +00:00
}
2023-06-08 18:07:09 +00:00
fn ui_sender(&self) -> UISender {
self.inner.lock().ui_sender.clone()
2021-11-22 16:28:30 +00:00
}
fn capi(&self) -> ClientApiConnection {
2023-06-08 18:07:09 +00:00
self.inner.lock().capi.as_ref().unwrap().clone()
2021-11-22 16:28:30 +00:00
}
fn word_split(line: &str) -> (String, Option<String>) {
let trimmed = line.trim();
if let Some(p) = trimmed.find(char::is_whitespace) {
let first = trimmed[0..p].to_owned();
let rest = trimmed[p..].trim_start().to_owned();
(first, Some(rest))
} else {
(trimmed.to_owned(), None)
}
}
2022-11-03 15:28:29 +00:00
pub fn cancel_command(&self) {
trace!("CommandProcessor::cancel_command");
let capi = self.capi();
2023-06-08 18:07:09 +00:00
capi.cancel_all();
2022-11-03 15:28:29 +00:00
}
2021-11-22 16:28:30 +00:00
pub fn cmd_help(&self, _rest: Option<String>, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_help");
2023-06-08 18:07:09 +00:00
self.ui_sender().add_node_event(
2023-06-21 02:18:59 +00:00
Level::Info,
2021-11-22 16:28:30 +00:00
r#"Commands:
exit/quit - exit the client
disconnect - disconnect the client from the Veilid node
shutdown - shut the server down
attach - attach the server to the Veilid network
detach - detach the server from the Veilid network
2021-12-11 01:14:33 +00:00
debug - send a debugging command to the Veilid server
2022-07-01 16:13:52 +00:00
change_log_level - change the log level for a tracing layer
2022-10-01 02:37:55 +00:00
reply - reply to an AppCall not handled directly by the server
2022-08-29 19:19:28 +00:00
"#
.to_owned(),
2021-11-22 16:28:30 +00:00
);
2023-06-08 18:07:09 +00:00
let ui = self.ui_sender();
2021-12-11 01:14:33 +00:00
ui.send_callback(callback);
2021-11-22 16:28:30 +00:00
Ok(())
}
pub fn cmd_exit(&self, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_exit");
2023-06-08 18:07:09 +00:00
let ui = self.ui_sender();
2021-12-11 01:14:33 +00:00
ui.send_callback(callback);
ui.quit();
2021-11-22 16:28:30 +00:00
Ok(())
}
pub fn cmd_shutdown(&self, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_shutdown");
2023-06-08 18:07:09 +00:00
let capi = self.capi();
let ui = self.ui_sender();
2022-06-28 03:46:29 +00:00
spawn_detached_local(async move {
2021-11-22 16:28:30 +00:00
if let Err(e) = capi.server_shutdown().await {
error!("Server command 'shutdown' failed to execute: {}", e);
}
2021-12-11 01:14:33 +00:00
ui.send_callback(callback);
2021-11-22 16:28:30 +00:00
});
Ok(())
}
pub fn cmd_attach(&self, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_attach");
2023-06-08 18:07:09 +00:00
let capi = self.capi();
let ui = self.ui_sender();
2022-06-28 03:46:29 +00:00
spawn_detached_local(async move {
2021-11-22 16:28:30 +00:00
if let Err(e) = capi.server_attach().await {
2022-07-01 16:13:52 +00:00
error!("Server command 'attach' failed: {}", e);
2021-11-22 16:28:30 +00:00
}
2021-12-11 01:14:33 +00:00
ui.send_callback(callback);
2021-11-22 16:28:30 +00:00
});
Ok(())
}
pub fn cmd_detach(&self, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_detach");
2023-06-08 18:07:09 +00:00
let capi = self.capi();
let ui = self.ui_sender();
2022-06-28 03:46:29 +00:00
spawn_detached_local(async move {
2021-11-22 16:28:30 +00:00
if let Err(e) = capi.server_detach().await {
2022-07-01 16:13:52 +00:00
error!("Server command 'detach' failed: {}", e);
2021-11-22 16:28:30 +00:00
}
2021-12-11 01:14:33 +00:00
ui.send_callback(callback);
2021-11-22 16:28:30 +00:00
});
Ok(())
}
pub fn cmd_disconnect(&self, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_disconnect");
2023-06-08 18:07:09 +00:00
let capi = self.capi();
let ui = self.ui_sender();
2022-06-28 03:46:29 +00:00
spawn_detached_local(async move {
2021-11-22 16:28:30 +00:00
capi.disconnect().await;
2021-12-11 01:14:33 +00:00
ui.send_callback(callback);
});
Ok(())
}
pub fn cmd_debug(&self, rest: Option<String>, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_debug");
2023-06-08 18:07:09 +00:00
let capi = self.capi();
let ui = self.ui_sender();
2022-06-28 03:46:29 +00:00
spawn_detached_local(async move {
2021-12-11 01:14:33 +00:00
match capi.server_debug(rest.unwrap_or_default()).await {
Ok(output) => ui.display_string_dialog("Debug Output", output, callback),
2022-07-22 17:05:28 +00:00
Err(e) => ui.display_string_dialog("Debug Error", e.to_string(), callback),
2022-07-01 16:13:52 +00:00
}
});
Ok(())
}
pub fn cmd_change_log_level(
&self,
rest: Option<String>,
callback: UICallback,
) -> Result<(), String> {
trace!("CommandProcessor::cmd_change_log_level");
2023-06-08 18:07:09 +00:00
let capi = self.capi();
let ui = self.ui_sender();
2022-07-01 16:13:52 +00:00
spawn_detached_local(async move {
let (layer, rest) = Self::word_split(&rest.unwrap_or_default());
let log_level = match convert_loglevel(&rest.unwrap_or_default()) {
Ok(v) => v,
Err(e) => {
2023-06-21 02:18:59 +00:00
ui.add_node_event(Level::Error, format!("Failed to change log level: {}", e));
2022-07-01 16:13:52 +00:00
ui.send_callback(callback);
return;
}
};
match capi.server_change_log_level(layer, log_level).await {
Ok(()) => {
2022-08-29 19:19:28 +00:00
ui.display_string_dialog("Success", "Log level changed", callback);
2022-07-01 16:13:52 +00:00
}
Err(e) => {
2022-08-29 19:19:28 +00:00
ui.display_string_dialog(
"Server command 'change_log_level' failed",
e.to_string(),
callback,
);
2021-12-11 01:14:33 +00:00
}
}
2021-11-22 16:28:30 +00:00
});
Ok(())
}
2022-10-01 02:37:55 +00:00
pub fn cmd_reply(&self, rest: Option<String>, callback: UICallback) -> Result<(), String> {
trace!("CommandProcessor::cmd_reply");
2023-06-08 18:07:09 +00:00
let capi = self.capi();
let ui = self.ui_sender();
2022-10-01 02:37:55 +00:00
let some_last_id = self.inner_mut().last_call_id.take();
spawn_detached_local(async move {
let (first, second) = Self::word_split(&rest.clone().unwrap_or_default());
let (id, msg) = if let Some(second) = second {
let id = match u64::from_str(&first) {
Err(e) => {
2023-06-21 02:18:59 +00:00
ui.add_node_event(Level::Error, format!("invalid appcall id: {}", e));
2022-10-01 02:37:55 +00:00
ui.send_callback(callback);
return;
}
Ok(v) => v,
};
2023-06-08 18:07:09 +00:00
(id, second)
2022-10-01 02:37:55 +00:00
} else {
let id = match some_last_id {
None => {
2023-06-21 02:18:59 +00:00
ui.add_node_event(Level::Error, "must specify last call id".to_owned());
2022-10-01 02:37:55 +00:00
ui.send_callback(callback);
return;
}
Some(v) => v,
};
(id, rest.unwrap_or_default())
};
let msg = if msg[0..1] == "#".to_owned() {
match hex::decode(msg[1..].as_bytes().to_vec()) {
Err(e) => {
2023-06-21 02:18:59 +00:00
ui.add_node_event(Level::Error, format!("invalid hex message: {}", e));
2022-10-01 02:37:55 +00:00
ui.send_callback(callback);
return;
}
Ok(v) => v,
}
} else {
msg[1..].as_bytes().to_vec()
};
let msglen = msg.len();
match capi.server_appcall_reply(id, msg).await {
Ok(()) => {
2023-06-21 02:18:59 +00:00
ui.add_node_event(
Level::Info,
format!("reply sent to {} : {} bytes", id, msglen),
);
2022-10-01 02:37:55 +00:00
ui.send_callback(callback);
return;
}
Err(e) => {
ui.display_string_dialog(
"Server command 'appcall_reply' failed",
e.to_string(),
callback,
);
}
}
});
Ok(())
}
2021-11-22 16:28:30 +00:00
pub fn run_command(&self, command_line: &str, callback: UICallback) -> Result<(), String> {
//
let (cmd, rest) = Self::word_split(command_line);
match cmd.as_str() {
"help" => self.cmd_help(rest, callback),
"exit" => self.cmd_exit(callback),
"quit" => self.cmd_exit(callback),
"disconnect" => self.cmd_disconnect(callback),
"shutdown" => self.cmd_shutdown(callback),
"attach" => self.cmd_attach(callback),
"detach" => self.cmd_detach(callback),
2021-12-11 01:14:33 +00:00
"debug" => self.cmd_debug(rest, callback),
2022-07-01 16:13:52 +00:00
"change_log_level" => self.cmd_change_log_level(rest, callback),
2022-10-01 02:37:55 +00:00
"reply" => self.cmd_reply(rest, callback),
2021-11-22 16:28:30 +00:00
_ => {
2023-06-08 18:07:09 +00:00
let ui = self.ui_sender();
2021-12-11 01:14:33 +00:00
ui.send_callback(callback);
2021-11-22 16:28:30 +00:00
Err(format!("Invalid command: {}", cmd))
}
}
}
2023-06-08 18:07:09 +00:00
pub async fn connection_manager(&self) {
2021-11-22 16:28:30 +00:00
// Connect until we're done
while !self.inner_mut().finished {
// Wait for connection request
if !self.inner().autoconnect {
let waker = self.inner_mut().connection_waker.instance_clone(());
waker.await;
} else {
self.inner_mut().autoconnect = false;
}
self.inner_mut().connection_waker.reset();
// Loop while we want to keep the connection
let mut first = true;
while self.inner().reconnect {
2021-11-28 02:31:01 +00:00
let server_addr_opt = self.inner_mut().server_addr;
2021-11-22 16:28:30 +00:00
let server_addr = match server_addr_opt {
None => break,
Some(addr) => addr,
};
if first {
info!("Connecting to server at {}", server_addr);
self.set_connection_state(ConnectionState::Retrying(
2021-11-28 02:31:01 +00:00
server_addr,
2021-11-22 16:28:30 +00:00
SystemTime::now(),
));
} else {
debug!("Retrying connection to {}", server_addr);
}
2023-06-08 18:07:09 +00:00
let capi = self.capi();
2021-11-28 02:31:01 +00:00
let res = capi.connect(server_addr).await;
if res.is_ok() {
2021-11-22 16:28:30 +00:00
info!(
"Connection to server at {} terminated normally",
server_addr
);
break;
}
if !self.inner().autoreconnect {
info!("Connection to server lost.");
break;
}
self.set_connection_state(ConnectionState::Retrying(
2021-11-28 02:31:01 +00:00
server_addr,
2021-11-22 16:28:30 +00:00
SystemTime::now(),
));
debug!("Connection lost, retrying in 2 seconds");
{
let waker = self.inner_mut().connection_waker.instance_clone(());
2022-12-02 00:59:19 +00:00
let _ = timeout(2000, waker).await;
2021-11-22 16:28:30 +00:00
}
self.inner_mut().connection_waker.reset();
first = false;
}
info!("Disconnected.");
self.set_connection_state(ConnectionState::Disconnected);
self.inner_mut().reconnect = true;
}
}
// called by ui
////////////////////////////////////////////
2023-06-08 18:07:09 +00:00
pub fn set_server_address(&self, server_addr: Option<SocketAddr>) {
2021-11-22 16:28:30 +00:00
self.inner_mut().server_addr = server_addr;
}
pub fn get_server_address(&self) -> Option<SocketAddr> {
2021-11-28 02:31:01 +00:00
self.inner().server_addr
2021-11-22 16:28:30 +00:00
}
// called by client_api_connection
// calls into ui
////////////////////////////////////////////
2022-11-16 17:49:53 +00:00
2023-06-21 02:18:59 +00:00
pub fn log_message(&self, log_level: Level, message: String) {
self.inner().ui_sender.add_node_event(log_level, message);
2023-06-08 01:55:23 +00:00
}
2023-06-09 23:08:49 +00:00
pub fn update_attachment(&self, attachment: &json::JsonValue) {
2023-06-08 18:07:09 +00:00
self.inner_mut().ui_sender.set_attachment_state(
attachment["state"].as_str().unwrap_or_default().to_owned(),
attachment["public_internet_ready"]
.as_bool()
.unwrap_or_default(),
attachment["local_network_ready"]
.as_bool()
.unwrap_or_default(),
2022-12-26 21:33:48 +00:00
);
2021-11-22 16:28:30 +00:00
}
2023-06-09 23:08:49 +00:00
pub fn update_network_status(&self, network: &json::JsonValue) {
2023-06-08 18:07:09 +00:00
self.inner_mut().ui_sender.set_network_status(
network["started"].as_bool().unwrap_or_default(),
json_str_u64(&network["bps_down"]),
json_str_u64(&network["bps_up"]),
network["peers"]
.members()
.cloned()
.collect::<Vec<json::JsonValue>>(),
2022-09-06 20:49:43 +00:00
);
2022-05-16 15:52:48 +00:00
}
2023-06-09 23:08:49 +00:00
pub fn update_config(&self, config: &json::JsonValue) {
2023-06-08 18:07:09 +00:00
self.inner_mut().ui_sender.set_config(&config["config"])
2022-11-16 17:49:53 +00:00
}
2023-06-09 23:08:49 +00:00
pub fn update_route(&self, route: &json::JsonValue) {
2022-11-25 19:21:55 +00:00
let mut out = String::new();
2023-06-08 18:07:09 +00:00
if route["dead_routes"].len() != 0 {
out.push_str(&format!("Dead routes: {:?}", route["dead_routes"]));
2022-11-25 19:21:55 +00:00
}
2023-06-08 18:07:09 +00:00
if route["dead_routes"].len() != 0 {
2022-11-25 19:21:55 +00:00
if !out.is_empty() {
out.push_str("\n");
}
out.push_str(&format!(
"Dead remote routes: {:?}",
2023-06-08 18:07:09 +00:00
route["dead_remote_routes"]
2022-11-25 19:21:55 +00:00
));
}
if !out.is_empty() {
2023-06-21 02:18:59 +00:00
self.inner().ui_sender.add_node_event(Level::Info, out);
2022-11-25 19:21:55 +00:00
}
2022-11-25 01:17:54 +00:00
}
2023-06-09 23:08:49 +00:00
pub fn update_value_change(&self, value_change: &json::JsonValue) {
2023-06-08 01:55:23 +00:00
let out = format!("Value change: {:?}", value_change.as_str().unwrap_or("???"));
2023-06-21 02:18:59 +00:00
self.inner().ui_sender.add_node_event(Level::Info, out);
2023-03-01 20:50:30 +00:00
}
2022-05-16 15:52:48 +00:00
2023-06-09 23:08:49 +00:00
pub fn update_log(&self, log: &json::JsonValue) {
2023-06-21 02:18:59 +00:00
let log_level =
Level::from_str(log["log_level"].as_str().unwrap_or("error")).unwrap_or(Level::Error);
self.inner().ui_sender.add_node_event(
log_level,
format!(
"{}: {}{}",
log["log_level"].as_str().unwrap_or("???"),
log["message"].as_str().unwrap_or("???"),
if let Some(bt) = log["backtrace"].as_str() {
format!("\nBacktrace:\n{}", bt)
} else {
"".to_owned()
}
),
);
2022-06-08 13:33:41 +00:00
}
2023-06-09 23:08:49 +00:00
pub fn update_app_message(&self, msg: &json::JsonValue) {
2023-06-08 18:07:09 +00:00
let message = json_str_vec_u8(&msg["message"]);
2022-10-01 02:37:55 +00:00
// check is message body is ascii printable
let mut printable = true;
2023-06-08 18:07:09 +00:00
for c in &message {
2022-10-01 02:37:55 +00:00
if *c < 32 || *c > 126 {
printable = false;
}
}
let strmsg = if printable {
2023-06-08 18:07:09 +00:00
String::from_utf8_lossy(&message).to_string()
2022-10-01 02:37:55 +00:00
} else {
2023-06-08 18:07:09 +00:00
hex::encode(message)
2022-10-01 02:37:55 +00:00
};
2023-06-21 02:18:59 +00:00
self.inner().ui_sender.add_node_event(
Level::Info,
format!("AppMessage ({:?}): {}", msg["sender"], strmsg),
);
2022-10-01 02:37:55 +00:00
}
2023-06-09 23:08:49 +00:00
pub fn update_app_call(&self, call: &json::JsonValue) {
2023-06-08 18:07:09 +00:00
let message = json_str_vec_u8(&call["message"]);
2022-10-01 02:37:55 +00:00
// check is message body is ascii printable
let mut printable = true;
2023-06-08 18:07:09 +00:00
for c in &message {
2022-10-01 02:37:55 +00:00
if *c < 32 || *c > 126 {
printable = false;
}
}
let strmsg = if printable {
2023-06-08 18:07:09 +00:00
String::from_utf8_lossy(&message).to_string()
2022-10-01 02:37:55 +00:00
} else {
2023-06-08 18:07:09 +00:00
format!("#{}", hex::encode(&message))
2022-10-01 02:37:55 +00:00
};
2023-06-16 15:57:55 +00:00
let id = json_str_u64(&call["call_id"]);
2023-06-08 18:07:09 +00:00
2023-06-21 02:18:59 +00:00
self.inner().ui_sender.add_node_event(
Level::Info,
format!(
"AppCall ({:?}) id = {:016x} : {}",
call["sender"], id, strmsg
),
);
2022-10-01 02:37:55 +00:00
2023-06-08 18:07:09 +00:00
self.inner_mut().last_call_id = Some(id);
2022-10-01 02:37:55 +00:00
}
2023-06-08 18:07:09 +00:00
pub fn update_shutdown(&self) {
2022-06-08 13:33:41 +00:00
// Do nothing with this, we'll process shutdown when rpc connection closes
2021-12-11 03:04:38 +00:00
}
2021-11-22 16:28:30 +00:00
// called by client_api_connection
// calls into ui
////////////////////////////////////////////
2023-06-08 18:07:09 +00:00
pub fn set_connection_state(&self, state: ConnectionState) {
self.inner_mut().ui_sender.set_connection_state(state);
2021-11-22 16:28:30 +00:00
}
// called by ui
////////////////////////////////////////////
2023-06-08 18:07:09 +00:00
pub fn start_connection(&self) {
2021-11-22 16:28:30 +00:00
self.inner_mut().reconnect = true;
self.inner_mut().connection_waker.resolve();
}
2023-06-08 18:07:09 +00:00
// pub fn stop_connection(&self) {
2021-11-22 16:28:30 +00:00
// self.inner_mut().reconnect = false;
// let mut capi = self.capi().clone();
2022-06-28 03:46:29 +00:00
// spawn_detached(async move {
2021-11-22 16:28:30 +00:00
// capi.disconnect().await;
// });
// }
2023-06-08 18:07:09 +00:00
pub fn cancel_reconnect(&self) {
2021-11-22 16:28:30 +00:00
self.inner_mut().reconnect = false;
self.inner_mut().connection_waker.resolve();
}
2023-06-08 18:07:09 +00:00
pub fn quit(&self) {
2021-11-22 16:28:30 +00:00
self.inner_mut().finished = true;
self.inner_mut().reconnect = false;
self.inner_mut().connection_waker.resolve();
}
// called by ui
// calls into client_api_connection
////////////////////////////////////////////
2023-06-08 18:07:09 +00:00
pub fn attach(&self) {
let capi = self.capi();
2021-11-22 16:28:30 +00:00
2022-06-28 03:46:29 +00:00
spawn_detached_local(async move {
2021-11-22 16:28:30 +00:00
if let Err(e) = capi.server_attach().await {
error!("Server command 'attach' failed to execute: {}", e);
}
});
}
2023-06-08 18:07:09 +00:00
pub fn detach(&self) {
let capi = self.capi();
2021-11-22 16:28:30 +00:00
2022-06-28 03:46:29 +00:00
spawn_detached_local(async move {
2021-11-22 16:28:30 +00:00
if let Err(e) = capi.server_detach().await {
error!("Server command 'detach' failed to execute: {}", e);
}
});
}
}