use std::ffi::OsStr;
use std::path::{Path, PathBuf};
#[cfg(feature = "model-relay")]
use std::time::Duration;
#[cfg(feature = "model-relay")]
use crate::error::{OlError, ERR_CLINE_APP_NOT_CAPTURED};
#[cfg(feature = "model-relay")]
use crate::hooks::bindings::cline::ClineBinding;
#[cfg(feature = "model-relay")]
pub(crate) mod process;
#[cfg(feature = "model-relay")]
use process::ProcessTable;
pub(crate) const BUNDLE_ID: &str = "bot.cline.app";
pub const APP_DIR_ENV: &str = "OPENLATCH_CLINE_APP_DIR";
const MAX_PLIST_BYTES: u64 = 256 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ClineApp {
pub(crate) bundle: PathBuf,
pub(crate) sidecar: PathBuf,
pub(crate) version: String,
}
pub(crate) fn detect() -> Option<ClineApp> {
detect_in(&app_roots())
}
fn app_dir_value() -> Option<std::ffi::OsString> {
std::env::var_os(APP_DIR_ENV).filter(|v| !v.is_empty())
}
fn app_roots() -> Vec<PathBuf> {
match app_dir_value() {
Some(dir) => {
let dir = PathBuf::from(dir);
if dir.is_absolute() {
vec![dir.join("Cline.app")]
} else {
Vec::new()
}
}
None => production_roots(dirs::home_dir().as_deref()),
}
}
fn production_roots(home: Option<&Path>) -> Vec<PathBuf> {
if !cfg!(target_os = "macos") {
return Vec::new();
}
let mut v = vec![PathBuf::from("/Applications").join("Cline.app")]; if let Some(h) = home {
v.push(h.join("Applications").join("Cline.app"));
}
v
}
pub(crate) fn not_detectable_here() -> bool {
not_detectable_with(
app_dir_value().as_deref(),
&production_roots(dirs::home_dir().as_deref()),
)
}
fn not_detectable_with(app_dir: Option<&OsStr>, production: &[PathBuf]) -> bool {
app_dir.is_none() && production.is_empty()
}
fn detect_in(roots: &[PathBuf]) -> Option<ClineApp> {
roots.iter().find_map(|bundle| {
let plist = bundle.join("Contents").join("Info.plist");
let len = std::fs::metadata(&plist).ok()?.len();
if len > MAX_PLIST_BYTES {
return None;
}
let xml = std::fs::read_to_string(&plist).ok()?;
if plist_string(&xml, "CFBundleIdentifier").as_deref() != Some(BUNDLE_ID) {
return None;
}
let version = plist_string(&xml, "CFBundleShortVersionString")?;
let sidecar = bundle.join("Contents").join("MacOS").join("code-sidecar");
if !sidecar.is_file() {
return None;
}
Some(ClineApp {
bundle: bundle.clone(),
sidecar,
version,
})
})
}
fn plist_string(xml: &str, key: &str) -> Option<String> {
if xml.starts_with("bplist00") {
return None;
}
let needle = format!("<key>{key}</key>");
let after = &xml[xml.find(&needle)? + needle.len()..];
let rest = after.trim_start().strip_prefix("<string>")?;
let raw = &rest[..rest.find("</string>")?];
Some(decode_xml_entities(raw))
}
fn decode_xml_entities(raw: &str) -> String {
raw.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'")
.replace("&", "&")
}
#[cfg(feature = "model-relay")]
pub(crate) const HUB_ARG: &str = "--cline-hub-daemon";
#[cfg(feature = "model-relay")]
pub(crate) const HUB_PORT_ENV: &str = "CLINE_HUB_PORT";
#[cfg(feature = "model-relay")]
pub(crate) const HUB_DAEMON_ENV: &str = "CLINE_RUN_AS_HUB_DAEMON";
#[cfg(feature = "model-relay")]
pub(crate) const HUB_HOST_ENV: &str = "CLINE_HUB_HOST";
#[cfg(feature = "model-relay")]
const HUB_HOST: &str = "127.0.0.1";
#[cfg(feature = "model-relay")]
pub(crate) const DEFAULT_HUB_PORT: u16 = 25463;
#[cfg(feature = "model-relay")]
pub(crate) const WRAPPER_VARS: [&str; 7] = [
"HTTPS_PROXY",
"HTTP_PROXY",
"https_proxy",
"http_proxy",
"NODE_EXTRA_CA_CERTS",
"NO_PROXY",
"no_proxy",
];
#[cfg(feature = "model-relay")]
pub(crate) const HUB_STARTUP_POLL: Duration = Duration::from_millis(200);
#[cfg(feature = "model-relay")]
const HUB_STARTUP_POLLS: u32 = 40;
#[cfg(feature = "model-relay")]
const HUB_RELEASE_POLLS: u32 = 10;
#[cfg(feature = "model-relay")]
pub(crate) fn hub_port() -> u16 {
std::env::var(HUB_PORT_ENV)
.ok()
.and_then(|v| v.trim().parse::<u16>().ok())
.filter(|p| *p != 0)
.unwrap_or(DEFAULT_HUB_PORT)
}
#[cfg(feature = "model-relay")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum StaleWhy {
NoProxyEnv,
OlderApp { hub: String, installed: String },
ProxyChanged,
}
#[cfg(feature = "model-relay")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum HubState {
Absent,
Ours {
pid: u32,
},
Foreign {
pid: u32,
},
Stale {
pid: u32,
why: StaleWhy,
},
}
#[cfg(feature = "model-relay")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProxyEnvVars {
pub(crate) proxy_url: String,
pub(crate) ca_pem: PathBuf,
pub(crate) no_proxy: String,
}
#[cfg(feature = "model-relay")]
impl ProxyEnvVars {
pub(crate) fn for_relay(relay_port: u16) -> ProxyEnvVars {
ProxyEnvVars {
proxy_url: format!("http://127.0.0.1:{relay_port}"),
ca_pem: this_install_ca_pem(),
no_proxy: crate::hooks::proxy_env_file::relay_no_proxy(relay_port),
}
}
pub(crate) fn overrides(
&self,
inherited_upper: Option<&str>,
inherited_lower: Option<&str>,
) -> Vec<(String, String)> {
let merge = |inherited: Option<&str>| match inherited {
Some(v) if !v.is_empty() => format!("{v},{}", self.no_proxy),
_ => self.no_proxy.clone(),
};
let values = [
self.proxy_url.clone(),
self.proxy_url.clone(),
self.proxy_url.clone(),
self.proxy_url.clone(),
self.ca_pem.display().to_string(),
merge(inherited_upper),
merge(inherited_lower),
];
WRAPPER_VARS
.iter()
.zip(values)
.map(|(name, value)| ((*name).to_string(), value))
.collect()
}
fn is_exactly_in(&self, env: &[(String, String)]) -> bool {
env_value(env, "NODE_EXTRA_CA_CERTS") == Some(self.ca_pem.display().to_string().as_str())
&& env_value(env, "HTTPS_PROXY") == Some(self.proxy_url.as_str())
}
}
#[cfg(feature = "model-relay")]
fn this_install_ca_pem() -> PathBuf {
let ol = crate::config::openlatch_dir();
crate::model_relay::ca::ca_pem_path(&crate::model_relay::ca::ca_dir(&ol))
}
#[cfg(feature = "model-relay")]
fn env_value<'a>(env: &'a [(String, String)], key: &str) -> Option<&'a str> {
env.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
}
#[cfg(feature = "model-relay")]
fn env_https_proxy(env: &[(String, String)]) -> Option<&str> {
env_value(env, "HTTPS_PROXY").or_else(|| env_value(env, "https_proxy"))
}
#[cfg(feature = "model-relay")]
fn carries_our_proxy(env: &[(String, String)]) -> bool {
env_value(env, "NODE_EXTRA_CA_CERTS")
== Some(this_install_ca_pem().display().to_string().as_str())
&& env_https_proxy(env).is_some_and(|u| u.starts_with("http://127.0.0.1:"))
}
#[cfg(feature = "model-relay")]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct HubRecord {
pid: u32,
start: u64,
exe: PathBuf,
port: u16,
app_version: String,
proxy_url: String,
ca_pem: PathBuf,
}
#[cfg(feature = "model-relay")]
fn record_path() -> PathBuf {
crate::config::openlatch_dir()
.join("model-relay")
.join("cline-hub.json")
}
#[cfg(feature = "model-relay")]
fn read_record() -> Option<HubRecord> {
let raw = std::fs::read_to_string(record_path()).ok()?;
serde_json::from_str(&raw).ok()
}
#[cfg(feature = "model-relay")]
fn write_record(record: &HubRecord) -> Result<(), OlError> {
let path = record_path();
let fail = |e: String| {
OlError::new(
ERR_CLINE_APP_NOT_CAPTURED,
format!("could not write the Cline.app hub ownership record: {e}"),
)
};
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| fail(e.to_string()))?;
}
let body = serde_json::to_string_pretty(record).map_err(|e| fail(e.to_string()))?;
crate::fs_secure::write_readable(&path, &body).map_err(|e| fail(e.to_string()))
}
#[cfg(feature = "model-relay")]
pub(crate) fn remove_record() {
match std::fs::remove_file(record_path()) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => tracing::warn!(
code = ERR_CLINE_APP_NOT_CAPTURED,
error = %e,
"could not remove the Cline.app hub ownership record"
),
}
}
#[cfg(feature = "model-relay")]
pub(crate) fn forget_record_unless(state: &HubState) {
let Some(r) = read_record() else { return };
let gone = match state {
HubState::Absent => true,
HubState::Foreign { pid: 0 } => false,
HubState::Ours { pid } | HubState::Stale { pid, .. } | HubState::Foreign { pid } => {
*pid != r.pid
}
};
if gone {
remove_record();
}
}
#[cfg(feature = "model-relay")]
pub(crate) fn hub_state(port: u16) -> HubState {
hub_state_with(&*process::table(), port, detect().as_ref())
}
#[cfg(feature = "model-relay")]
fn same_exe(a: Option<&Path>, b: &Path) -> bool {
let Some(a) = a else { return false };
match (std::fs::canonicalize(a), std::fs::canonicalize(b)) {
(Ok(a), Ok(b)) => crate::path_compat::dedup_key(&a) == crate::path_compat::dedup_key(&b),
_ => false,
}
}
#[cfg(feature = "model-relay")]
fn is_record_of(rec: Option<&HubRecord>, pid: u32, info: &process::ProcInfo) -> bool {
rec.is_some_and(|r| {
r.pid == pid && same_exe(info.exe.as_deref(), &r.exe) && info.start == Some(r.start)
})
}
#[cfg(feature = "model-relay")]
pub(crate) fn hub_state_with(t: &dyn ProcessTable, port: u16, app: Option<&ClineApp>) -> HubState {
hub_state_inspected(t, port, app).0
}
#[cfg(feature = "model-relay")]
pub(crate) fn hub_state_inspected(
t: &dyn ProcessTable,
port: u16,
app: Option<&ClineApp>,
) -> (HubState, Option<process::ProcInfo>) {
hub_state_of(t, t.listener(port), app)
}
#[cfg(feature = "model-relay")]
fn hub_state_of(
t: &dyn ProcessTable,
listener: Result<Option<u32>, String>,
app: Option<&ClineApp>,
) -> (HubState, Option<process::ProcInfo>) {
let pid = match listener {
Ok(None) => return (HubState::Absent, None),
Ok(Some(p)) => p,
Err(_) => return (HubState::Foreign { pid: 0 }, None), };
let Some(info) = t.inspect(pid) else {
return (HubState::Foreign { pid }, None);
};
let state = classify(pid, &info, app);
(state, Some(info))
}
#[cfg(feature = "model-relay")]
fn classify(pid: u32, info: &process::ProcInfo, app: Option<&ClineApp>) -> HubState {
if !info.args.iter().any(|a| a.contains(HUB_ARG)) {
return HubState::Foreign { pid };
}
let rec = read_record();
let by_record = is_record_of(rec.as_ref(), pid, info);
match app {
Some(a) => {
if !same_exe(info.exe.as_deref(), &a.sidecar) {
return HubState::Foreign { pid };
}
}
None => {
if !by_record {
return HubState::Foreign { pid };
}
}
}
let by_env = info.env.as_deref().map(carries_our_proxy);
if !(by_record || by_env == Some(true)) {
return HubState::Stale {
pid,
why: StaleWhy::NoProxyEnv,
};
}
if by_record {
if let (Some(r), Some(a)) = (rec.as_ref(), app) {
if r.app_version != a.version {
return HubState::Stale {
pid,
why: StaleWhy::OlderApp {
hub: r.app_version.clone(),
installed: a.version.clone(),
},
};
}
}
}
let url = if by_record {
rec.as_ref().map(|r| r.proxy_url.clone())
} else {
info.env
.as_deref()
.and_then(env_https_proxy)
.map(str::to_string)
};
if let Some(live) = crate::hooks::proxy_env_file::live_proxy_url(ClineBinding::AGENT_TYPE) {
if url.as_deref() != Some(live.as_str()) {
return HubState::Stale {
pid,
why: StaleWhy::ProxyChanged,
};
}
}
HubState::Ours { pid }
}
#[cfg(feature = "model-relay")]
fn hub_args(port: u16) -> Vec<String> {
[
HUB_ARG,
"--cwd",
"/",
"--host",
HUB_HOST,
"--port",
&port.to_string(),
"--pathname",
"/hub",
]
.iter()
.map(|s| (*s).to_string())
.collect()
}
#[cfg(feature = "model-relay")]
fn hub_mode_env(port: u16) -> Vec<(String, String)> {
vec![
(HUB_DAEMON_ENV.to_string(), "1".to_string()),
(HUB_PORT_ENV.to_string(), port.to_string()),
(HUB_HOST_ENV.to_string(), HUB_HOST.to_string()),
]
}
#[cfg(feature = "model-relay")]
fn hub_cwd(app: &ClineApp) -> PathBuf {
if cfg!(windows) {
app.sidecar
.parent()
.map_or_else(|| PathBuf::from("."), Path::to_path_buf)
} else {
PathBuf::from("/")
}
}
#[cfg(feature = "model-relay")]
fn not_captured(message: impl Into<String>) -> OlError {
OlError::new(ERR_CLINE_APP_NOT_CAPTURED, message)
}
#[cfg(feature = "model-relay")]
pub(crate) fn start_hub(app: &ClineApp, env: &ProxyEnvVars) -> Result<u32, OlError> {
start_hub_with(&*process::table(), app, env, hub_port(), HUB_STARTUP_POLL)
}
#[cfg(feature = "model-relay")]
fn start_hub_with(
t: &dyn ProcessTable,
app: &ClineApp,
env: &ProxyEnvVars,
port: u16,
poll: Duration,
) -> Result<u32, OlError> {
if !matches!(t.listener(port), Ok(None)) {
return Err(not_captured(format!(
"port {port} is already held — the hub was not started"
)));
}
let args = hub_args(port);
let mut spawn_env = hub_mode_env(port);
spawn_env.extend(env.overrides(
std::env::var("NO_PROXY").ok().as_deref(),
std::env::var("no_proxy").ok().as_deref(),
));
let spawned = t
.spawn_detached(&app.sidecar, &args, &hub_cwd(app), &spawn_env)
.map_err(|e| not_captured(format!("the Cline.app hub could not be spawned: {e}")))?;
let claim = process::poll_until(HUB_STARTUP_POLLS + 1, poll, || match t.listener(port) {
Ok(Some(p)) if p == spawned => Some(Ok((p, t.inspect(p)))),
Ok(Some(p)) => Some(match t.inspect(p) {
Some(i)
if i.args.iter().any(|a| a.contains(HUB_ARG))
&& same_exe(i.exe.as_deref(), &app.sidecar)
&& i.env.as_deref().is_some_and(|e| env.is_exactly_in(e)) =>
{
Ok((p, Some(i)))
}
_ => Err(p),
}),
_ => None,
});
let (listener, info) = match claim {
Some(Ok(claimed)) => claimed,
Some(Err(p)) => {
let _ = process::terminate(t, spawned, None); return Err(not_captured(format!(
"startup contention: port {port} was taken by pid {p}, not the hub OpenLatch \
started"
)));
}
None => {
let _ = process::terminate(t, spawned, None);
return Err(not_captured("the hub did not listen within 8 s"));
}
};
let abandon = |info: Option<&process::ProcInfo>| {
if listener != spawned {
let id = info.map(process::ProcIdentity::of);
if let Some(id) = &id {
let _ = process::terminate(t, listener, Some(id));
}
}
let _ = process::terminate(t, spawned, None);
};
let Some(start) = info.as_ref().and_then(|i| i.start) else {
abandon(info.as_ref());
return Err(not_captured(
"the hub's start identity is unreadable — no ownership record was written",
));
};
let record = HubRecord {
pid: listener,
start,
exe: app.sidecar.clone(),
port,
app_version: app.version.clone(),
proxy_url: env.proxy_url.clone(),
ca_pem: env.ca_pem.clone(),
};
if let Err(e) = write_record(&record) {
abandon(info.as_ref());
return Err(e);
}
tracing::info!(
pid = listener,
port,
version = %app.version,
"Cline.app hub started with capture"
);
Ok(listener)
}
#[cfg(feature = "model-relay")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RestartWhen {
IfIdle,
Now,
}
#[cfg(feature = "model-relay")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum RestartOutcome {
Started {
pid: u32,
},
Restarted {
old: u32,
new: Option<u32>,
},
DeferredBusy {
clients: usize,
},
IdleUnknown(String),
LeftForeign {
pid: u32,
},
NothingToDo,
}
#[cfg(feature = "model-relay")]
const HUB_IDLE_BY_CONNECTIONS: bool = true;
#[cfg(feature = "model-relay")]
pub(crate) const IDLE_RECHECK: Duration = Duration::from_secs(2);
#[cfg(feature = "model-relay")]
pub(crate) fn restart_hub(
app: &ClineApp,
env: Option<&ProxyEnvVars>,
when: RestartWhen,
) -> Result<RestartOutcome, OlError> {
restart_hub_with(
&*process::table(),
app,
env,
when,
hub_port(),
IDLE_RECHECK,
HUB_STARTUP_POLL,
)
}
#[cfg(feature = "model-relay")]
pub(crate) fn restart_hub_with(
t: &dyn ProcessTable,
app: &ClineApp,
env: Option<&ProxyEnvVars>,
when: RestartWhen,
port: u16,
recheck: Duration,
poll: Duration,
) -> Result<RestartOutcome, OlError> {
let (listener, clients) = t.sockets(port);
let (state, info) = hub_state_of(t, listener, Some(app));
let pid = match state {
HubState::Absent => {
return match env {
Some(e) => {
start_hub_with(t, app, e, port, poll).map(|pid| RestartOutcome::Started { pid })
}
None => Ok(RestartOutcome::NothingToDo),
}
}
HubState::Foreign { pid } => return Ok(RestartOutcome::LeftForeign { pid }),
HubState::Ours { pid } | HubState::Stale { pid, .. } => pid,
};
let Some(identity) = info.as_ref().map(process::ProcIdentity::of) else {
return Ok(RestartOutcome::LeftForeign { pid });
};
let attached = match idle(t, port, pid, recheck, clients) {
Idle::Idle => 0,
Idle::Busy(n) if when == RestartWhen::IfIdle => {
return Ok(RestartOutcome::DeferredBusy { clients: n })
}
Idle::Busy(n) => n,
Idle::Unknown(why) if when == RestartWhen::IfIdle => {
return Ok(RestartOutcome::IdleUnknown(why))
}
Idle::Unknown(_) => 1,
};
process::terminate(t, pid, Some(&identity)).map_err(not_captured)?;
let _ = process::poll_until(HUB_RELEASE_POLLS, poll, || {
matches!(t.listener(port), Ok(None)).then_some(())
});
remove_record();
let new = match env {
Some(e) => Some(start_hub_with(t, app, e, port, poll)?),
None if attached > 0 => t
.spawn_detached(
&app.sidecar,
&hub_args(port),
&hub_cwd(app),
&hub_mode_env(port),
)
.ok(),
None => None,
};
Ok(RestartOutcome::Restarted { old: pid, new })
}
#[cfg(feature = "model-relay")]
#[allow(clippy::enum_variant_names)] enum Idle {
Idle,
Busy(usize),
Unknown(String),
}
#[cfg(feature = "model-relay")]
fn idle(
t: &dyn ProcessTable,
port: u16,
hub: u32,
recheck: Duration,
first: Result<Vec<u32>, String>,
) -> Idle {
if !HUB_IDLE_BY_CONNECTIONS {
return Idle::Unknown("the idle signal is not proven on this host (06 §0 L4)".into());
}
let busy = |seen: Result<Vec<u32>, String>| match seen {
Err(e) => Some(Idle::Unknown(e)),
Ok(pids) => {
let n = pids
.iter()
.filter(|&&x| x != hub)
.collect::<std::collections::BTreeSet<_>>()
.len();
(n > 0).then_some(Idle::Busy(n))
}
};
if let Some(verdict) = busy(first) {
return verdict;
}
std::thread::sleep(recheck);
busy(t.clients(port)).unwrap_or(Idle::Idle)
}
#[cfg(feature = "model-relay")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum HubFinding {
Captured {
pid: u32,
},
NotLive,
NoHub,
WaitingForRestart {
pid: u32,
},
OlderApp {
pid: u32,
hub: String,
installed: String,
},
Foreign {
pid: u32,
port: u16,
},
NotDetectedOnPlatform,
}
#[cfg(feature = "model-relay")]
pub(crate) fn hub_finding(live: bool) -> Option<HubFinding> {
let Some(app) = visible_app()? else {
return Some(HubFinding::NotDetectedOnPlatform); };
let port = hub_port();
let t = process::table();
let state = hub_state_with(&*t, port, Some(&app));
Some(finding_of(&*t, state, port, live))
}
#[cfg(feature = "model-relay")]
pub(crate) fn hub_visible() -> bool {
visible_app().is_some()
}
#[cfg(feature = "model-relay")]
fn visible_app() -> Option<Option<ClineApp>> {
if not_detectable_here() {
return Some(None);
}
detect().map(Some)
}
#[cfg(feature = "model-relay")]
fn finding_of(t: &dyn ProcessTable, state: HubState, port: u16, live: bool) -> HubFinding {
match (state, live) {
(HubState::Ours { pid }, _) => HubFinding::Captured { pid },
(HubState::Foreign { pid }, _) if captured_off_path(t, pid) => HubFinding::Captured { pid },
(
HubState::Stale {
pid,
why: StaleWhy::OlderApp { hub, installed },
},
_,
) => HubFinding::OlderApp {
pid,
hub,
installed,
},
(_, false) => HubFinding::NotLive,
(HubState::Absent, true) => HubFinding::NoHub,
(HubState::Stale { pid, .. }, true) => HubFinding::WaitingForRestart { pid },
(HubState::Foreign { pid }, true) => HubFinding::Foreign { pid, port },
}
}
#[cfg(feature = "model-relay")]
fn captured_off_path(t: &dyn ProcessTable, pid: u32) -> bool {
pid != 0
&& t.inspect(pid).is_some_and(|i| {
i.args.iter().any(|a| a.contains(HUB_ARG))
&& i.env.as_deref().is_some_and(|e| {
carries_our_proxy(e)
&& crate::hooks::proxy_env_file::live_proxy_url(ClineBinding::AGENT_TYPE)
.is_some_and(|u| env_value(e, "HTTPS_PROXY") == Some(u.as_str()))
})
})
}
#[cfg(test)]
pub(crate) fn write_fake_bundle(apps: &Path, id: &str, version: &str) -> PathBuf {
let bundle = apps.join("Cline.app");
let contents = bundle.join("Contents");
std::fs::create_dir_all(contents.join("MacOS")).expect("create the fake bundle");
std::fs::write(
contents.join("Info.plist"),
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \
\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
<plist version=\"1.0\">\n<dict>\n\
\t<key>CFBundleExecutable</key>\n\t<string>cline</string>\n\
\t<key>CFBundleIdentifier</key>\n\t<string>{id}</string>\n\
\t<key>CFBundleShortVersionString</key>\n\t<string>{version}</string>\n\
</dict>\n</plist>\n"
),
)
.expect("write the fake Info.plist");
std::fs::write(contents.join("MacOS").join("code-sidecar"), "").expect("write the sidecar");
bundle
}
#[cfg(all(test, feature = "model-relay"))]
pub(crate) mod test_fixture {
use std::path::PathBuf;
use std::sync::Arc;
use super::process::test_support::FakeTable;
use super::process::{install_for_tests, ProcInfo, TableGuard};
use super::{hub_args, ClineApp, APP_DIR_ENV, HUB_PORT_ENV};
pub(crate) struct HubFx {
pub(crate) table: Arc<FakeTable>,
_guard: TableGuard,
_seam: crate::hooks::cline::ClineSeam,
_state: std::sync::MutexGuard<'static, ()>,
pub(crate) root: tempfile::TempDir,
pub(crate) app: ClineApp,
pub(crate) port: u16,
}
fn free_port() -> u16 {
std::net::TcpListener::bind("127.0.0.1:0")
.and_then(|l| l.local_addr())
.map(|a| a.port())
.expect("bind an ephemeral loopback port")
}
pub(crate) fn hub_fx() -> HubFx {
hub_fx_at("0.0.34")
}
pub(crate) fn hub_fx_at(version: &str) -> HubFx {
let state = crate::config::OPENLATCH_DIR_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let root = tempfile::tempdir().expect("tempdir");
let ol = root.path().join("openlatch");
std::fs::create_dir_all(&ol).expect("create the isolated OPENLATCH_DIR");
let apps = root.path().join("apps");
super::write_fake_bundle(&apps, super::BUNDLE_ID, version);
let port = free_port();
assert_ne!(port, super::DEFAULT_HUB_PORT);
let seam = crate::hooks::cline::cline_isolated([
("OPENLATCH_DIR", Some(ol.into_os_string())),
(
crate::hooks::cline::STORE_DIR_ENV,
Some(root.path().join("store").into_os_string()),
),
(
crate::hooks::cline::DATA_DIR_ENV,
Some(root.path().join("data").into_os_string()),
),
(
crate::hooks::cline::ASSETS_DIR_ENV,
Some(root.path().join("assets").into_os_string()),
),
(APP_DIR_ENV, Some(apps.into_os_string())),
(HUB_PORT_ENV, Some(port.to_string().into())),
]);
let app = super::detect().expect("the fake bundle is detected");
let table = Arc::new(FakeTable::new(port));
let guard = install_for_tests(table.clone());
HubFx {
table,
_guard: guard,
_seam: seam,
_state: state,
root,
app,
port,
}
}
impl HubFx {
pub(crate) fn hub_info(&self, env: Option<Vec<(String, String)>>) -> ProcInfo {
ProcInfo {
exe: Some(self.app.sidecar.clone()),
args: hub_args(self.port),
env,
start: Some(1),
}
}
pub(crate) fn other_exe(&self, rel: &str) -> PathBuf {
let p = self.root.path().join(rel);
std::fs::create_dir_all(p.parent().expect("a parent")).expect("mkdir");
std::fs::write(&p, "").expect("write the other executable");
p
}
pub(crate) fn fake(&self) -> FakeTable {
FakeTable::new(self.port)
}
}
pub(crate) fn our_env(proxy: &str) -> Vec<(String, String)> {
vec![
(
"NODE_EXTRA_CA_CERTS".to_string(),
super::this_install_ca_pem().display().to_string(),
),
("HTTPS_PROXY".to_string(), proxy.to_string()),
("LANG".to_string(), "C".to_string()),
]
}
pub(crate) fn clean_env() -> Vec<(String, String)> {
vec![("LANG".to_string(), "C".to_string())]
}
pub(crate) fn seed_record(fx: &HubFx, pid: u32, start: u64, version: &str, proxy_url: &str) {
super::write_record(&super::HubRecord {
pid,
start,
exe: fx.app.sidecar.clone(),
port: fx.port,
app_version: version.to_string(),
proxy_url: proxy_url.to_string(),
ca_pem: super::this_install_ca_pem(),
})
.expect("seed the record");
}
pub(crate) fn record_exists() -> bool {
super::record_path().exists()
}
pub(crate) fn record_pid() -> Option<u32> {
super::read_record().map(|r| r.pid)
}
pub(crate) fn listen(t: &FakeTable, pid: u32, info: ProcInfo) {
*t.listener.lock().unwrap() = Ok(Some(pid));
t.procs.lock().unwrap().insert(pid, info);
}
pub(crate) fn wrapper_at(url: &str, live: bool) {
let ca = super::this_install_ca_pem();
crate::hooks::proxy_env_file::write_block(
"cline",
&["cline"],
url,
&ca,
&crate::hooks::proxy_env_file::relay_no_proxy(7600),
)
.expect("write Cline's wrapper block");
crate::hooks::proxy_env_file::set_live(live).expect("set the live marker");
}
}
#[cfg(test)]
mod tests {
use super::*;
const PLIST: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<plist version=\"1.0\">\n\
<dict>\n\t<key>CFBundleIdentifier</key>\n\t<string>bot.cline.app</string>\n\
\t<key>CFBundleShortVersionString</key>\n\t<string>0.0.34</string>\n\
\t<key>CFBundleName</key>\n\t<string>Cline & Co</string>\n\
</dict>\n</plist>\n";
#[test]
fn plist_string_reads_identifier_and_version() {
assert_eq!(
plist_string(PLIST, "CFBundleIdentifier").as_deref(),
Some("bot.cline.app")
);
assert_eq!(
plist_string(PLIST, "CFBundleShortVersionString").as_deref(),
Some("0.0.34")
);
assert_eq!(plist_string(PLIST, "CFBundleMissing"), None);
let binary = format!("bplist00{PLIST}");
assert_eq!(plist_string(&binary, "CFBundleIdentifier"), None);
assert_eq!(
plist_string(PLIST, "CFBundleName").as_deref(),
Some("Cline & Co")
);
}
#[test]
fn detect_in_needs_the_identifier_and_the_sidecar() {
let root = tempfile::tempdir().expect("tempdir");
let bundle = write_fake_bundle(&root.path().join("a"), BUNDLE_ID, "0.0.34");
let app = detect_in(std::slice::from_ref(&bundle)).expect("the fake bundle is Cline's");
assert_eq!(app.version, "0.0.34");
assert_eq!(
app.sidecar,
bundle.join("Contents").join("MacOS").join("code-sidecar")
);
let other = write_fake_bundle(&root.path().join("b"), "com.other.app", "0.0.34");
assert_eq!(detect_in(&[other]), None, "another identifier");
std::fs::remove_file(bundle.join("Contents").join("MacOS").join("code-sidecar"))
.expect("delete the sidecar");
assert_eq!(detect_in(&[bundle]), None, "no sidecar");
}
#[test]
fn detect_in_takes_the_first_matching_root() {
let root = tempfile::tempdir().expect("tempdir");
let first = write_fake_bundle(&root.path().join("a"), BUNDLE_ID, "0.0.1");
let second = write_fake_bundle(&root.path().join("b"), BUNDLE_ID, "0.0.2");
let found = detect_in(&[first.clone(), second.clone()]).expect("a match");
assert_eq!(found.bundle, first);
let invalid = root.path().join("missing").join("Cline.app");
let found = detect_in(&[invalid, second.clone()]).expect("a match");
assert_eq!(found.bundle, second);
}
#[test]
fn production_roots_are_macos_only() {
let home = Path::new("/home-for-test");
if cfg!(target_os = "macos") {
assert_eq!(
production_roots(Some(home)),
vec![
PathBuf::from("/Applications/Cline.app"),
home.join("Applications").join("Cline.app"),
]
);
assert_eq!(
production_roots(None),
vec![PathBuf::from("/Applications/Cline.app")]
);
} else {
assert!(production_roots(Some(home)).is_empty());
assert!(production_roots(None).is_empty());
}
}
#[test]
fn app_dir_env_replaces_the_default_roots() {
let root = tempfile::tempdir().expect("tempdir");
let apps = root.path().join("apps");
write_fake_bundle(&apps, BUNDLE_ID, "0.0.34");
let _seam = crate::hooks::cline::cline_isolated([
(
crate::hooks::cline::STORE_DIR_ENV,
Some(root.path().join("store").into_os_string()),
),
(
crate::hooks::cline::DATA_DIR_ENV,
Some(root.path().join("data").into_os_string()),
),
(
crate::hooks::cline::ASSETS_DIR_ENV,
Some(root.path().join("assets").into_os_string()),
),
(APP_DIR_ENV, Some(apps.clone().into_os_string())),
]);
let app = detect().expect("APP_DIR_ENV names the fake bundle's parent");
assert_eq!(app.bundle, apps.join("Cline.app"));
let _relative =
crate::hooks::cline::EnvOverride::apply([(APP_DIR_ENV, Some("apps".into()))]);
assert_eq!(detect(), None, "a relative root is not a location");
}
#[test]
fn cline_isolated_hides_the_real_app() {
let prior = {
let _lock = crate::hooks::cline::SEAM_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
std::env::var_os(APP_DIR_ENV)
};
let root = tempfile::tempdir().expect("tempdir");
{
let _seam = crate::hooks::cline::cline_isolated([
(
crate::hooks::cline::STORE_DIR_ENV,
Some(root.path().join("store").into_os_string()),
),
(
crate::hooks::cline::DATA_DIR_ENV,
Some(root.path().join("data").into_os_string()),
),
(
crate::hooks::cline::ASSETS_DIR_ENV,
Some(root.path().join("assets").into_os_string()),
),
]);
let set = std::env::var_os(APP_DIR_ENV).expect("the seam default is applied");
assert!(
Path::new(&set).ends_with(".openlatch-test-no-cline-app"),
"{set:?}"
);
assert_eq!(detect(), None);
}
let _lock = crate::hooks::cline::SEAM_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
assert_eq!(std::env::var_os(APP_DIR_ENV), prior);
}
#[test]
fn not_detectable_only_without_paths_or_seam() {
assert!(not_detectable_with(None, &[]));
assert!(!not_detectable_with(Some(OsStr::new("/x")), &[]));
assert!(!not_detectable_with(
None,
&[PathBuf::from("/Applications/Cline.app")]
));
}
#[cfg(feature = "model-relay")]
use super::process::test_support::FakeTable;
#[cfg(feature = "model-relay")]
use super::test_fixture::{
clean_env, hub_fx, hub_fx_at, listen, our_env, record_exists, record_pid, seed_record,
wrapper_at,
};
#[cfg(feature = "model-relay")]
use crate::hooks::cline::EnvOverride;
#[cfg(feature = "model-relay")]
const RELAY: &str = "http://127.0.0.1:7600";
#[cfg(feature = "model-relay")]
fn spawns(t: &FakeTable) -> Vec<process::test_support::Spawn> {
t.spawns.lock().unwrap().clone()
}
#[cfg(feature = "model-relay")]
fn terminated(t: &FakeTable) -> Vec<u32> {
t.terminated.lock().unwrap().clone()
}
#[cfg(feature = "model-relay")]
fn names(env: &[(String, String)]) -> Vec<&str> {
env.iter().map(|(k, _)| k.as_str()).collect()
}
#[cfg(feature = "model-relay")]
fn stale() -> HubState {
HubState::Stale {
pid: 500,
why: StaleWhy::NoProxyEnv,
}
}
#[cfg(feature = "model-relay")]
#[test]
fn hub_port_reads_cline_hub_port() {
let _fx = hub_fx();
for (value, want) in [
(Some("34567"), 34567),
(None, DEFAULT_HUB_PORT),
(Some("abc"), DEFAULT_HUB_PORT),
(Some("0"), DEFAULT_HUB_PORT),
] {
let _v = EnvOverride::apply([(HUB_PORT_ENV, value.map(Into::into))]);
assert_eq!(hub_port(), want, "{value:?}");
}
}
#[cfg(feature = "model-relay")]
#[test]
fn hub_state_absent_when_nothing_listens() {
let fx = hub_fx();
let t = fx.fake();
assert_eq!(hub_state_with(&t, fx.port, Some(&fx.app)), HubState::Absent);
}
#[cfg(feature = "model-relay")]
#[test]
fn hub_state_foreign_when_the_listener_is_not_a_hub() {
let fx = hub_fx();
let t = fx.fake();
let mut info = fx.hub_info(Some(clean_env()));
info.args = vec!["--serve".to_string()];
listen(&t, 500, info);
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Foreign { pid: 500 }
);
}
#[cfg(feature = "model-relay")]
#[test]
fn hub_state_foreign_zero_when_the_table_errs() {
let fx = hub_fx();
let t = fx.fake();
*t.listener.lock().unwrap() = Err("lsof failed".into());
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Foreign { pid: 0 }
);
}
#[cfg(feature = "model-relay")]
#[test]
fn hub_state_ours_by_record() {
let fx = hub_fx();
let t = fx.fake();
seed_record(&fx, 500, 1, "0.0.34", RELAY);
listen(&t, 500, fx.hub_info(Some(clean_env())));
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Ours { pid: 500 }
);
}
#[cfg(feature = "model-relay")]
#[test]
fn hub_state_reused_pid_is_not_ours() {
let fx = hub_fx();
seed_record(&fx, 500, 111, "0.0.34", RELAY);
for (start, want) in [
(Some(222), stale()),
(None, stale()),
(Some(111), HubState::Ours { pid: 500 }),
] {
let t = fx.fake();
let mut info = fx.hub_info(Some(clean_env()));
info.start = start;
listen(&t, 500, info);
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
want,
"start {start:?}"
);
}
}
#[cfg(feature = "model-relay")]
#[test]
fn hub_state_ours_by_environment() {
let fx = hub_fx();
let t = fx.fake();
listen(&t, 500, fx.hub_info(Some(our_env(RELAY))));
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Ours { pid: 500 }
);
let t = fx.fake();
let mut env = our_env(RELAY);
env[0].1 = "/elsewhere/ca.pem".to_string();
listen(&t, 500, fx.hub_info(Some(env)));
assert_eq!(hub_state_with(&t, fx.port, Some(&fx.app)), stale());
}
#[cfg(feature = "model-relay")]
#[test]
fn hub_state_unreadable_environment_is_stale() {
let fx = hub_fx();
let t = fx.fake();
listen(&t, 500, fx.hub_info(None));
assert_eq!(hub_state_with(&t, fx.port, Some(&fx.app)), stale());
}
#[cfg(feature = "model-relay")]
#[test]
fn hub_state_older_app_is_stale() {
let fx = hub_fx_at("0.0.34");
let t = fx.fake();
listen(&t, 500, fx.hub_info(Some(clean_env())));
seed_record(&fx, 500, 1, "0.0.33", RELAY);
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Stale {
pid: 500,
why: StaleWhy::OlderApp {
hub: "0.0.33".into(),
installed: "0.0.34".into()
}
}
);
seed_record(&fx, 500, 1, "0.0.34", RELAY);
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Ours { pid: 500 }
);
}
#[cfg(feature = "model-relay")]
#[test]
fn hub_state_moved_relay_is_stale() {
let fx = hub_fx();
let t = fx.fake();
listen(&t, 500, fx.hub_info(Some(clean_env())));
seed_record(&fx, 500, 1, "0.0.34", RELAY);
wrapper_at("http://127.0.0.1:7601", true);
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Stale {
pid: 500,
why: StaleWhy::ProxyChanged
}
);
wrapper_at(RELAY, true);
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Ours { pid: 500 }
);
}
#[cfg(feature = "model-relay")]
#[test]
fn start_hub_passes_exactly_the_wrapper_variables() {
let fx = hub_fx();
let t = fx.fake();
*t.on_spawn_listen.lock().unwrap() = Some(900);
t.procs
.lock()
.unwrap()
.insert(900, fx.hub_info(Some(clean_env())));
let env = ProxyEnvVars::for_relay(7600);
assert_eq!(
start_hub_with(&t, &fx.app, &env, fx.port, Duration::ZERO).expect("started"),
900
);
let s = spawns(&t);
assert_eq!(s.len(), 1, "one spawn");
let (exe, args, _cwd, spawn_env) = &s[0];
assert_eq!(exe, &fx.app.sidecar);
assert_eq!(args, &hub_args(fx.port));
assert_eq!(args.len(), 9);
assert_eq!(&spawn_env[..3], hub_mode_env(fx.port).as_slice());
assert_eq!(names(&spawn_env[3..]), WRAPPER_VARS.to_vec());
for (k, v) in &spawn_env[3..7] {
assert_eq!(v, RELAY, "{k}");
}
assert_eq!(spawn_env[7].1, env.ca_pem.display().to_string());
assert!(spawn_env[8].1.ends_with(&env.no_proxy));
let rec = read_record().expect("the record is written");
assert_eq!(rec.pid, 900);
assert_eq!(rec.start, 1);
assert_eq!(rec.exe, fx.app.sidecar);
assert_eq!(rec.proxy_url, RELAY);
}
#[cfg(feature = "model-relay")]
#[test]
fn overrides_merge_the_inherited_no_proxy() {
let env = ProxyEnvVars {
proxy_url: RELAY.to_string(),
ca_pem: PathBuf::from("/x/ca.pem"),
no_proxy: crate::hooks::proxy_env_file::relay_no_proxy(7600),
};
assert!(env.no_proxy.starts_with("127.0.0.1:7600,"));
assert!(env.no_proxy.ends_with(",127.0.0.1:7632"));
let get = |o: &[(String, String)], k: &str| {
o.iter().find(|(n, _)| n == k).map(|(_, v)| v.clone())
};
let o = env.overrides(Some("corp.internal"), None);
assert_eq!(names(&o), WRAPPER_VARS.to_vec());
assert_eq!(
get(&o, "NO_PROXY"),
Some(format!("corp.internal,{}", env.no_proxy))
);
assert_eq!(get(&o, "no_proxy"), Some(env.no_proxy.clone()));
let o = env.overrides(None, Some("lower.internal"));
assert_eq!(get(&o, "NO_PROXY"), Some(env.no_proxy.clone()));
assert_eq!(
get(&o, "no_proxy"),
Some(format!("lower.internal,{}", env.no_proxy))
);
let o = env.overrides(Some(""), Some(""));
assert_eq!(get(&o, "NO_PROXY"), Some(env.no_proxy.clone()));
assert_eq!(get(&o, "no_proxy"), Some(env.no_proxy.clone()));
}
#[cfg(feature = "model-relay")]
#[test]
fn start_hub_claims_another_listener_only_by_our_environment() {
let fx = hub_fx();
let env = ProxyEnvVars::for_relay(7600);
let exactly_ours = vec![
(
"NODE_EXTRA_CA_CERTS".to_string(),
env.ca_pem.display().to_string(),
),
("HTTPS_PROXY".to_string(), env.proxy_url.clone()),
];
let run = |listener: u32, info_env: Option<Vec<(String, String)>>| {
remove_record();
let t = fx.fake();
*t.on_spawn_listen.lock().unwrap() = Some(listener);
t.procs
.lock()
.unwrap()
.insert(listener, fx.hub_info(info_env));
let r = start_hub_with(&t, &fx.app, &env, fx.port, Duration::ZERO);
(r, terminated(&t))
};
let (r, term) = run(901, Some(exactly_ours.clone()));
assert_eq!(r.expect("claimed by environment"), 901);
assert!(term.is_empty());
assert_eq!(record_pid(), Some(901));
for info_env in [Some(clean_env()), None] {
let (r, term) = run(901, info_env.clone());
let e = r.expect_err("not ours");
assert_eq!(e.code, ERR_CLINE_APP_NOT_CAPTURED);
assert!(e.message.contains("startup contention"), "{}", e.message);
assert_eq!(term, vec![900], "our child only, never 901 ({info_env:?})");
assert!(!record_exists());
}
let (r, term) = run(900, None);
assert_eq!(r.expect("claimed by pid"), 900);
assert!(term.is_empty());
assert_eq!(record_pid(), Some(900));
}
#[cfg(feature = "model-relay")]
#[test]
fn start_hub_times_out_and_records_nothing() {
let fx = hub_fx();
let t = fx.fake();
let e = start_hub_with(
&t,
&fx.app,
&ProxyEnvVars::for_relay(7600),
fx.port,
Duration::ZERO,
)
.expect_err("nothing listened");
assert_eq!(e.code, ERR_CLINE_APP_NOT_CAPTURED);
assert_eq!(terminated(&t), vec![900]);
assert!(!record_exists());
}
#[cfg(feature = "model-relay")]
#[test]
fn wrapper_variables_match_the_env_file() {
let _fx = hub_fx();
wrapper_at(RELAY, false);
let sh = std::fs::read_to_string(crate::hooks::proxy_env_file::env_sh_path(
&crate::config::openlatch_dir(),
))
.expect("env.sh is written");
let mut in_block = false;
let mut found = std::collections::BTreeSet::new();
for line in sh.lines() {
let line = line.trim();
if line.starts_with("HTTPS_PROXY=") {
in_block = true;
}
if !in_block {
continue;
}
if line.starts_with("command ") {
break;
}
for token in line.split_whitespace() {
if let Some((name, _)) = token.split_once('=') {
if !name.is_empty()
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
{
found.insert(name.to_string());
}
}
}
}
let want: std::collections::BTreeSet<String> =
WRAPPER_VARS.iter().map(|s| (*s).to_string()).collect();
assert_eq!(found, want, "env.sh:\n{sh}");
}
#[cfg(feature = "model-relay")]
fn restart(
fx: &test_fixture::HubFx,
t: &FakeTable,
env: Option<&ProxyEnvVars>,
when: RestartWhen,
) -> Result<RestartOutcome, OlError> {
restart_hub_with(
t,
&fx.app,
env,
when,
fx.port,
Duration::ZERO,
Duration::ZERO,
)
}
#[cfg(feature = "model-relay")]
fn fake_that_starts(fx: &test_fixture::HubFx) -> FakeTable {
let t = fx.fake();
*t.on_spawn_listen.lock().unwrap() = Some(900);
t.procs
.lock()
.unwrap()
.insert(900, fx.hub_info(Some(clean_env())));
t
}
#[cfg(feature = "model-relay")]
#[test]
fn restart_if_idle_defers_while_a_client_is_attached() {
let fx = hub_fx();
let t = fx.fake();
listen(&t, 500, fx.hub_info(Some(clean_env())));
*t.clients.lock().unwrap() = Ok(vec![500, 4242]);
let env = ProxyEnvVars::for_relay(7600);
assert_eq!(
restart(&fx, &t, Some(&env), RestartWhen::IfIdle).expect("ok"),
RestartOutcome::DeferredBusy { clients: 1 }
);
assert!(terminated(&t).is_empty());
assert!(spawns(&t).is_empty());
}
#[cfg(feature = "model-relay")]
#[test]
fn restart_if_idle_restarts_an_idle_hub_with_capture() {
let fx = hub_fx();
let t = fake_that_starts(&fx);
listen(&t, 500, fx.hub_info(Some(clean_env())));
*t.clients.lock().unwrap() = Ok(vec![500]);
assert_eq!(hub_state_with(&t, fx.port, Some(&fx.app)), stale());
let env = ProxyEnvVars::for_relay(7600);
assert_eq!(
restart(&fx, &t, Some(&env), RestartWhen::IfIdle).expect("ok"),
RestartOutcome::Restarted {
old: 500,
new: Some(900)
}
);
assert_eq!(terminated(&t), vec![500]);
let s = spawns(&t);
assert_eq!(s.len(), 1);
assert_eq!(names(&s[0].3[3..]), WRAPPER_VARS.to_vec());
}
#[cfg(feature = "model-relay")]
#[test]
fn restart_now_takes_capture_off_a_busy_hub() {
let fx = hub_fx();
let t = fx.fake();
seed_record(&fx, 500, 1, "0.0.34", RELAY);
listen(&t, 500, fx.hub_info(Some(clean_env())));
*t.clients.lock().unwrap() = Ok(vec![500, 4242]);
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Ours { pid: 500 }
);
assert_eq!(
restart(&fx, &t, None, RestartWhen::Now).expect("ok"),
RestartOutcome::Restarted {
old: 500,
new: Some(900)
}
);
assert_eq!(terminated(&t), vec![500]);
let s = spawns(&t);
assert_eq!(s.len(), 1);
assert_eq!(s[0].3, hub_mode_env(fx.port));
assert!(!s[0]
.3
.iter()
.any(|(k, _)| WRAPPER_VARS.contains(&k.as_str())));
assert!(!record_exists());
}
#[cfg(feature = "model-relay")]
#[test]
fn restart_now_with_no_client_starts_nothing() {
let fx = hub_fx();
let t = fx.fake();
seed_record(&fx, 500, 1, "0.0.34", RELAY);
listen(&t, 500, fx.hub_info(Some(clean_env())));
*t.clients.lock().unwrap() = Ok(vec![500]);
assert_eq!(
restart(&fx, &t, None, RestartWhen::Now).expect("ok"),
RestartOutcome::Restarted {
old: 500,
new: None
}
);
assert_eq!(terminated(&t), vec![500]);
assert!(spawns(&t).is_empty());
}
#[cfg(feature = "model-relay")]
#[test]
fn restart_never_touches_a_foreign_hub() {
let fx = hub_fx();
let env = ProxyEnvVars::for_relay(7600);
for when in [RestartWhen::IfIdle, RestartWhen::Now] {
let t = fx.fake();
let mut info = fx.hub_info(Some(clean_env()));
info.args = vec!["node".to_string(), "server.js".to_string()];
listen(&t, 500, info);
*t.clients.lock().unwrap() = Ok(vec![500]);
assert_eq!(
restart(&fx, &t, Some(&env), when).expect("ok"),
RestartOutcome::LeftForeign { pid: 500 }
);
assert!(terminated(&t).is_empty(), "{when:?}");
assert!(spawns(&t).is_empty(), "{when:?}");
}
}
#[cfg(feature = "model-relay")]
#[test]
fn restart_never_touches_a_hub_of_another_sidecar() {
let fx = hub_fx();
let env = ProxyEnvVars::for_relay(7600);
let other = fx.other_exe("other/code-sidecar");
for when in [RestartWhen::IfIdle, RestartWhen::Now] {
let t = fx.fake();
let mut info = fx.hub_info(Some(clean_env()));
info.exe = Some(other.clone());
listen(&t, 500, info);
*t.clients.lock().unwrap() = Ok(vec![500]);
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Foreign { pid: 500 }
);
assert_eq!(
restart(&fx, &t, Some(&env), when).expect("ok"),
RestartOutcome::LeftForeign { pid: 500 }
);
assert!(terminated(&t).is_empty(), "{when:?}");
assert!(spawns(&t).is_empty(), "{when:?}");
}
let t = fake_that_starts(&fx);
let mut info = fx.hub_info(Some(clean_env()));
info.exe = Some(
fx.app
.bundle
.join("Contents")
.join("MacOS")
.join("..")
.join("MacOS")
.join("code-sidecar"),
);
listen(&t, 500, info);
*t.clients.lock().unwrap() = Ok(vec![500]);
assert_eq!(hub_state_with(&t, fx.port, Some(&fx.app)), stale());
assert_eq!(
restart(&fx, &t, Some(&env), RestartWhen::IfIdle).expect("ok"),
RestartOutcome::Restarted {
old: 500,
new: Some(900)
}
);
}
#[cfg(feature = "model-relay")]
#[test]
fn start_hub_refuses_an_unreadable_start_identity() {
let fx = hub_fx();
let env = ProxyEnvVars::for_relay(7600);
let t = fx.fake();
*t.on_spawn_listen.lock().unwrap() = Some(900);
let mut info = fx.hub_info(Some(clean_env()));
info.start = None;
t.procs.lock().unwrap().insert(900, info);
let e = start_hub_with(&t, &fx.app, &env, fx.port, Duration::ZERO)
.expect_err("no start identity");
assert_eq!(e.code, ERR_CLINE_APP_NOT_CAPTURED);
assert_eq!(terminated(&t), vec![900]);
assert!(!record_exists());
let t = fx.fake();
*t.on_spawn_listen.lock().unwrap() = Some(900);
let mut info = fx.hub_info(Some(clean_env()));
info.start = Some(7);
t.procs.lock().unwrap().insert(900, info);
assert_eq!(
start_hub_with(&t, &fx.app, &env, fx.port, Duration::ZERO).expect("started"),
900
);
assert_eq!(read_record().expect("recorded").start, 7);
}
#[cfg(feature = "model-relay")]
#[test]
fn restart_if_idle_does_nothing_when_idleness_is_unknown() {
let fx = hub_fx();
let t = fx.fake();
listen(&t, 500, fx.hub_info(Some(clean_env())));
*t.clients.lock().unwrap() = Err("lsof failed".into());
let env = ProxyEnvVars::for_relay(7600);
assert!(matches!(
restart(&fx, &t, Some(&env), RestartWhen::IfIdle).expect("ok"),
RestartOutcome::IdleUnknown(_)
));
assert!(terminated(&t).is_empty());
assert!(spawns(&t).is_empty());
}
#[cfg(feature = "model-relay")]
#[test]
fn idle_ignores_the_hubs_own_sockets() {
let fx = hub_fx();
let t = fake_that_starts(&fx);
listen(&t, 500, fx.hub_info(Some(clean_env())));
*t.clients.lock().unwrap() = Ok(vec![500, 500]);
let env = ProxyEnvVars::for_relay(7600);
assert_eq!(
restart(&fx, &t, Some(&env), RestartWhen::IfIdle).expect("ok"),
RestartOutcome::Restarted {
old: 500,
new: Some(900)
}
);
assert_eq!(terminated(&t), vec![500]);
}
#[cfg(feature = "model-relay")]
#[test]
#[should_panic(expected = "a test reached the real process table")]
fn the_real_process_table_is_unreachable_under_test() {
let _lock = process::HUB_SEAM_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let _ = process::table();
}
#[cfg(feature = "model-relay")]
#[test]
fn an_env_carrying_hub_off_the_sidecar_renders_captured_and_is_never_signalled() {
let fx = hub_fx();
let cli = fx.other_exe("cli/cline");
let env = ProxyEnvVars::for_relay(7600);
let off_path = |e: Option<Vec<(String, String)>>| process::ProcInfo {
exe: Some(cli.clone()),
args: hub_args(fx.port),
env: e,
start: Some(1),
};
wrapper_at(RELAY, true);
listen(&fx.table, 500, off_path(Some(our_env(RELAY))));
assert_eq!(
hub_state_with(&*fx.table, fx.port, Some(&fx.app)),
HubState::Foreign { pid: 500 }
);
assert_eq!(hub_finding(true), Some(HubFinding::Captured { pid: 500 }));
assert_eq!(hub_finding(false), Some(HubFinding::Captured { pid: 500 }));
for when in [RestartWhen::IfIdle, RestartWhen::Now] {
assert_eq!(
restart(&fx, &fx.table, Some(&env), when).expect("ok"),
RestartOutcome::LeftForeign { pid: 500 }
);
}
assert!(terminated(&fx.table).is_empty());
assert!(spawns(&fx.table).is_empty());
let foreign = Some(HubFinding::Foreign {
pid: 500,
port: fx.port,
});
listen(&fx.table, 500, off_path(None));
assert_eq!(hub_finding(true), foreign);
listen(&fx.table, 500, off_path(Some(our_env(RELAY))));
wrapper_at("http://127.0.0.1:7601", true);
assert_eq!(hub_finding(true), foreign);
wrapper_at(RELAY, false);
assert_eq!(hub_finding(true), foreign);
assert_eq!(hub_finding(false), Some(HubFinding::NotLive));
}
#[cfg(feature = "model-relay")]
#[test]
fn a_table_error_never_forgets_the_record() {
let fx = hub_fx();
let t = fake_that_starts(&fx);
start_hub_with(
&t,
&fx.app,
&ProxyEnvVars::for_relay(7600),
fx.port,
Duration::ZERO,
)
.expect("started");
assert_eq!(record_pid(), Some(900));
let t = fx.fake();
*t.listener.lock().unwrap() = Err("lsof failed".into());
let state = hub_state_with(&t, fx.port, Some(&fx.app));
assert_eq!(state, HubState::Foreign { pid: 0 });
forget_record_unless(&state);
assert!(record_exists(), "a failed read keeps the record");
let t = fx.fake();
listen(&t, 900, fx.hub_info(Some(clean_env())));
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Ours { pid: 900 }
);
let t = fx.fake();
let state = hub_state_with(&t, fx.port, Some(&fx.app));
assert_eq!(state, HubState::Absent);
forget_record_unless(&state);
assert!(!record_exists());
seed_record(&fx, 900, 1, "0.0.34", RELAY);
let t = fx.fake();
listen(&t, 901, fx.hub_info(Some(clean_env())));
let state = hub_state_with(&t, fx.port, Some(&fx.app));
forget_record_unless(&state);
assert!(!record_exists());
}
#[cfg(feature = "model-relay")]
#[test]
fn hub_finding_maps_every_state() {
let t = FakeTable::new(34567);
let older = HubState::Stale {
pid: 5,
why: StaleWhy::OlderApp {
hub: "0.0.33".into(),
installed: "0.0.34".into(),
},
};
let older_finding = HubFinding::OlderApp {
pid: 5,
hub: "0.0.33".into(),
installed: "0.0.34".into(),
};
let no_proxy = HubState::Stale {
pid: 5,
why: StaleWhy::NoProxyEnv,
};
let moved = HubState::Stale {
pid: 5,
why: StaleWhy::ProxyChanged,
};
let rows = [
(
HubState::Ours { pid: 5 },
true,
HubFinding::Captured { pid: 5 },
),
(
HubState::Ours { pid: 5 },
false,
HubFinding::Captured { pid: 5 },
),
(older.clone(), true, older_finding.clone()),
(older, false, older_finding),
(HubState::Absent, true, HubFinding::NoHub),
(HubState::Absent, false, HubFinding::NotLive),
(
no_proxy.clone(),
true,
HubFinding::WaitingForRestart { pid: 5 },
),
(no_proxy, false, HubFinding::NotLive),
(
moved.clone(),
true,
HubFinding::WaitingForRestart { pid: 5 },
),
(moved, false, HubFinding::NotLive),
(
HubState::Foreign { pid: 5 },
true,
HubFinding::Foreign {
pid: 5,
port: 34567,
},
),
(HubState::Foreign { pid: 5 }, false, HubFinding::NotLive),
];
for (state, live, want) in rows {
assert_eq!(
finding_of(&t, state.clone(), 34567, live),
want,
"{state:?} live={live}"
);
}
}
}
#[cfg(all(test, feature = "model-relay"))]
mod live_check_tests {
use super::test_fixture::{clean_env, hub_fx, listen};
use super::*;
#[test]
fn every_spawned_hub_runs_in_hub_daemon_mode_on_its_port() {
let fx = hub_fx();
let want = [
("CLINE_RUN_AS_HUB_DAEMON", "1".to_string()),
("CLINE_HUB_PORT", fx.port.to_string()),
("CLINE_HUB_HOST", "127.0.0.1".to_string()),
];
let pinned = |env: &[(String, String)]| {
for (k, v) in &want {
assert_eq!(
env.iter()
.find(|(n, _)| n == k)
.map(|(_, val)| val.as_str()),
Some(v.as_str()),
"{k} in {env:?}"
);
}
};
let t = fx.fake();
*t.on_spawn_listen.lock().unwrap() = Some(900);
t.procs
.lock()
.unwrap()
.insert(900, fx.hub_info(Some(clean_env())));
start_hub_with(
&t,
&fx.app,
&ProxyEnvVars::for_relay(7600),
fx.port,
Duration::ZERO,
)
.expect("started");
let s = t.spawns.lock().unwrap().clone();
pinned(&s[0].3);
assert!(s[0].1.iter().any(|a| a == HUB_ARG));
assert!(s[0]
.1
.windows(2)
.any(|w| w[0] == "--port" && w[1] == fx.port.to_string()));
let t = fx.fake();
listen(&t, 900, fx.hub_info(Some(clean_env())));
*t.clients.lock().unwrap() = Ok(vec![900, 4242]);
restart_hub_with(
&t,
&fx.app,
None,
RestartWhen::Now,
fx.port,
Duration::ZERO,
Duration::ZERO,
)
.expect("restarted");
let s = t.spawns.lock().unwrap().clone();
assert_eq!(s.len(), 1);
pinned(&s[0].3);
}
#[test]
fn start_hub_never_spawns_while_the_port_is_held_or_unknown() {
let fx = hub_fx();
for listener in [Ok(Some(500)), Err("lsof failed".to_string())] {
let t = fx.fake();
*t.listener.lock().unwrap() = listener.clone();
let e = start_hub_with(
&t,
&fx.app,
&ProxyEnvVars::for_relay(7600),
fx.port,
Duration::ZERO,
)
.expect_err("never a second hub");
assert_eq!(e.code, ERR_CLINE_APP_NOT_CAPTURED);
assert!(t.spawns.lock().unwrap().is_empty(), "{listener:?}");
assert!(t.terminated.lock().unwrap().is_empty(), "{listener:?}");
}
}
#[test]
fn a_handed_off_listener_is_recorded_by_its_own_pid() {
let fx = hub_fx();
let env = ProxyEnvVars::for_relay(7600);
let t = fx.fake();
*t.on_spawn_listen.lock().unwrap() = Some(26725);
let mut info = fx.hub_info(Some(env.overrides(None, None)));
info.start = Some(42);
t.procs.lock().unwrap().insert(26725, info);
assert_eq!(
start_hub_with(&t, &fx.app, &env, fx.port, Duration::ZERO).expect("claimed"),
26725
);
let rec = read_record().expect("recorded");
assert_eq!((rec.pid, rec.start), (26725, 42));
let t = fx.fake();
let mut info = fx.hub_info(None);
info.start = Some(42);
listen(&t, 26725, info);
assert_eq!(
hub_state_with(&t, fx.port, Some(&fx.app)),
HubState::Ours { pid: 26725 }
);
}
}
#[cfg(all(test, feature = "model-relay"))]
mod review_tests {
use std::collections::VecDeque;
use super::test_fixture::{clean_env, hub_fx, listen, record_exists};
use super::*;
#[test]
fn a_pid_reused_during_the_idle_wait_is_never_signalled() {
let fx = hub_fx();
let t = fx.fake();
let hub = fx.hub_info(Some(clean_env()));
listen(&t, 500, hub.clone());
*t.clients.lock().unwrap() = Ok(vec![500]);
let mut reused = hub.clone();
reused.exe = Some(fx.other_exe("other/editor"));
reused.start = Some(99);
t.inspect_script
.lock()
.unwrap()
.insert(500, VecDeque::from([Some(hub), Some(reused)]));
let env = ProxyEnvVars::for_relay(7600);
let _ = restart_hub_with(
&t,
&fx.app,
Some(&env),
RestartWhen::IfIdle,
fx.port,
Duration::ZERO,
Duration::ZERO,
);
assert!(t.terminated.lock().unwrap().is_empty(), "no SIGTERM");
assert!(t.killed.lock().unwrap().is_empty(), "no SIGKILL");
assert!(
t.spawns.lock().unwrap().is_empty(),
"the port is still held"
);
}
#[test]
fn a_release_never_signals_a_reused_pid() {
let fx = hub_fx();
let t = fx.fake();
super::test_fixture::seed_record(&fx, 500, 1, "0.0.34", "http://127.0.0.1:7600");
let hub = fx.hub_info(Some(clean_env()));
listen(&t, 500, hub.clone());
t.inspect_script
.lock()
.unwrap()
.insert(500, VecDeque::from([Some(hub), None]));
let (state, info) = hub_state_inspected(&t, fx.port, None);
assert_eq!(state, HubState::Ours { pid: 500 });
let id = process::ProcIdentity::of(info.as_ref().expect("inspected"));
process::terminate(&t, 500, Some(&id)).expect("gone is Ok");
assert!(t.terminated.lock().unwrap().is_empty());
}
#[test]
fn a_claimed_listener_with_an_unreadable_start_is_stopped() {
let fx = hub_fx();
let env = ProxyEnvVars::for_relay(7600);
let t = fx.fake();
*t.on_spawn_listen.lock().unwrap() = Some(26725);
let mut info = fx.hub_info(Some(env.overrides(None, None)));
info.start = None;
t.procs.lock().unwrap().insert(26725, info);
let e = start_hub_with(&t, &fx.app, &env, fx.port, Duration::ZERO)
.expect_err("no start identity");
assert_eq!(e.code, ERR_CLINE_APP_NOT_CAPTURED);
assert_eq!(*t.terminated.lock().unwrap(), vec![26725, 900]);
assert!(!record_exists());
}
#[test]
fn a_claimed_listener_is_stopped_when_the_record_cannot_be_written() {
let fx = hub_fx();
let env = ProxyEnvVars::for_relay(7600);
std::fs::write(crate::config::openlatch_dir().join("model-relay"), "")
.expect("block the record directory");
let t = fx.fake();
*t.on_spawn_listen.lock().unwrap() = Some(26725);
let mut info = fx.hub_info(Some(env.overrides(None, None)));
info.start = Some(42);
t.procs.lock().unwrap().insert(26725, info);
let e = start_hub_with(&t, &fx.app, &env, fx.port, Duration::ZERO)
.expect_err("the record cannot be written");
assert_eq!(e.code, ERR_CLINE_APP_NOT_CAPTURED);
assert_eq!(*t.terminated.lock().unwrap(), vec![26725, 900]);
}
}