use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use anyhow::{anyhow, bail, Context, Result};
use serde_json::Value;
use supercode_harness::mail_route::{Door, LiveSessions};
use supercode_harness::mailbox::MailAddress;
use supercode_harness::{DiscoveryQuery, HarnessHomes, HarnessId, StorageLocator};
#[derive(clap::Args)]
pub(crate) struct OpenArgs {
#[arg(required_unless_present = "new")]
pub(crate) session: Option<String>,
#[arg(long, value_name = "PROGRAM", conflicts_with_all = ["session", "relocate"])]
pub(crate) new: Option<String>,
#[arg(long, value_name = "KIND", requires = "new")]
pub(crate) kind: Option<String>,
#[arg(long, value_name = "KEY", requires = "new")]
pub(crate) key: Option<String>,
#[arg(long, value_name = "TEXT", requires = "new")]
pub(crate) input: Option<String>,
#[arg(long, requires = "new")]
pub(crate) notify_when_ended: bool,
#[arg(long, value_name = "MACHINE", conflicts_with = "relocate")]
pub(crate) on: Option<String>,
#[arg(long, conflicts_with = "take_control")]
pub(crate) observe: bool,
#[arg(long, value_enum, default_value_t)]
pub(crate) ui: crate::frontend_client::FrontendClientId,
#[arg(long)]
pub(crate) take_control: bool,
#[arg(long)]
pub(crate) remote_instructions: bool,
#[arg(long, value_name = "TOKEN", conflicts_with = "credential_fd")]
pub(crate) token: Option<String>,
#[arg(long, value_name = "FD", conflicts_with = "token")]
pub(crate) credential_fd: Option<i32>,
#[arg(long, value_name = "ID")]
pub(crate) client_id: Option<String>,
#[arg(long)]
pub(crate) acp: bool,
#[arg(long, value_name = "ID", requires = "acp")]
pub(crate) acp_session: Option<String>,
#[arg(long, value_name = "SEQUENCE", requires = "acp")]
pub(crate) after_sequence: Option<u64>,
#[arg(long, value_name = "HARNESS", requires = "receipt_session")]
pub(crate) harness: Option<String>,
#[arg(long = "session", value_name = "SESSION", requires = "harness")]
pub(crate) receipt_session: Option<String>,
#[arg(long, conflicts_with_all = ["new", "relocate"])]
pub(crate) native: bool,
#[arg(long = "move")]
pub(crate) relocate: bool,
#[arg(long)]
pub(crate) detach: bool,
#[arg(last = true)]
pub(crate) args: Vec<String>,
}
enum Resolved {
Session(Target),
Runtime(String),
}
async fn open_runtime(cli: &crate::Cli, open: &OpenArgs, runtime: &str) -> Result<()> {
if open.on.is_some() {
bail!("a runtime on another machine is opened by its URL there");
}
crate::attach_runtime_client_cmd(
cli,
runtime,
open.ui,
open.observe,
open.take_control,
open.remote_instructions,
)
.await
}
struct Target {
harness: String,
id: String,
name: Option<String>,
cwd: Option<PathBuf>,
transcript: Option<PathBuf>,
pid: Option<u32>,
pane: Option<String>,
tmux: Option<String>,
}
pub(crate) async fn run(cli: &crate::Cli, open: &OpenArgs) -> Result<()> {
let view = View {
detach: open.detach,
observe: open.observe,
on: open.on.as_deref(),
context: cli.context.as_deref(),
};
if let Some(program) = &open.new {
return open_new(program, open, cli.cwd.as_deref(), &view);
}
let wanted = open.session.as_deref().unwrap_or_default();
if wanted.starts_with("http://") || wanted.starts_with("https://") {
return crate::attach_cmd(
cli,
crate::AttachConnectOptions {
base_url: wanted,
token: open.token.as_deref(),
credential_fd: open.credential_fd,
client_id: open.client_id.as_deref(),
through_acp: open.acp,
session_id: open.acp_session.clone(),
after_sequence: open.after_sequence,
},
)
.await;
}
if wanted.starts_with("supercode-live://") {
let (Some(harness), Some(session)) = (&open.harness, &open.receipt_session) else {
bail!("a supercode-live:// receipt is opened with --harness and --session, which it is checked against");
};
return crate::harness_attach_cmd(cli, wanted, harness, session).await;
}
if is_daemon_pane(wanted) {
return attach(wanted, &view, wanted);
}
if let Some(record) = supercode_harness::runtime_mail::controlled_runtimes()
.into_iter()
.find(|record| record.runtime_session_id == wanted || record.source.session_id == wanted)
{
return open_runtime(cli, open, &record.runtime_session_id).await;
}
if let Some(machine) = &open.on {
bail!(
"on another machine a pane is named by its id (`p_…`, from `supercode teams panes \
ls --machine {machine}`); `{wanted}` is not one"
);
}
let homes = HarnessHomes::default();
if let Some(machine) = crate::message::other_machine(wanted) {
let row = crate::message::team_session(&homes, wanted)
.ok_or_else(|| anyhow!("no session named `{wanted}` is running on {machine}"))?;
return open_there(&homes, &row, open, view);
}
let target = match resolve(&homes, wanted) {
Ok(Resolved::Session(target)) => target,
Ok(Resolved::Runtime(runtime)) => return open_runtime(cli, open, &runtime).await,
Err(error) => match crate::message::team_session(&homes, wanted) {
Some(row) => return open_there(&homes, &row, open, view),
None => return Err(error),
},
};
let label = target.name.clone().unwrap_or_else(|| short(&target.id));
if open.observe && target.pane.is_none() {
bail!(
"{label} is not in a pane or hosted here right now, so there is nothing to observe. \
Nothing was opened; without --observe it would be resumed in a new pane."
);
}
if open.native {
return open_native(&target, open, &view, &label);
}
if let Some(pane) = &target.pane {
return attach(pane, &view, &label);
}
if let Some(pid) = target.pid {
if !open.relocate {
bail!(
"{label} is running outside supercode (pid {pid}{}). Nothing was opened: a \
session has one writer. End it there (`/exit`), or run `supercode open {} --move` \
to end it and open it here.",
target
.tmux
.as_ref()
.map(|tmux| format!(", tmux session {tmux}"))
.unwrap_or_default(),
wanted
);
}
refuse_foreign_process(pid, &label)?;
end_process(pid)?;
}
if target.harness == HarnessId::CODEX {
give_codex_its_delivery_door();
}
let (program, arguments) = launch_command(&target, &open.args)?;
let key = format!("open:{}:{}", target.harness, target.id);
let mut request = vec![
"launch".to_string(),
format!("--launch-key={key}"),
format!("--pane={}", pane_for(&key)),
format!("--kind={}", target.harness),
];
if attaches_here(&view) {
request.push("--popup=false".into());
}
if let Some(cwd) = &target.cwd {
request.push(format!("--cwd={}", cwd.display()));
}
request.push(program);
request.push("--".into());
request.extend(arguments);
let launched = fleet_json(&request)?;
let pane = launched["pane"]
.as_str()
.ok_or_else(|| anyhow!("the machine daemon opened no pane: {launched}"))?
.to_string();
attach(&pane, &view, &label)
}
fn open_native(target: &Target, open: &OpenArgs, view: &View, label: &str) -> Result<()> {
let transcript = target
.transcript
.as_ref()
.ok_or_else(|| anyhow!("{label} has no transcript file supercode can continue from"))?;
let program = std::env::current_exe().context("locate this supercode")?;
let key = format!("open:native:{}:{}", target.harness, target.id);
let mut request = vec![
"launch".to_string(),
format!("--launch-key={key}"),
format!("--pane={}", pane_for(&key)),
"--kind=supercode".to_string(),
];
if attaches_here(view) {
request.push("--popup=false".into());
}
if let Some(cwd) = &target.cwd {
request.push(format!("--cwd={}", cwd.display()));
}
request.push(program.display().to_string());
request.push("--".into());
request.push("resume".into());
request.push(transcript.display().to_string());
request.extend(open.args.iter().cloned());
let launched = fleet_json(&request)?;
let pane = launched["pane"]
.as_str()
.ok_or_else(|| anyhow!("the machine daemon opened no pane: {launched}"))?
.to_string();
attach(&pane, view, label)
}
fn attaches_here(view: &View) -> bool {
use std::io::IsTerminal;
view.on.is_none()
&& !view.detach
&& std::io::stdin().is_terminal()
&& std::io::stdout().is_terminal()
}
struct View<'a> {
detach: bool,
observe: bool,
on: Option<&'a str>,
context: Option<&'a str>,
}
fn open_new(program: &str, open: &OpenArgs, cwd: Option<&Path>, view: &View) -> Result<()> {
let program = match program {
"claude-code" => "claude",
other => other,
};
let mut request = vec!["launch".to_string()];
if attaches_here(view) {
request.push("--popup=false".into());
}
if let Some(machine) = view.on {
request.extend(["--on".to_string(), machine.to_string()]);
if let Some(context) = view.context {
request.push(format!("--context={context}"));
}
}
if let Some(input) = &open.input {
request.push(format!("--input={input}"));
}
if let Some(key) = &open.key {
request.push(format!("--launch-key={key}"));
}
if open.notify_when_ended {
let caller = supercode_harness::mail_route::resolve_caller(
&HarnessHomes::default(),
&supercode_harness::mail_route::process_ancestry(),
)
.map_err(|message| anyhow!(message))?;
request.push(format!("--notify={}", caller.address));
let machine = match view.on {
Some(machine) => supercode_harness::mailbox::normal_machine_name(machine),
None => supercode_harness::mailbox::local_machine_name(),
};
request.push(format!("--notify-machine={machine}"));
}
let kind = open.kind.clone().unwrap_or_else(|| {
match Path::new(program)
.file_stem()
.and_then(|name| name.to_str())
.unwrap_or(program)
{
"claude" => HarnessId::CLAUDE_CODE.to_string(),
"codex" => HarnessId::CODEX.to_string(),
other => other.to_string(),
}
});
request.push(format!("--kind={kind}"));
let cwd = match cwd {
Some(cwd) => Some(cwd.to_path_buf()),
None if view.on.is_none() => std::env::current_dir().ok(),
None => None,
};
if let Some(cwd) = &cwd {
request.push(format!("--cwd={}", cwd.display()));
}
request.push(program.to_string());
request.push("--".into());
if kind == HarnessId::CODEX && view.on.is_none() {
give_codex_its_delivery_door();
}
let no_prompts = no_prompt_arguments(&kind, cwd.as_deref(), &open.args);
request.extend(no_prompts);
request.extend(open.args.iter().cloned());
let launched = fleet_json(&request)?;
let pane = launched["pane"]
.as_str()
.ok_or_else(|| anyhow!("the machine daemon opened no pane: {launched}"))?
.to_string();
if launched["status"] == "finished" {
eprintln!("`{program}` ran and finished (status 0) in pane {pane}, which has closed.");
println!(
"{}",
serde_json::json!({"pane": pane, "session": program, "machine": view.on, "finished": true})
);
return Ok(());
}
if launched["status"] == "exited" {
let how = launched["exit"]
.as_str()
.unwrap_or("no exit status recorded");
bail!("`{program}` ended as soon as it started ({how}), so pane {pane} is gone. A command with arguments goes as the program and its arguments (`open --new bash -- script.sh`) or as one quoted command line.");
}
attach(&pane, view, program)
}
fn resolve(homes: &HarnessHomes, wanted: &str) -> Result<Resolved> {
let live = LiveSessions::read(homes);
let address = MailAddress::parse(wanted).ok();
let running = live.all().iter().find(|session| {
address.as_ref() == Some(&session.address)
|| session.name == wanted
|| session.name.split('@').next() == Some(wanted)
|| session.address.session_id == wanted
});
if let Some(session) = running {
let name = session.name.split('@').next().unwrap_or(&session.name);
if let Door::Runtime(record) = &session.door {
return Ok(Resolved::Runtime(record.runtime_session_id.clone()));
}
let harness = session.address.harness.clone();
let id = session.address.session_id.clone();
let tmux = session
.tmux
.as_deref()
.and_then(|tmux| tmux.split(':').next())
.map(str::to_string);
let pane = crate::message::session_pane(session.tmux.as_deref(), session.pid, &mut None);
let pid = match session.pid {
Some(pid) => Some(pid),
None if harness == HarnessId::CODEX => codex_pid(&id),
None => None,
};
let mut target = persisted(homes, &harness, &id).unwrap_or(Target {
harness,
id,
name: None,
cwd: None,
transcript: None,
pid: None,
pane: None,
tmux: None,
});
target.name = Some(name.to_string());
if target.cwd.is_none() {
target.cwd = session.cwd.clone();
}
target.pid = pid;
target.tmux = tmux.filter(|_| pane.is_none());
target.pane = pane;
return Ok(Resolved::Session(target));
}
let query = DiscoveryQuery {
harnesses: vec![
HarnessId::new(HarnessId::CLAUDE_CODE),
HarnessId::new(HarnessId::CODEX),
],
homes: homes.clone(),
query: Some(wanted.to_string()),
..Default::default()
};
let mut found: Vec<_> = supercode_harness::discover_sessions(&query)?
.into_iter()
.filter(|descriptor| {
let id = &descriptor.locator.session_id;
id == wanted
|| (wanted.len() >= 6 && id.starts_with(wanted))
|| descriptor.title.as_deref() == Some(wanted)
})
.collect();
found.sort_by_key(|descriptor| std::cmp::Reverse(descriptor.updated_at_ms));
let ids: std::collections::BTreeSet<_> = found
.iter()
.map(|descriptor| descriptor.locator.session_id.clone())
.collect();
if ids.len() > 1 && !found.iter().any(|d| d.locator.session_id == wanted) {
let listed: Vec<_> = ids.iter().take(5).map(|id| short(id)).collect();
bail!(
"`{wanted}` names {} sessions ({}). Name one by its id.",
ids.len(),
listed.join(", ")
);
}
let descriptor = found.into_iter().next().ok_or_else(|| {
anyhow!("no session is named `{wanted}`: not running, and no saved Claude Code or Codex session has that id or name")
})?;
let transcript = match &descriptor.locator.storage {
StorageLocator::File { path } => Some(path.clone()),
_ => None,
};
Ok(Resolved::Session(Target {
harness: descriptor.locator.harness.0.clone(),
id: descriptor.locator.session_id.clone(),
name: descriptor.title.clone(),
cwd: descriptor.cwd.clone(),
transcript,
pid: None,
pane: None,
tmux: None,
}))
}
fn persisted(homes: &HarnessHomes, harness: &str, id: &str) -> Option<Target> {
let query = DiscoveryQuery {
harnesses: vec![HarnessId::new(harness)],
homes: homes.clone(),
..Default::default()
};
let descriptor = supercode_harness::discover_sessions(&query)
.ok()?
.into_iter()
.find(|descriptor| descriptor.locator.session_id == id)?;
let transcript = match &descriptor.locator.storage {
StorageLocator::File { path } => Some(path.clone()),
_ => None,
};
Some(Target {
harness: harness.to_string(),
id: id.to_string(),
name: descriptor.title,
cwd: descriptor.cwd,
transcript,
pid: None,
pane: None,
tmux: None,
})
}
fn launch_command(target: &Target, extra: &[String]) -> Result<(String, Vec<String>)> {
let mut arguments = Vec::new();
let program = match target.harness.as_str() {
HarnessId::CLAUDE_CODE => {
arguments.extend(["--resume".to_string(), target.id.clone()]);
if let Some(name) = &target.name {
arguments.extend(["--name".to_string(), name.clone()]);
}
"claude"
}
HarnessId::CODEX => {
arguments.extend(["resume".to_string(), target.id.clone()]);
"codex"
}
other => {
bail!("`supercode open` opens Claude Code and Codex sessions; this one is {other}")
}
};
arguments.extend(no_prompt_arguments(
&target.harness,
target.cwd.as_deref(),
extra,
));
arguments.extend(extra.iter().cloned());
Ok((program.to_string(), arguments))
}
fn give_codex_its_delivery_door() {
match crate::message::install_codex_hooks() {
Ok((path, added)) if !added.is_empty() => eprintln!(
"Added supercode's mail hooks to {}, so sessions can message this Codex.",
path.display()
),
Ok(_) => {}
Err(error) => eprintln!("supercode's mail hooks were not added to Codex: {error:#}"),
}
}
fn no_prompt_arguments(harness: &str, cwd: Option<&Path>, arguments: &[String]) -> Vec<String> {
supercode_harness::startup_prompts::startup_arguments(harness, cwd, arguments, true)
}
fn pane_for(launch_key: &str) -> String {
format!("p_{}", &blake3::hash(launch_key.as_bytes()).to_hex()[..16])
}
pub(crate) fn is_daemon_pane(name: &str) -> bool {
name.strip_prefix("p_").is_some_and(|rest| {
!rest.is_empty()
&& rest
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
})
}
fn codex_pid(id: &str) -> Option<u32> {
let output = std::process::Command::new("ps")
.args(["-axo", "pid=,args="])
.output()
.ok()?;
String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| {
let mut fields = line.split_whitespace();
let pid: u32 = fields.next()?.parse().ok()?;
let program = fields.next()?;
let is_codex = Path::new(program)
.file_name()
.is_some_and(|name| name == "codex");
(is_codex && fields.next() != Some("app-server")).then_some(pid)
})
.find(|pid| {
supercode_harness::codex_peer::session_of_process(*pid)
.is_some_and(|(session, _)| session == id)
})
}
#[derive(clap::Args)]
pub(crate) struct CloseArgs {
pub(crate) session: String,
}
pub(crate) fn close(args: &CloseArgs) -> Result<()> {
let wanted = args.session.as_str();
if is_daemon_pane(wanted) {
return close_pane(wanted, wanted);
}
let homes = HarnessHomes::default();
if let Some(machine) = crate::message::other_machine(wanted) {
let row = crate::message::team_session(&homes, wanted)
.ok_or_else(|| anyhow!("no session named `{wanted}` is running on {machine}"))?;
return close_there(&homes, &row);
}
let target = match resolve(&homes, wanted) {
Ok(Resolved::Session(target)) => target,
Ok(Resolved::Runtime(runtime)) => {
bail!("{wanted} is runtime {runtime}, which supercode hosts; it ends with its host")
}
Err(error) => match crate::message::team_session(&homes, wanted) {
Some(row) => return close_there(&homes, &row),
None => return Err(error),
},
};
let label = target.name.clone().unwrap_or_else(|| short(&target.id));
if let Some(pane) = &target.pane {
return close_pane(pane, &label);
}
let Some(pid) = target.pid else {
println!("{label} is not running; there is nothing to close.");
return Ok(());
};
refuse_foreign_process(pid, &label)?;
end_process(pid)?;
println!("closed {label} (pid {pid}); its transcript stays, and `supercode open {wanted}` resumes it");
Ok(())
}
#[derive(clap::Args)]
pub(crate) struct StopArgs {
pub(crate) session: String,
}
pub(crate) fn stop(args: &StopArgs) -> Result<()> {
let wanted = args.session.as_str();
let homes = HarnessHomes::default();
if let Some(machine) = crate::message::other_machine(wanted) {
let row = crate::message::team_session(&homes, wanted)
.ok_or_else(|| anyhow!("no session named `{wanted}` is running on {machine}"))?;
let (machine, address) = row_machine(&row)?;
run_there(&machine, &address, &["stop", &address])?;
println!(
"asked {machine} to stop {} (`supercode stop` runs there)",
row["name"].as_str().unwrap_or(wanted)
);
return Ok(());
}
let target = match resolve(&homes, wanted) {
Ok(Resolved::Session(target)) => target,
Ok(Resolved::Runtime(runtime)) => {
bail!("{wanted} is runtime {runtime}, which supercode hosts; interrupt it through `supercode harness drive`")
}
Err(error) => return Err(error),
};
let label = target.name.clone().unwrap_or_else(|| short(&target.id));
let Some(pane) = &target.pane else {
bail!(
"{label} runs in a terminal of its own, which supercode has no input door to: interrupt it at that terminal (or through its harness's own remote control)"
)
};
let program = std::env::current_exe().context("locate this supercode")?;
let since = turn_started_at(target.transcript.as_deref());
let status = std::process::Command::new(program)
.args(["teams", "input", pane, "--key", "Escape"])
.stdout(std::process::Stdio::null())
.status()
.context("run supercode teams input")?;
if !status.success() {
bail!("the machine daemon did not deliver Escape to pane {pane}");
}
std::thread::sleep(Duration::from_secs(2));
let ended = match (target.pid, since) {
(Some(pid), Some(since)) => end_turn_children(pid, since),
_ => Vec::new(),
};
if ended.is_empty() {
println!("stopped {label} (pane {pane}): its turn was interrupted; no process its turn started was left running");
} else {
let list = ended
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(", ");
println!("stopped {label} (pane {pane}): its turn was interrupted, and the processes it started were ended (pids {list})");
}
Ok(())
}
fn turn_started_at(transcript: Option<&Path>) -> Option<i64> {
let text = std::fs::read_to_string(transcript?).ok()?;
text.lines().rev().find_map(|line| {
let record: Value = serde_json::from_str(line).ok()?;
if record["type"] != "user" || record["isSidechain"] == true || record["isMeta"] == true {
return None;
}
let content = &record["message"]["content"];
let texts: Vec<&str> = match content {
Value::String(text) => vec![text.as_str()],
Value::Array(items) => items
.iter()
.filter(|item| item["type"] == "text")
.filter_map(|item| item["text"].as_str())
.collect(),
_ => Vec::new(),
};
let typed = !texts.is_empty()
&& !texts
.iter()
.all(|text| text.starts_with("[Request interrupted by user"));
if !typed {
return None;
}
supercode_interchange::sidecar::rfc3339_to_ms(record["timestamp"].as_str()?)
.map(|ms| ms / 1000)
})
}
#[cfg(unix)]
fn end_turn_children(pid: u32, since: i64) -> Vec<u32> {
let Ok(output) = std::process::Command::new("ps")
.args(["-axo", "pid=,ppid=,etime="])
.output()
else {
return Vec::new();
};
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs() as i64);
let seconds = |etime: &str| -> Option<i64> {
let (days, rest) = match etime.split_once('-') {
Some((days, rest)) => (days.parse::<i64>().ok()?, rest),
None => (0, etime),
};
let parts: Vec<i64> = rest
.split(':')
.map(|part| part.parse().ok())
.collect::<Option<_>>()?;
let (h, m, s) = match parts.as_slice() {
[m, s] => (0, *m, *s),
[h, m, s] => (*h, *m, *s),
_ => return None,
};
Some(days * 86_400 + h * 3_600 + m * 60 + s)
};
let table: Vec<(u32, u32, i64)> = String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| {
let mut fields = line.split_whitespace();
let child: u32 = fields.next()?.parse().ok()?;
let parent: u32 = fields.next()?.parse().ok()?;
let started = now - seconds(fields.next()?)?;
Some((child, parent, started))
})
.collect();
let roots: Vec<u32> = table
.iter()
.filter(|(_, parent, started)| *parent == pid && *started >= since)
.map(|(child, _, _)| *child)
.collect();
for root in &roots {
let mut tree = vec![*root];
let mut index = 0;
while index < tree.len() {
let parent = tree[index];
tree.extend(
table
.iter()
.filter(|(_, p, _)| *p == parent)
.map(|(c, _, _)| *c),
);
index += 1;
}
for member in tree.iter().rev() {
unsafe { libc::kill(*member as libc::pid_t, libc::SIGTERM) };
}
}
roots
}
#[cfg(not(unix))]
fn end_turn_children(_pid: u32, _since: i64) -> Vec<u32> {
Vec::new()
}
fn close_pane(pane: &str, label: &str) -> Result<()> {
close_pane_on(pane, label, None)
}
fn close_pane_on(pane: &str, label: &str, machine: Option<&str>) -> Result<()> {
let program = std::env::current_exe().context("locate this supercode")?;
let mut command = std::process::Command::new(program);
command.args(["teams", "panes", "close", pane]);
if let Some(machine) = machine {
command.args(["--machine", machine]);
}
let status = command
.stdout(std::process::Stdio::null())
.status()
.context("run supercode teams panes close")?;
let there = machine
.map(|machine| format!(" on {machine}"))
.unwrap_or_default();
if !status.success() {
bail!("the machine daemon{there} did not close pane {pane}");
}
println!("closed {label} (pane {pane}{there})");
Ok(())
}
fn open_there(homes: &HarnessHomes, row: &Value, open: &OpenArgs, view: View) -> Result<()> {
let label = row["name"].as_str().unwrap_or_default().to_string();
let (machine, pane) = match remote_pane(row, "opened") {
Ok(found) => found,
Err(_) if open.relocate && row["pane"].is_null() && !is_hosted_row(row) => {
let (machine, address) = row_machine(row)?;
run_there(
&machine,
&address,
&["open", &address, "--move", "--detach"],
)?;
let pane = await_pane(homes, &address).ok_or_else(|| {
anyhow!("{label} was not seen in a pane on {machine} within 30 seconds")
})?;
(machine, pane)
}
Err(error) => return Err(error),
};
let view = View {
on: Some(&machine),
..view
};
attach(&pane, &view, &label)
}
fn close_there(homes: &HarnessHomes, row: &Value) -> Result<()> {
let label = row["name"].as_str().unwrap_or_default();
if row["pane"].is_null() && !is_hosted_row(row) {
let (machine, address) = row_machine(row)?;
run_there(&machine, &address, &["close", &address])?;
if !await_gone(homes, &address) {
bail!("{label} still runs on {machine} 30 seconds after `supercode close` ran there");
}
println!("closed {label} on {machine} (its process ended there; its transcript stays)");
return Ok(());
}
let (machine, pane) = remote_pane(row, "closed")?;
close_pane_on(&pane, label, Some(&machine))
}
fn is_hosted_row(row: &Value) -> bool {
row["delivery"] == "runtime" || row["status"] == "hosted"
}
fn row_machine(row: &Value) -> Result<(String, String)> {
let address = MailAddress::parse(row["address"].as_str().unwrap_or_default())?;
Ok((address.machine.clone(), address.to_string()))
}
fn run_there(machine: &str, address: &str, args: &[&str]) -> Result<()> {
let mut request = vec![
"launch".to_string(),
"--on".into(),
machine.to_string(),
format!("--launch-key=there:{}:{address}", args[0]),
"--act-for-caller".into(),
"supercode".into(),
"--".into(),
];
request.extend(args.iter().map(|arg| arg.to_string()));
let launched = fleet_json(&request)?;
if matches!(args.first(), Some(&"close" | &"stop")) {
let key = launched["key"]
.as_str()
.ok_or_else(|| anyhow!("remote command launch returned no receipt key"))?;
await_remote_command(machine, key)?;
}
Ok(())
}
fn await_remote_command(machine: &str, key: &str) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(30);
loop {
let program = std::env::current_exe().context("locate this supercode")?;
let output = std::process::Command::new(program)
.args(["teams", "launches", "get", key, "--machine", machine])
.stdin(std::process::Stdio::null())
.output()
.context("read the remote command receipt")?;
if !output.status.success() {
bail!(
"remote command receipt unavailable on {machine}: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
}
let receipt: Value = serde_json::from_slice(&output.stdout)
.context("read the remote command receipt JSON")?;
if receipt["live"] == false {
let how = receipt["exit"]["how"]
.as_str()
.unwrap_or("exit status unavailable");
let detail = receipt["exit_output"]["screen"]
.as_str()
.unwrap_or("")
.trim();
match receipt["exit"]["failed"].as_bool() {
Some(false) => return Ok(()),
Some(true) => bail!("remote command on {machine} failed ({how}): {detail}"),
None => bail!(
"remote command on {machine} ended without a confirmed exit status: {detail}"
),
}
}
if Instant::now() >= deadline {
bail!("remote command on {machine} has not ended after 30 seconds; receipt key {key}");
}
std::thread::sleep(Duration::from_millis(250));
}
}
fn await_pane(homes: &HarnessHomes, address: &str) -> Option<String> {
let deadline = Instant::now() + Duration::from_secs(30);
while Instant::now() < deadline {
let pane = crate::message::team_session(homes, address)
.and_then(|row| row["pane"].as_str().map(str::to_string));
if pane.is_some() {
return pane;
}
std::thread::sleep(Duration::from_secs(2));
}
None
}
fn await_gone(homes: &HarnessHomes, address: &str) -> bool {
let deadline = Instant::now() + Duration::from_secs(30);
while Instant::now() < deadline {
if crate::message::team_session(homes, address).is_none() {
return true;
}
std::thread::sleep(Duration::from_secs(2));
}
false
}
fn remote_pane(row: &Value, verb: &str) -> Result<(String, String)> {
let name = row["name"].as_str().unwrap_or_default();
let address = MailAddress::parse(row["address"].as_str().unwrap_or_default())?;
if is_hosted_row(row) {
bail!(
"{name} is a runtime supercode hosts on {}; it is {verb} there",
address.machine
);
}
match row["pane"].as_str() {
Some(pane) => Ok((address.machine, pane.to_string())),
None => bail!(
"{name} runs on {} outside any pane its machine daemon holds. Nothing was {verb}: \
`supercode open {name} --move` ends it there and opens it in a pane there.",
address.machine
),
}
}
fn refuse_foreign_process(pid: u32, label: &str) -> Result<()> {
let Some(caller) = crate::acting_caller() else {
return Ok(());
};
let Ok(address) = MailAddress::parse(&caller) else {
return Ok(());
};
if address.harness == "board" {
return Ok(());
}
let own = LiveSessions::read(&HarnessHomes::default())
.all()
.iter()
.find(|session| session.address == address)
.and_then(|session| session.pid);
let ancestry = supercode_harness::mail_route::ancestry_of(pid);
if own.is_some_and(|own| ancestry[1..].contains(&own)) {
return Ok(());
}
bail!(
"{label} (pid {pid}) was not started by {caller}: a session ends only the processes it started. \
Nothing was closed; reach that session through its own doors (supercode message send)"
)
}
fn end_process(pid: u32) -> Result<()> {
#[cfg(unix)]
{
let alive = |signal| unsafe { libc::kill(pid as libc::pid_t, signal) == 0 };
alive(libc::SIGTERM);
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if !alive(0) {
return Ok(());
}
std::thread::sleep(Duration::from_millis(100));
}
bail!("the session's process (pid {pid}) did not end within 10 seconds; nothing was opened")
}
#[cfg(windows)]
{
use windows_sys::Win32::Foundation::{CloseHandle, WAIT_OBJECT_0};
use windows_sys::Win32::System::Threading::{
OpenProcess, TerminateProcess, WaitForSingleObject, PROCESS_SYNCHRONIZE,
PROCESS_TERMINATE,
};
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE | PROCESS_SYNCHRONIZE, 0, pid);
if handle.is_null() {
bail!("the session's process (pid {pid}) could not be opened to end it");
}
let ended = TerminateProcess(handle, 1) != 0
&& WaitForSingleObject(handle, 10_000) == WAIT_OBJECT_0;
CloseHandle(handle);
if !ended {
bail!("the session's process (pid {pid}) did not end within 10 seconds; nothing was opened");
}
}
Ok(())
}
#[cfg(not(any(unix, windows)))]
bail!("ending a session outside a pane is not supported on this platform (pid {pid})")
}
fn attach(pane: &str, view: &View, label: &str) -> Result<()> {
use std::io::IsTerminal;
if view.detach || !(std::io::stdin().is_terminal() && std::io::stdout().is_terminal()) {
println!(
"{}",
serde_json::json!({"pane": pane, "session": label, "machine": view.on})
);
return Ok(());
}
let mode = if view.observe { "observe" } else { "control" };
if let Some(machine) = view.on {
let mut request = vec![
"attach".to_string(),
"--on".into(),
machine.to_string(),
pane.to_string(),
format!("--mode={mode}"),
];
if let Some(context) = view.context {
request.push(format!("--context={context}"));
}
return crate::run_fleet(&request);
}
let bound = std::process::Command::new("tmux")
.env_remove("TMUX")
.args([
"bind-key",
"-n",
"C-]",
"if-shell",
"-F",
"#{m:p_*,#{session_name}}",
"detach-client",
"send-keys C-]",
])
.status()
.context("run tmux")?;
if !bound.success() {
bail!("tmux refused the detach key for {pane}");
}
eprintln!("[{label}: ctrl-] detaches; the session keeps running]");
let mut client = std::process::Command::new("tmux");
client.args(["-T", "RGB", "attach-session"]);
if view.observe {
client.arg("-r");
}
client
.args(["-t", pane])
.env("TERM", "screen-256color")
.env_remove("TMUX");
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let error = client.exec();
bail!("could not attach to {pane}: {error}")
}
#[cfg(not(unix))]
{
let status = client.status().context("run tmux")?;
if !status.success() {
bail!("tmux could not attach to {pane}");
}
Ok(())
}
}
fn fleet_json(args: &[String]) -> Result<Value> {
let entry = crate::fleet_entry()?;
let mut command = std::process::Command::new(crate::teams_node_bin());
command.arg(&entry).args(args);
crate::act_for_caller(&mut command);
let output = command
.stdin(std::process::Stdio::null())
.output()
.context("run the teams CLI")?;
if !output.status.success() {
bail!(
"the machine daemon refused: {}",
String::from_utf8_lossy(&output.stderr).trim()
);
}
serde_json::from_slice(&output.stdout).context("read the machine daemon's answer")
}
fn short(id: &str) -> String {
id.chars().take(8).collect()
}