use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
use nomoreide_core::remote::connector::{CommandSink, ConnectorConfig, RelaySnapshot, RelayStatus};
use nomoreide_core::remote::credentials::RemoteCredentials;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StartOutcome {
Started,
AlreadyRunning,
NotPaired,
Disabled,
}
struct Connection {
device_id: String,
status: RelayStatus,
task: tokio::task::AbortHandle,
}
#[derive(Clone)]
pub(crate) struct RelaySupervisor {
state_dir: PathBuf,
credential: String,
terminal: nomoreide_core::terminal::TerminalManager,
router: Arc<OnceLock<axum::Router>>,
running: Arc<Mutex<Option<Connection>>>,
outbound: nomoreide_core::remote::connector::RelayOutbound,
}
impl RelaySupervisor {
pub(crate) fn new(
state_dir: PathBuf,
credential: String,
terminal: nomoreide_core::terminal::TerminalManager,
) -> Self {
Self {
state_dir,
credential,
terminal,
router: Arc::new(OnceLock::new()),
running: Arc::new(Mutex::new(None)),
outbound: nomoreide_core::remote::connector::RelayOutbound::new(),
}
}
pub(crate) fn attach_router(&self, router: axum::Router) {
let _ = self.router.set(router);
}
pub(crate) fn ensure_started(&self) -> StartOutcome {
if super::disabled_by_environment() {
return StartOutcome::Disabled;
}
let credentials = RemoteCredentials::new(&self.state_dir);
let Some(stored) = credentials.load() else {
return StartOutcome::NotPaired;
};
let Some(router) = self.router.get() else {
return StartOutcome::NotPaired;
};
let mut running = match self.running.lock() {
Ok(running) => running,
Err(poisoned) => poisoned.into_inner(),
};
if let Some(connection) = running.as_ref() {
if connection.device_id == stored.device_id {
return StartOutcome::AlreadyRunning;
}
connection.task.abort();
self.outbound.disarm_now();
}
let mut config = ConnectorConfig::from_credential(&stored);
config.capabilities = super::dispatcher::served_capabilities();
let status = RelayStatus::new(&config, &stored.device_name);
let device_id = stored.device_id.clone();
let sink: Arc<dyn CommandSink> = Arc::new(super::dispatcher::RouterDispatcher::new(
router.clone(),
self.credential.clone(),
stored.device_id,
stored.device_name.clone(),
self.terminal.clone(),
));
let task = tokio::spawn(nomoreide_core::remote::connector::run_forever(
config,
sink,
status.clone(),
self.outbound.clone(),
));
*running = Some(Connection {
device_id,
status,
task: task.abort_handle(),
});
StartOutcome::Started
}
pub(crate) fn stop(&self) {
let mut running = match self.running.lock() {
Ok(running) => running,
Err(poisoned) => poisoned.into_inner(),
};
if let Some(connection) = running.take() {
connection.task.abort();
}
self.outbound.disarm_now();
}
pub(crate) fn snapshot(&self) -> Option<RelaySnapshot> {
let running = match self.running.lock() {
Ok(running) => running,
Err(poisoned) => poisoned.into_inner(),
};
running
.as_ref()
.map(|connection| connection.status.snapshot())
}
pub(crate) fn retire(&self) -> bool {
use nomoreide_core::remote::protocol::platform_bound::{
DeviceRetire, PlatformBound, RetireReason,
};
self.outbound
.try_send(PlatformBound::DeviceRetire(DeviceRetire {
reason: RetireReason::Unpaired,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch(label: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"nomoreide-supervisor-{label}-{}-{}",
std::process::id(),
uuid::Uuid::new_v4()
));
std::fs::create_dir_all(&dir).expect("scratch");
dir
}
#[test]
fn an_unpaired_machine_does_not_connect() {
let supervisor = RelaySupervisor::new(
scratch("unpaired"),
"cred".into(),
nomoreide_core::terminal::TerminalManager::new(),
);
supervisor.attach_router(axum::Router::new());
assert_eq!(supervisor.ensure_started(), StartOutcome::NotPaired);
assert_eq!(supervisor.snapshot(), None);
}
#[test]
fn nothing_starts_before_the_router_exists() {
let dir = scratch("no-router");
write_credential(&dir);
let supervisor = RelaySupervisor::new(
dir,
"cred".into(),
nomoreide_core::terminal::TerminalManager::new(),
);
assert_eq!(supervisor.ensure_started(), StartOutcome::NotPaired);
}
#[tokio::test]
async fn starting_twice_starts_once() {
let dir = scratch("twice");
write_credential(&dir);
let supervisor = RelaySupervisor::new(
dir,
"cred".into(),
nomoreide_core::terminal::TerminalManager::new(),
);
supervisor.attach_router(axum::Router::new());
assert_eq!(supervisor.ensure_started(), StartOutcome::Started);
assert_eq!(supervisor.ensure_started(), StartOutcome::AlreadyRunning);
assert_eq!(supervisor.ensure_started(), StartOutcome::AlreadyRunning);
}
#[tokio::test]
async fn a_started_connector_reports_itself_before_it_is_connected() {
let dir = scratch("status");
write_credential(&dir);
let supervisor = RelaySupervisor::new(
dir,
"cred".into(),
nomoreide_core::terminal::TerminalManager::new(),
);
supervisor.attach_router(axum::Router::new());
supervisor.ensure_started();
let snapshot = supervisor.snapshot().expect("a status once started");
assert_eq!(snapshot.device_name, "Test Machine");
assert!(!snapshot.connected);
}
#[tokio::test]
async fn pairing_again_reconnects_as_the_new_device() {
let dir = scratch("repair");
write_credential(&dir);
let supervisor = RelaySupervisor::new(
dir.clone(),
"cred".into(),
nomoreide_core::terminal::TerminalManager::new(),
);
supervisor.attach_router(axum::Router::new());
assert_eq!(supervisor.ensure_started(), StartOutcome::Started);
assert_eq!(
supervisor.snapshot().expect("a status").device_name,
"Test Machine"
);
write_credential_named(&dir, "99999999-8888-7777-6666-555555555555", "Mac");
assert_eq!(
supervisor.ensure_started(),
StartOutcome::Started,
"a different device on disk must start a connector, not report the old one"
);
assert_eq!(
supervisor.snapshot().expect("a status").device_name,
"Mac",
"the reported machine must be the one that is actually connecting"
);
}
#[tokio::test]
async fn unpairing_stops_the_connection() {
let dir = scratch("stop");
write_credential(&dir);
let supervisor = RelaySupervisor::new(
dir,
"cred".into(),
nomoreide_core::terminal::TerminalManager::new(),
);
supervisor.attach_router(axum::Router::new());
supervisor.ensure_started();
supervisor.stop();
assert_eq!(supervisor.snapshot(), None);
assert!(
!supervisor.retire(),
"a stopped connection has nowhere to send"
);
}
fn write_credential(dir: &std::path::Path) {
write_credential_named(dir, "11111111-2222-3333-4444-555555555555", "Test Machine");
}
fn write_credential_named(dir: &std::path::Path, device_id: &str, device_name: &str) {
RemoteCredentials::new(dir)
.store(&nomoreide_core::remote::credentials::StoredCredential {
device_id: device_id.into(),
device_name: device_name.into(),
credential: "c".repeat(64),
platform_base_url: "http://127.0.0.1:1".into(),
web_base_url: String::new(),
paired_at: "2026-09-02T00:00:00Z".into(),
})
.expect("store");
}
}