use std::net::{TcpStream, ToSocketAddrs};
use std::path::{Path, PathBuf};
use std::time::Duration;
const REACHABILITY_TIMEOUT: Duration = Duration::from_millis(600);
pub fn channels_enabled_in(parent_cmdline: &[String]) -> bool {
let joined = parent_cmdline.join(" ");
joined.contains("--dangerously-load-development-channels") && joined.contains("marshal")
}
pub fn detect() -> Option<bool> {
let cmd = parent_cmdline();
if cmd.is_empty() {
return None;
}
Some(channels_enabled_in(&cmd))
}
pub fn cannot_receive() -> bool {
match daemon_address() {
Some(addr) => !is_reachable(&addr),
None => false,
}
}
fn is_reachable(addr: &str) -> bool {
let Some((host, port)) = parse_host_port(addr) else {
return true;
};
match (host.as_str(), port).to_socket_addrs() {
Ok(socket_addrs) => {
let mut any = false;
for sa in socket_addrs {
any = true;
if TcpStream::connect_timeout(&sa, REACHABILITY_TIMEOUT).is_ok() {
return true;
}
}
!any
}
Err(_) => true,
}
}
fn parse_host_port(addr: &str) -> Option<(String, u16)> {
let no_scheme = addr
.strip_prefix("ws://")
.or_else(|| addr.strip_prefix("wss://"))
.unwrap_or(addr);
let authority = no_scheme.split(['/', '?', '#']).next().unwrap_or(no_scheme);
let (host, port) = authority.rsplit_once(':')?;
if host.is_empty() {
return None;
}
Some((host.to_string(), port.parse().ok()?))
}
fn daemon_address() -> Option<String> {
for var in ["MARSHAL_DAEMON_ADDRESS", "MYKO_ADDRESS"] {
if let Ok(v) = std::env::var(var) {
let v = v.trim();
if !v.is_empty() {
return Some(v.to_string());
}
}
}
let home = home_dir()?;
let path = config_dir(&home).join("daemon-address");
let contents = std::fs::read_to_string(path).ok()?;
let line = contents.lines().next()?.trim();
if line.is_empty() {
None
} else {
Some(line.to_string())
}
}
fn config_dir(home: &Path) -> PathBuf {
#[cfg(windows)]
{
if let Some(appdata) = std::env::var_os("APPDATA").filter(|s| !s.is_empty()) {
return PathBuf::from(appdata).join("marshal");
}
}
#[cfg(unix)]
{
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME").filter(|s| !s.is_empty()) {
return PathBuf::from(xdg).join("marshal");
}
}
home.join(".config").join("marshal")
}
#[cfg(unix)]
fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from)
}
#[cfg(windows)]
fn home_dir() -> Option<PathBuf> {
if let Some(p) = std::env::var_os("USERPROFILE") {
return Some(PathBuf::from(p));
}
std::env::var_os("HOME").map(PathBuf::from)
}
#[cfg(not(any(unix, windows)))]
fn home_dir() -> Option<PathBuf> {
None
}
pub fn parent_cmdline() -> Vec<String> {
#[cfg(target_os = "linux")]
{
let ppid = unsafe { libc::getppid() } as u32;
if let Ok(raw) = std::fs::read(format!("/proc/{ppid}/cmdline")) {
return raw
.split(|&b| b == 0)
.filter(|s| !s.is_empty())
.map(|s| String::from_utf8_lossy(s).into_owned())
.collect();
}
Vec::new()
}
#[cfg(windows)]
{
windows_parent_cmdline().unwrap_or_default()
}
#[cfg(not(any(target_os = "linux", windows)))]
{
Vec::new()
}
}
#[cfg(windows)]
fn windows_parent_cmdline() -> Option<Vec<String>> {
use std::os::windows::process::CommandExt;
let ppid = super::session_discovery::windows_parent_pid()?;
let out = std::process::Command::new("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-Command",
&format!("(Get-CimInstance Win32_Process -Filter \"ProcessId={ppid}\").CommandLine"),
])
.creation_flags(0x08000000)
.output()
.ok()?;
if !out.status.success() {
return None;
}
let line = String::from_utf8_lossy(&out.stdout).trim().to_string();
if line.is_empty() {
return None;
}
Some(vec![line])
}
#[cfg(test)]
mod tests {
use super::{channels_enabled_in, parse_host_port};
fn v(s: &[&str]) -> Vec<String> {
s.iter().map(|x| x.to_string()).collect()
}
#[test]
fn flag_with_server_marshal_is_enabled() {
assert!(channels_enabled_in(&v(&[
"claude",
"--dangerously-load-development-channels",
"server:marshal",
])));
}
#[test]
fn flag_with_plugin_marshal_is_enabled() {
assert!(channels_enabled_in(&v(&[
"claude",
"--dangerously-load-development-channels",
"plugin:marshal-shim@marshal",
])));
}
#[test]
fn bare_claude_is_disabled() {
assert!(!channels_enabled_in(&v(&["claude"])));
}
#[test]
fn flag_for_a_different_server_is_disabled() {
assert!(!channels_enabled_in(&v(&[
"claude",
"--dangerously-load-development-channels",
"server:something-else",
])));
}
#[test]
fn parses_ws_url_with_port() {
assert_eq!(
parse_host_port("ws://100.86.142.61:6155"),
Some(("100.86.142.61".to_string(), 6155))
);
}
#[test]
fn parses_bare_host_port_and_strips_path() {
assert_eq!(
parse_host_port("marshal-01.lucid.host:6156/hook"),
Some(("marshal-01.lucid.host".to_string(), 6156))
);
}
#[test]
fn no_port_is_none() {
assert_eq!(parse_host_port("ws://justahost"), None);
}
}