use std::ffi::{OsStr, OsString};
use std::fs::{self, OpenOptions};
use std::io::Write;
#[cfg(unix)]
use std::os::unix::ffi::{OsStrExt, OsStringExt};
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
const CHILD_ENV: &str = "SUPERCODE_TMUX_CHILD";
const TMUX_ENV: &str = "SUPERCODE_TMUX_SESSION";
#[derive(Serialize, Deserialize)]
struct LaunchSpec {
executable: Vec<u8>,
arguments: Vec<Vec<u8>>,
environment: Vec<(Vec<u8>, Vec<u8>)>,
}
pub(crate) fn is_child() -> bool {
std::env::var_os(CHILD_ENV).is_some()
}
pub(crate) fn available() -> bool {
cfg!(unix)
&& Command::new("tmux")
.arg("-V")
.output()
.is_ok_and(|output| output.status.success())
}
pub(crate) fn predictable_name(kind: &str, identity: &str) -> String {
let slug = identity
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() {
character.to_ascii_lowercase()
} else {
'-'
}
})
.collect::<String>()
.split('-')
.filter(|part| !part.is_empty())
.take(4)
.collect::<Vec<_>>()
.join("-");
let slug = if slug.is_empty() { "session" } else { &slug };
format!(
"supercode-{kind}-{}-{:08x}",
slug.chars().take(28).collect::<String>(),
stable_hash(identity) as u32
)
}
pub(crate) fn session_exists(name: &str) -> bool {
Command::new("tmux")
.args(["has-session", "-t", name])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
pub(crate) fn registered_runtime(name: &str) -> Result<Option<supercode::LiveRuntimeRecord>> {
Ok(supercode::list_live_runtimes()?.into_iter().find(|record| {
matches!(
&record.metadata.supervisor,
Some(supercode::LiveRuntimeSupervisor::Tmux { session_name })
if session_name == name
)
}))
}
pub(crate) fn reconcile_stale(name: &str) -> Result<()> {
if session_exists(name) && registered_runtime(name)?.is_none() {
let status = Command::new("tmux")
.args(["kill-session", "-t", name])
.status()
.context("removing stale tmux supervisor")?;
if !status.success() {
bail!("could not remove stale tmux supervisor `{name}`");
}
}
Ok(())
}
pub(crate) async fn launch(
tmux_name: &str,
api_key: Option<&str>,
server_token: Option<&str>,
) -> Result<supercode::LiveRuntimeRecord> {
if session_exists(tmux_name) {
bail!("tmux session `{tmux_name}` already exists without a matching live runtime");
}
let executable = std::env::current_exe().context("resolving the supercode executable")?;
let arguments = sanitized_arguments();
let mut environment = captured_environment();
environment.extend([
(CHILD_ENV.as_bytes().to_vec(), b"1".to_vec()),
(TMUX_ENV.as_bytes().to_vec(), tmux_name.as_bytes().to_vec()),
]);
if let Some(value) = api_key {
environment.push((
b"SUPERCODE_SUPERVISED_API_KEY".to_vec(),
value.as_bytes().to_vec(),
));
}
if let Some(value) = server_token {
environment.push((
b"SUPERCODE_SERVER_TOKEN".to_vec(),
value.as_bytes().to_vec(),
));
}
let spec_path = write_spec(&LaunchSpec {
executable: os_bytes(executable.as_os_str()),
arguments: arguments.iter().map(|value| os_bytes(value)).collect(),
environment,
})?;
let status = Command::new("tmux")
.args(["new-session", "-d", "-s", tmux_name, "-n", "runtime"])
.arg(&executable)
.arg("__supervised-child")
.arg(&spec_path)
.status()
.context("starting tmux supervisor")?;
if !status.success() {
let _ = fs::remove_file(&spec_path);
bail!("tmux could not start `{tmux_name}`");
}
let deadline = tokio::time::Instant::now() + Duration::from_secs(20);
let record = loop {
if let Some(record) = registered_runtime(tmux_name)? {
break record;
}
if !session_exists(tmux_name) {
bail!("supervised runtime exited before registering; rerun with --no-tmux to inspect the startup error");
}
if tokio::time::Instant::now() >= deadline {
let _ = Command::new("tmux")
.args(["kill-session", "-t", tmux_name])
.status();
bail!("timed out waiting for supervised runtime `{tmux_name}` to register");
}
tokio::time::sleep(Duration::from_millis(50)).await;
};
let frontend_spec_path = write_spec(&LaunchSpec {
executable: os_bytes(executable.as_os_str()),
arguments: [
OsString::from("sessions"),
OsString::from("attach"),
OsString::from(&record.runtime_session_id),
OsString::from("--take-control"),
]
.iter()
.map(|value| os_bytes(value))
.collect(),
environment: captured_environment(),
})?;
let frontend_status = Command::new("tmux")
.args(["new-window", "-d", "-t", tmux_name, "-n", "frontend"])
.arg(&executable)
.arg("__supervised-child")
.arg(&frontend_spec_path)
.status()
.context("starting tmux frontend")?;
if !frontend_status.success() {
let _ = fs::remove_file(frontend_spec_path);
bail!("runtime started, but tmux could not create its frontend window");
}
let _ = Command::new("tmux")
.args(["select-window", "-t", &format!("{tmux_name}:frontend")])
.status();
Ok(record)
}
pub(crate) fn run_child(spec_path: &Path) -> Result<()> {
let bytes = fs::read(spec_path).context("reading supervised launch spec")?;
let _ = fs::remove_file(spec_path);
let spec: LaunchSpec =
serde_json::from_slice(&bytes).context("parsing supervised launch spec")?;
let executable = os_string(spec.executable);
let arguments = spec
.arguments
.into_iter()
.map(os_string)
.collect::<Vec<_>>();
let mut command = Command::new(executable);
command.args(arguments);
for (key, value) in spec.environment {
command.env(os_string(key), os_string(value));
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
Err(command.exec()).context("executing supervised runtime owner")
}
#[cfg(not(unix))]
{
let status = command
.status()
.context("executing supervised runtime owner")?;
if !status.success() {
bail!("supervised runtime owner exited with {status}");
}
Ok(())
}
}
fn sanitized_arguments() -> Vec<OsString> {
let mut output = Vec::new();
let mut input = std::env::args_os().skip(1);
while let Some(argument) = input.next() {
let text = argument.to_string_lossy();
if text == "--api-key" || text == "--token" {
let _ = input.next();
continue;
}
if text.starts_with("--api-key=") || text.starts_with("--token=") {
continue;
}
output.push(argument);
}
output
}
fn write_spec(spec: &LaunchSpec) -> Result<PathBuf> {
let directory = supervisor_directory();
fs::create_dir_all(&directory)?;
#[cfg(unix)]
fs::set_permissions(&directory, fs::Permissions::from_mode(0o700))?;
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let path = directory.join(format!("launch-{}-{nonce}.json", std::process::id()));
let mut options = OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
options.mode(0o600);
let mut file = options.open(&path)?;
file.write_all(&serde_json::to_vec(spec)?)?;
file.sync_all()?;
Ok(path)
}
fn supervisor_directory() -> PathBuf {
if let Some(home) = std::env::var_os("SUPERCODE_HOME").filter(|value| !value.is_empty()) {
return PathBuf::from(home).join("supervisor");
}
if let Some(home) = std::env::var_os("XDG_CONFIG_HOME").filter(|value| !value.is_empty()) {
return PathBuf::from(home).join("supercode/supervisor");
}
PathBuf::from(std::env::var_os("HOME").unwrap_or_else(|| OsString::from(".")))
.join(".config/supercode/supervisor")
}
fn captured_environment() -> Vec<(Vec<u8>, Vec<u8>)> {
std::env::vars_os()
.map(|(key, value)| (os_bytes(&key), os_bytes(&value)))
.collect()
}
fn stable_hash(value: &str) -> u64 {
value.bytes().fold(0xcbf29ce484222325, |hash, byte| {
(hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)
})
}
#[cfg(unix)]
fn os_bytes(value: &OsStr) -> Vec<u8> {
value.as_bytes().to_vec()
}
#[cfg(not(unix))]
fn os_bytes(value: &OsStr) -> Vec<u8> {
value.to_string_lossy().as_bytes().to_vec()
}
#[cfg(unix)]
fn os_string(value: Vec<u8>) -> OsString {
OsString::from_vec(value)
}
#[cfg(not(unix))]
fn os_string(value: Vec<u8>) -> OsString {
OsString::from(String::from_utf8_lossy(&value).into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn predictable_names_are_stable_sanitized_and_distinct() {
let first = predictable_name("claude", "session/A strange id");
assert_eq!(first, predictable_name("claude", "session/A strange id"));
assert!(first.starts_with("supercode-claude-session-a-strange-id-"));
assert_ne!(first, predictable_name("claude", "session/A strange id 2"));
}
}