pub use simploxide_core::SimplexVersion;
use tokio::process::{Child, Command};
use std::{
ffi::OsString,
io,
iter::{Chain, Empty, Once},
process::Stdio,
};
pub struct SimplexCli {
handle: Option<Child>,
port: u16,
version: SimplexVersion,
}
impl SimplexCli {
const MIN_SUPPORTED_VERSION: SimplexVersion = simploxide_core::MIN_SUPPORTED_VERSION;
const MAX_SUPPORTED_VERSION: SimplexVersion = simploxide_core::MAX_SUPPORTED_VERSION;
pub fn builder(default_bot_name: impl Into<String>, port: u16) -> SimplexCliBuilder {
SimplexCliBuilder {
port,
default_bot_name: default_bot_name.into(),
db_path: "bot".into(),
db_key: None,
extra_args: std::iter::empty(),
}
}
pub fn port(&self) -> u16 {
self.port
}
pub fn version(&self) -> &SimplexVersion {
&self.version
}
pub async fn kill(&mut self) -> io::Result<()> {
if let Some(mut handle) = self.handle.take() {
handle.kill().await?;
}
Ok(())
}
}
impl Drop for SimplexCli {
fn drop(&mut self) {
if let Some(ref mut handle) = self.handle {
if handle.try_wait().ok().flatten().is_none() {
let _ = handle.start_kill();
let _ = handle.try_wait();
}
}
}
}
pub struct SimplexCliBuilder<I = Empty<OsString>> {
port: u16,
default_bot_name: String,
db_path: String,
db_key: Option<String>,
extra_args: I,
}
impl<I> SimplexCliBuilder<I>
where
I: Iterator<Item = OsString>,
{
pub fn db_prefix(mut self, path: impl Into<String>) -> Self {
self.db_path = path.into();
self
}
pub fn db_key(mut self, key: impl Into<String>) -> Self {
self.db_key = Some(key.into());
self
}
pub fn arg(self, arg: impl Into<OsString>) -> SimplexCliBuilder<Chain<I, Once<OsString>>> {
SimplexCliBuilder {
port: self.port,
default_bot_name: self.default_bot_name,
db_path: self.db_path,
db_key: self.db_key,
extra_args: self.extra_args.chain(std::iter::once(arg.into())),
}
}
pub fn args<J>(self, args: J) -> SimplexCliBuilder<Chain<I, J::IntoIter>>
where
J: IntoIterator<Item = OsString>,
{
SimplexCliBuilder {
port: self.port,
default_bot_name: self.default_bot_name,
db_path: self.db_path,
db_key: self.db_key,
extra_args: self.extra_args.chain(args),
}
}
pub async fn spawn(self) -> io::Result<SimplexCli> {
let sxc_cmd = if std::path::Path::new("./simplex-chat").exists() {
"./simplex-chat"
} else {
"simplex-chat"
};
let version_output = Command::new(sxc_cmd).arg("--version").output().await?;
let output_str = String::from_utf8(version_output.stdout)
.map_err(|_| io::Error::other("simplex-chat --version returned invalid string"))?;
let version_str = output_str
.lines()
.next()
.and_then(|line| line.trim().strip_prefix("SimpleX Chat v"))
.ok_or_else(|| {
io::Error::other(format!("Cannot parse SimpleX Chat version: {output_str:?}"))
})?;
let version: SimplexVersion = version_str.parse().map_err(|_| {
io::Error::other(format!(
"Cannot parse SimpleX Chat version: {version_str:?}"
))
})?;
if !version.is_supported() {
return Err(io::Error::other(format!(
"The Simplex CLI {version} is incompatible with current simploxide version\n\
Supported CLI versions: {}...{}",
SimplexCli::MIN_SUPPORTED_VERSION,
SimplexCli::MAX_SUPPORTED_VERSION
)));
}
let mut cmd = Command::new(sxc_cmd);
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(unix)]
cmd.process_group(0);
cmd.arg("-d")
.arg(&self.db_path)
.arg("-p")
.arg(self.port.to_string())
.arg("--create-bot-display-name")
.arg(&self.default_bot_name);
if let Some(ref key) = self.db_key {
cmd.arg("-k").arg(key);
}
cmd.args(self.extra_args);
let mut handle = cmd.spawn()?;
if let Ok(ret) =
tokio::time::timeout(std::time::Duration::from_secs(2), handle.wait()).await
{
let exit = ret?;
return Err(io::Error::other(format!(
"SimpleX-CLI terminated unexpectedly: {exit}"
)));
}
Ok(SimplexCli {
handle: Some(handle),
port: self.port,
version,
})
}
}