Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@

### Formats And Versions

* **Control messages:** any change to a `Request` or `Response` shape in `ally-core/src/control.rs` raises `control::VERSION`, currently 14.
* **Control messages:** any change to a `Request` or `Response` shape in `ally-core/src/control.rs` raises `control::VERSION`, currently 15.
* **Menu bar lines:** if a field in `ally-cli/src/watch/event.rs` changes meaning or is removed, raise `watch::event::VERSION` and change `ally-cli/menubar/Sources/Model.swift` in the same commit.
* **Pairing messages:** a change to the pairing messages in `ally-core/src/protocol.rs` means both machines must update before the next link. A change to `Specs` or `Health`, which are saved or sent after pairing, forces every user to link again. Say which one explicitly.

Expand Down
46 changes: 41 additions & 5 deletions crates/ally-agent/src/jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,18 +495,42 @@ fn stop_run(job: &Job) {
kill_remaining(&tree);
}

fn stop_session(root: &Path, job: &Job) {
let target = format!("={}", job.id);
let panes: Vec<u32> = tmux(root)
.args(["list-panes", "-s", "-t", &target, "-F", "#{pane_pid}"])
/// Every live process that belongs to an active job, with its start time. A run whose
/// leader no longer has the recorded start time owns nothing, so a reused PID never counts.
pub fn processes(root: &Path, job: &Job) -> HashMap<u32, u64> {
match (job.kind, job.pid, job.process_start) {
(JobKind::Session, _, _) => process_tree(&session_panes(root, &job.id), None),
(JobKind::Run, Some(pid), Some(start)) if process_start(pid) == Some(start) => {
process_tree(&[pid], Some(pid))
}
(JobKind::Run, _, _) => HashMap::new(),
}
}

/// The shell process of each pane in a session.
fn session_panes(root: &Path, id: &str) -> Vec<u32> {
tmux(root)
.args([
"list-panes",
"-s",
"-t",
&format!("={id}"),
"-F",
"#{pane_pid}",
])
.output()
.map(|out| {
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|line| line.trim().parse().ok())
.collect()
})
.unwrap_or_default();
.unwrap_or_default()
}

fn stop_session(root: &Path, job: &Job) {
let target = format!("={}", job.id);
let panes = session_panes(root, &job.id);
let tree = process_tree(&panes, None);
let shells: HashMap<u32, u64> = tree
.iter()
Expand Down Expand Up @@ -607,6 +631,18 @@ pub fn signal_group(pid: u32, start: u64, signal: i32) -> bool {
mod tests {
use super::*;

/// A run whose leader has another start time is a reused PID, which owns nothing.
#[test]
fn a_run_owns_its_processes_only_while_its_leader_matches() {
let me = std::process::id();
let mut job = new_job(JobKind::Run, None, "test".to_string());
job.pid = Some(me);
job.process_start = process_start(me);
assert!(processes(Path::new("/nonexistent"), &job).contains_key(&me));
job.process_start = job.process_start.map(|start| start + 1);
assert!(processes(Path::new("/nonexistent"), &job).is_empty());
}

#[test]
fn sessions_get_color_mouse_and_the_login_shell() {
let config = tmux_config("/usr/bin/bash", "tmux-256color");
Expand Down
1 change: 1 addition & 0 deletions crates/ally-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod awake;
pub mod boot;
pub mod clients;
pub mod jobs;
pub mod ports;
pub mod projects;
pub mod runner;
pub mod service;
Expand Down
Loading
Loading