pub mod proto;
pub mod transport;
pub use proto::{ControlRequest, ControlResponse};
pub use transport::{
bind, dispatch, serve, AdminBootstrapCodeFn, AdminBootstrapFn, ControlContext, ControlServer,
ControlSocketName,
};
use crate::workspace::{expand_and_canonicalize, WorkspaceFlags, WorkspaceInfo};
#[derive(Debug, thiserror::Error)]
pub enum ControlError {
#[error("control socket transport error: {0}")]
Transport(#[from] std::io::Error),
#[error("running markon server rejected the request: {0}")]
Server(String),
#[error("unexpected control response for this request")]
Unexpected,
}
#[derive(Clone)]
pub struct RunningServer {
socket: ControlSocketName,
web_port: u16,
web_host: String,
web_advertised_host: Option<String>,
}
impl RunningServer {
pub fn new(socket: ControlSocketName) -> Self {
Self {
socket,
web_port: 0,
web_host: String::new(),
web_advertised_host: None,
}
}
pub fn from_lock(lock: &crate::workspace::ServerLock) -> Self {
let socket = if lock.control_socket.is_empty() {
ControlSocketName::default_name()
.unwrap_or_else(|_| ControlSocketName::from_raw(String::new()))
} else {
ControlSocketName::from_raw(lock.control_socket.clone())
};
Self {
socket,
web_port: lock.port,
web_host: lock.host.clone(),
web_advertised_host: lock.advertised_host.clone(),
}
}
pub fn discover() -> Option<Self> {
let lock = crate::workspace::ServerLock::read()?;
lock.is_alive().then(|| Self::from_lock(&lock))
}
pub fn socket(&self) -> &ControlSocketName {
&self.socket
}
pub fn port(&self) -> u16 {
self.web_port
}
pub fn host(&self) -> &str {
&self.web_host
}
pub fn advertised_host(&self) -> Option<&str> {
self.web_advertised_host.as_deref()
}
pub fn is_reachable(&self) -> bool {
if !self.socket.as_str().is_empty() && transport::probe(&self.socket) {
return true;
}
if self.web_port == 0 {
return false;
}
let connect_host = if crate::net::host_is_wildcard_v6(&self.web_host) {
"::1"
} else if crate::net::host_is_wildcard_v4(&self.web_host) || self.web_host.is_empty() {
"127.0.0.1"
} else {
self.web_host.as_str()
};
let Ok(addr) = crate::net::bind_socket_addr(connect_host, self.web_port) else {
return false;
};
std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(500)).is_ok()
}
async fn call(&self, req: ControlRequest) -> Result<ControlResponse, ControlError> {
match transport::request(&self.socket, &req).await? {
ControlResponse::Err(msg) => Err(ControlError::Server(msg)),
other => Ok(other),
}
}
pub async fn list_workspaces(&self) -> Result<Vec<WorkspaceInfo>, ControlError> {
match self.call(ControlRequest::ListWorkspaces).await? {
ControlResponse::Workspaces(w) => Ok(w),
_ => Err(ControlError::Unexpected),
}
}
pub async fn add_workspace(
&self,
path: &str,
flags: WorkspaceFlags,
collaborator_access_code_hash: &str,
) -> Result<String, ControlError> {
match self
.call(ControlRequest::AddWorkspace {
path: path.to_string(),
flags,
collaborator_access_code_hash: collaborator_access_code_hash.to_string(),
single_file: None,
alias: String::new(),
})
.await?
{
ControlResponse::WorkspaceId(id) => Ok(id),
_ => Err(ControlError::Unexpected),
}
}
pub async fn add_single_file(
&self,
path: &str,
single_file: &str,
flags: WorkspaceFlags,
collaborator_access_code_hash: &str,
) -> Result<String, ControlError> {
match self
.call(ControlRequest::AddWorkspace {
path: path.to_string(),
flags,
collaborator_access_code_hash: collaborator_access_code_hash.to_string(),
single_file: Some(single_file.to_string()),
alias: String::new(),
})
.await?
{
ControlResponse::WorkspaceId(id) => Ok(id),
_ => Err(ControlError::Unexpected),
}
}
pub async fn add_or_update_workspace(
&self,
path: &str,
flags: WorkspaceFlags,
collaborator_access_code_hash: Option<&str>,
) -> Result<String, ControlError> {
self.add_or_update_workspace_scoped(path, flags, None, collaborator_access_code_hash, None)
.await
}
pub async fn add_or_update_workspace_scoped(
&self,
path: &str,
flags: WorkspaceFlags,
single_file: Option<&str>,
collaborator_access_code_hash: Option<&str>,
alias: Option<&str>,
) -> Result<String, ControlError> {
let canonical_path = expand_and_canonicalize(path)
.map(|value| value.to_string_lossy().into_owned())
.unwrap_or_else(|_| path.to_string());
let existing = self
.list_workspaces()
.await?
.into_iter()
.find(|w| w.path == canonical_path && w.single_file.as_deref() == single_file);
if let Some(existing) = existing {
self.update_flags(&existing.id, flags).await?;
if let Some(hash) = collaborator_access_code_hash {
self.set_access_code(&existing.id, Some(hash)).await?;
}
if let Some(alias) = alias {
self.set_alias(&existing.id, alias).await?;
}
return Ok(existing.id);
}
match self
.call(ControlRequest::AddWorkspace {
path: canonical_path,
flags,
collaborator_access_code_hash: collaborator_access_code_hash
.unwrap_or("")
.to_string(),
single_file: single_file.map(str::to_string),
alias: alias.unwrap_or("").to_string(),
})
.await?
{
ControlResponse::WorkspaceId(id) => Ok(id),
_ => Err(ControlError::Unexpected),
}
}
pub async fn update_flags(&self, id: &str, flags: WorkspaceFlags) -> Result<(), ControlError> {
match self
.call(ControlRequest::UpdateFlags {
id: id.to_string(),
flags,
})
.await?
{
ControlResponse::Ok => Ok(()),
_ => Err(ControlError::Unexpected),
}
}
pub async fn set_alias(&self, id: &str, alias: &str) -> Result<(), ControlError> {
match self
.call(ControlRequest::SetAlias {
id: id.to_string(),
alias: alias.to_string(),
})
.await?
{
ControlResponse::Ok => Ok(()),
_ => Err(ControlError::Unexpected),
}
}
pub async fn remove_workspace(&self, id: &str) -> Result<(), ControlError> {
match self
.call(ControlRequest::RemoveWorkspace { id: id.to_string() })
.await?
{
ControlResponse::Ok => Ok(()),
_ => Err(ControlError::Unexpected),
}
}
pub async fn set_access_code(
&self,
id: &str,
collaborator_access_code_hash: Option<&str>,
) -> Result<(), ControlError> {
match self
.call(ControlRequest::SetAccessCode {
id: id.to_string(),
collaborator_access_code_hash: collaborator_access_code_hash.map(str::to_string),
})
.await?
{
ControlResponse::Ok => Ok(()),
_ => Err(ControlError::Unexpected),
}
}
pub async fn admin_bootstrap(&self, redirect: &str) -> Result<String, ControlError> {
match self
.call(ControlRequest::AdminBootstrap {
redirect: redirect.to_string(),
})
.await?
{
ControlResponse::Url(url) => Ok(url),
_ => Err(ControlError::Unexpected),
}
}
pub async fn admin_bootstrap_code(
&self,
redirect: &str,
) -> Result<(String, String), ControlError> {
match self
.call(ControlRequest::AdminBootstrapCode {
redirect: redirect.to_string(),
})
.await?
{
ControlResponse::AdminCode { url, code } => Ok((url, code)),
_ => Err(ControlError::Unexpected),
}
}
pub async fn shutdown(&self) -> Result<(), ControlError> {
match self.call(ControlRequest::Shutdown).await? {
ControlResponse::Ok => Ok(()),
_ => Err(ControlError::Unexpected),
}
}
}
#[cfg(test)]
mod tests;