@team-agent/installer 0.5.92 → 0.5.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/mod.rs +370 -441
- package/crates/team-agent/src/cli/models.rs +1 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +154 -30
- package/crates/team-agent/src/coordinator/health.rs +92 -30
- package/crates/team-agent/src/coordinator/tick.rs +62 -8
- package/crates/team-agent/src/coordinator/types.rs +2 -0
- package/crates/team-agent/src/kill_audit.rs +41 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +48 -14
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +4 -8
- package/crates/team-agent/src/lifecycle/launch.rs +17 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +10 -7
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +91 -4
- package/crates/team-agent/src/platform/process.rs +4 -45
- package/crates/team-agent/src/tmux_backend.rs +113 -3
- package/crates/team-agent/src/transport/test_support.rs +30 -0
- package/crates/team-agent/src/transport.rs +41 -0
- package/package.json +4 -4
|
@@ -549,9 +549,7 @@ pub mod lifecycle_port {
|
|
|
549
549
|
/// E12 下沉纯函数:bare-shutdown socket 拆除决策。
|
|
550
550
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
551
551
|
pub(crate) enum KillDecision {
|
|
552
|
-
///
|
|
553
|
-
KillServerExclusive,
|
|
554
|
-
/// 有 spare(leader 锚/前缀)或非独享 → 逐 session kill,绝不 kill-server。
|
|
552
|
+
/// Ownership-gated cleanup always uses per-session kills; never whole-server teardown.
|
|
555
553
|
KillIndividually {
|
|
556
554
|
to_kill: Vec<crate::transport::SessionName>,
|
|
557
555
|
spared: Vec<crate::transport::SessionName>,
|
|
@@ -560,8 +558,7 @@ pub mod lifecycle_port {
|
|
|
560
558
|
|
|
561
559
|
///
|
|
562
560
|
/// E12 纯决策(单测下沉):spare = `anchor_sessions` ∪ `team-agent-leader-*` 前缀(并集,锚优先)。
|
|
563
|
-
///
|
|
564
|
-
/// kill 非 spare 的(共享 socket / leader 在 → 绝不整 server 拆)。空 session 集 → 逐 kill(no-op)。
|
|
561
|
+
/// Always returns a per-session plan; ownership markers are checked by the caller.
|
|
565
562
|
pub(crate) fn sessions_to_kill(
|
|
566
563
|
sessions: &[crate::transport::SessionName],
|
|
567
564
|
anchor_sessions: &std::collections::BTreeSet<String>,
|
|
@@ -572,12 +569,9 @@ pub mod lifecycle_port {
|
|
|
572
569
|
};
|
|
573
570
|
let spared: Vec<_> = sessions.iter().filter(|s| is_spared(s)).cloned().collect();
|
|
574
571
|
let to_kill: Vec<_> = sessions.iter().filter(|s| !is_spared(s)).cloned().collect();
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
} else {
|
|
579
|
-
KillDecision::KillIndividually { to_kill, spared }
|
|
580
|
-
}
|
|
572
|
+
// Fail closed: even an apparently exclusive socket is not proof of
|
|
573
|
+
// ownership, so whole-server teardown is never selected.
|
|
574
|
+
KillDecision::KillIndividually { to_kill, spared }
|
|
581
575
|
}
|
|
582
576
|
|
|
583
577
|
#[derive(Debug, Default)]
|
|
@@ -615,118 +609,178 @@ pub mod lifecycle_port {
|
|
|
615
609
|
}
|
|
616
610
|
|
|
617
611
|
fn bare_shutdown_socket_cleanup(
|
|
612
|
+
workspace: &Path,
|
|
618
613
|
transport: &dyn crate::transport::Transport,
|
|
619
614
|
state: &Value,
|
|
620
615
|
event_log: &crate::event_log::EventLog,
|
|
621
616
|
) -> ShutdownSocketCleanup {
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
);
|
|
645
|
-
let mut error = None;
|
|
646
|
-
for session in &sessions {
|
|
647
|
-
let targets = vec![session.as_str().to_string()];
|
|
648
|
-
if let Err(audit_error) = crate::kill_audit::pre_kill_audit(
|
|
649
|
-
event_log,
|
|
650
|
-
transport,
|
|
651
|
-
"shutdown.shared_socket_individual",
|
|
652
|
-
crate::kill_audit::KILL_SESSION,
|
|
653
|
-
&targets,
|
|
654
|
-
) {
|
|
655
|
-
error.get_or_insert_with(|| {
|
|
656
|
-
format!("pre-kill audit failed: {audit_error}")
|
|
657
|
-
});
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
if let Err(err) = transport.kill_session(session) {
|
|
661
|
-
if !tmux_absent_error(&err.to_string()) {
|
|
662
|
-
error.get_or_insert_with(|| err.to_string());
|
|
663
|
-
}
|
|
664
|
-
}
|
|
617
|
+
// A bare shutdown is scoped to sessions registered in state. Never turn a
|
|
618
|
+
// socket-wide enumeration into an authorization decision: a shared/default
|
|
619
|
+
// tmux socket may contain unrelated teams and the absence of a marker is
|
|
620
|
+
// deliberately treated as Unknown.
|
|
621
|
+
let mut candidates = Vec::new();
|
|
622
|
+
if let Some(session) = state
|
|
623
|
+
.get("session_name")
|
|
624
|
+
.and_then(Value::as_str)
|
|
625
|
+
.filter(|name| !name.is_empty())
|
|
626
|
+
{
|
|
627
|
+
candidates.push(crate::transport::SessionName::new(session));
|
|
628
|
+
}
|
|
629
|
+
if let Some(teams) = state.get("teams").and_then(Value::as_object) {
|
|
630
|
+
for team in teams.values() {
|
|
631
|
+
if let Some(session) = team
|
|
632
|
+
.get("session_name")
|
|
633
|
+
.and_then(Value::as_str)
|
|
634
|
+
.filter(|name| !name.is_empty())
|
|
635
|
+
{
|
|
636
|
+
let session = crate::transport::SessionName::new(session);
|
|
637
|
+
if !candidates.iter().any(|existing| existing == &session) {
|
|
638
|
+
candidates.push(session);
|
|
665
639
|
}
|
|
666
|
-
return ShutdownSocketCleanup {
|
|
667
|
-
killed_sessions: sessions,
|
|
668
|
-
spared_sessions: Vec::new(),
|
|
669
|
-
error,
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
let targets = sessions
|
|
673
|
-
.iter()
|
|
674
|
-
.map(|session| session.as_str().to_string())
|
|
675
|
-
.collect::<Vec<_>>();
|
|
676
|
-
let error = match crate::kill_audit::pre_kill_audit(
|
|
677
|
-
event_log,
|
|
678
|
-
transport,
|
|
679
|
-
"shutdown.exclusive_socket",
|
|
680
|
-
crate::kill_audit::KILL_SERVER,
|
|
681
|
-
&targets,
|
|
682
|
-
) {
|
|
683
|
-
Ok(()) => transport.kill_server().err().map(|error| error.to_string()),
|
|
684
|
-
Err(error) => Some(format!("pre-kill audit failed: {error}")),
|
|
685
|
-
};
|
|
686
|
-
ShutdownSocketCleanup {
|
|
687
|
-
killed_sessions: sessions,
|
|
688
|
-
spared_sessions: Vec::new(),
|
|
689
|
-
error,
|
|
690
640
|
}
|
|
691
641
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
"killed_sessions": to_kill.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
|
|
700
|
-
}),
|
|
701
|
-
);
|
|
642
|
+
}
|
|
643
|
+
let targets = match transport.list_targets() {
|
|
644
|
+
Ok(targets) => targets,
|
|
645
|
+
Err(error) => {
|
|
646
|
+
return ShutdownSocketCleanup {
|
|
647
|
+
error: Some(format!("session ownership probe failed: {error}")),
|
|
648
|
+
..ShutdownSocketCleanup::default()
|
|
702
649
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
let protected_sessions = anchor_sessions_from_state(state, &targets, event_log);
|
|
653
|
+
let mut result = ShutdownSocketCleanup::default();
|
|
654
|
+
for session in candidates {
|
|
655
|
+
if protected_sessions.contains(session.as_str()) {
|
|
656
|
+
push_unique_session(&mut result.spared_sessions, session);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
match session_ownership(workspace, state, transport, &session, &targets) {
|
|
660
|
+
SessionOwnership::Owned => {
|
|
661
|
+
let target = vec![session.as_str().to_string()];
|
|
662
|
+
if let Err(error) = crate::kill_audit::pre_kill_audit_scoped(
|
|
707
663
|
event_log,
|
|
708
664
|
transport,
|
|
709
|
-
"shutdown.
|
|
665
|
+
"shutdown.state_owned_session",
|
|
710
666
|
crate::kill_audit::KILL_SESSION,
|
|
711
|
-
&
|
|
667
|
+
&target,
|
|
668
|
+
workspace,
|
|
669
|
+
state_for_session(state, session.as_str())
|
|
670
|
+
.and_then(|identity| identity.get("team_key").and_then(Value::as_str)),
|
|
671
|
+
state_for_session(state, session.as_str()).and_then(state_generation),
|
|
672
|
+
"session_marker+state_session+pane_path",
|
|
673
|
+
"positive workspace/team ownership",
|
|
712
674
|
) {
|
|
713
|
-
error
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
if let Err(err) = transport.kill_session(session) {
|
|
718
|
-
if !tmux_absent_error(&err.to_string()) {
|
|
719
|
-
error.get_or_insert_with(|| err.to_string());
|
|
675
|
+
result.error.get_or_insert_with(|| format!("pre-kill audit failed: {error}"));
|
|
676
|
+
} else if let Err(error) = transport.kill_session(&session) {
|
|
677
|
+
if !tmux_absent_error(&error.to_string()) {
|
|
678
|
+
result.error.get_or_insert_with(|| error.to_string());
|
|
720
679
|
}
|
|
680
|
+
} else {
|
|
681
|
+
result.killed_sessions.push(session);
|
|
721
682
|
}
|
|
722
683
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
spared_sessions: spared,
|
|
726
|
-
error,
|
|
684
|
+
SessionOwnership::Foreign => {
|
|
685
|
+
push_unique_session(&mut result.spared_sessions, session);
|
|
727
686
|
}
|
|
687
|
+
SessionOwnership::Unknown => {
|
|
688
|
+
push_unique_session(&mut result.spared_sessions, session);
|
|
689
|
+
result.error.get_or_insert_with(|| "session ownership is unknown; destructive cleanup refused".to_string());
|
|
690
|
+
}
|
|
691
|
+
SessionOwnership::Gone => {}
|
|
728
692
|
}
|
|
729
693
|
}
|
|
694
|
+
result
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
698
|
+
enum SessionOwnership {
|
|
699
|
+
Owned,
|
|
700
|
+
Foreign,
|
|
701
|
+
Unknown,
|
|
702
|
+
Gone,
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
fn state_generation(state: &Value) -> Option<&str> {
|
|
706
|
+
state
|
|
707
|
+
.get("generation")
|
|
708
|
+
.and_then(Value::as_str)
|
|
709
|
+
.or_else(|| {
|
|
710
|
+
state
|
|
711
|
+
.get("agents")
|
|
712
|
+
.and_then(Value::as_object)
|
|
713
|
+
.and_then(|agents| {
|
|
714
|
+
agents.values().find_map(|agent| {
|
|
715
|
+
agent.get("spawned_at").and_then(Value::as_str)
|
|
716
|
+
})
|
|
717
|
+
})
|
|
718
|
+
})
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
fn state_for_session<'a>(state: &'a Value, session: &str) -> Option<&'a Value> {
|
|
722
|
+
if state.get("session_name").and_then(Value::as_str) == Some(session) {
|
|
723
|
+
return Some(state);
|
|
724
|
+
}
|
|
725
|
+
state
|
|
726
|
+
.get("teams")
|
|
727
|
+
.and_then(Value::as_object)
|
|
728
|
+
.and_then(|teams| teams.values().find_map(|team| state_for_session(team, session)))
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
fn session_ownership(
|
|
732
|
+
workspace: &Path,
|
|
733
|
+
state: &Value,
|
|
734
|
+
transport: &dyn crate::transport::Transport,
|
|
735
|
+
session: &crate::transport::SessionName,
|
|
736
|
+
targets: &[crate::transport::PaneInfo],
|
|
737
|
+
) -> SessionOwnership {
|
|
738
|
+
let identity_state = state_for_session(state, session.as_str()).unwrap_or(state);
|
|
739
|
+
let session_targets = targets
|
|
740
|
+
.iter()
|
|
741
|
+
.filter(|target| target.session.as_str() == session.as_str())
|
|
742
|
+
.collect::<Vec<_>>();
|
|
743
|
+
if session_targets.is_empty() {
|
|
744
|
+
return SessionOwnership::Gone;
|
|
745
|
+
}
|
|
746
|
+
let owner = match transport.session_owner(session) {
|
|
747
|
+
Ok(Some(owner)) => owner,
|
|
748
|
+
Ok(None) | Err(_) => return SessionOwnership::Unknown,
|
|
749
|
+
};
|
|
750
|
+
let canonical = workspace
|
|
751
|
+
.canonicalize()
|
|
752
|
+
.unwrap_or_else(|_| workspace.to_path_buf())
|
|
753
|
+
.to_string_lossy()
|
|
754
|
+
.to_string();
|
|
755
|
+
if owner.workspace != canonical {
|
|
756
|
+
return SessionOwnership::Foreign;
|
|
757
|
+
}
|
|
758
|
+
let Some(expected_team) = identity_state
|
|
759
|
+
.get("team_key")
|
|
760
|
+
.or_else(|| identity_state.get("active_team_key"))
|
|
761
|
+
.and_then(Value::as_str)
|
|
762
|
+
.filter(|team| !team.is_empty())
|
|
763
|
+
else {
|
|
764
|
+
return SessionOwnership::Unknown;
|
|
765
|
+
};
|
|
766
|
+
if owner.team != expected_team {
|
|
767
|
+
return SessionOwnership::Foreign;
|
|
768
|
+
}
|
|
769
|
+
let Some(expected_generation) = state_generation(identity_state)
|
|
770
|
+
.filter(|generation| !generation.is_empty()) else {
|
|
771
|
+
return SessionOwnership::Unknown;
|
|
772
|
+
};
|
|
773
|
+
if owner.generation != expected_generation {
|
|
774
|
+
return SessionOwnership::Foreign;
|
|
775
|
+
}
|
|
776
|
+
if session_targets.iter().any(|target| {
|
|
777
|
+
target.current_path.as_ref().map_or(true, |path| {
|
|
778
|
+
!path_is_under(path.as_path(), workspace)
|
|
779
|
+
})
|
|
780
|
+
}) {
|
|
781
|
+
return SessionOwnership::Unknown;
|
|
782
|
+
}
|
|
783
|
+
SessionOwnership::Owned
|
|
730
784
|
}
|
|
731
785
|
|
|
732
786
|
#[cfg(test)]
|
|
@@ -762,10 +816,12 @@ pub mod lifecycle_port {
|
|
|
762
816
|
leader_env: BTreeMap::new(),
|
|
763
817
|
}]);
|
|
764
818
|
let cleanup = bare_shutdown_socket_cleanup(
|
|
819
|
+
Path::new("/tmp/team-agent-test"),
|
|
765
820
|
&transport,
|
|
766
821
|
&json!({
|
|
767
822
|
"is_external_leader": true,
|
|
768
823
|
"tmux_socket_source": "leader_env",
|
|
824
|
+
"session_name": "team-audit-fail",
|
|
769
825
|
}),
|
|
770
826
|
&event_log,
|
|
771
827
|
);
|
|
@@ -777,7 +833,7 @@ pub mod lifecycle_port {
|
|
|
777
833
|
assert!(cleanup
|
|
778
834
|
.error
|
|
779
835
|
.as_deref()
|
|
780
|
-
.is_some_and(|error| error.contains("
|
|
836
|
+
.is_some_and(|error| error.contains("ownership is unknown")));
|
|
781
837
|
let _ = std::fs::remove_dir_all(root);
|
|
782
838
|
}
|
|
783
839
|
}
|
|
@@ -786,106 +842,6 @@ pub mod lifecycle_port {
|
|
|
786
842
|
crate::state::projection::state_is_external_leader(state)
|
|
787
843
|
}
|
|
788
844
|
|
|
789
|
-
fn managed_leader_socket_cleanup(
|
|
790
|
-
transport: &dyn crate::transport::Transport,
|
|
791
|
-
state: &Value,
|
|
792
|
-
sessions: &[crate::transport::SessionName],
|
|
793
|
-
event_log: &crate::event_log::EventLog,
|
|
794
|
-
) -> ShutdownSocketCleanup {
|
|
795
|
-
let target = state
|
|
796
|
-
.get("session_name")
|
|
797
|
-
.and_then(Value::as_str)
|
|
798
|
-
.filter(|session| !session.is_empty())
|
|
799
|
-
.map(crate::transport::SessionName::new);
|
|
800
|
-
// E49 (0.3.24 P0, shutdown kills leader CLI): for managed leader topology
|
|
801
|
-
// the target session may carry the leader anchor pane. The pre-fix code
|
|
802
|
-
// pushed it into `to_kill` and then issued `kill_session` — which ended
|
|
803
|
-
// the leader pane (= leader CLI). When the target session has a live
|
|
804
|
-
// anchor pane in `list_targets`, spare it instead.
|
|
805
|
-
let leader_anchor_ids = collect_state_leader_anchor_pane_ids(state);
|
|
806
|
-
let live_targets = transport.list_targets().unwrap_or_default();
|
|
807
|
-
let mut to_kill = Vec::new();
|
|
808
|
-
let mut target_spared_for_anchor: Option<crate::transport::SessionName> = None;
|
|
809
|
-
if let Some(target) = target {
|
|
810
|
-
let target_has_anchor = live_targets.iter().any(|t| {
|
|
811
|
-
t.session.as_str() == target.as_str()
|
|
812
|
-
&& leader_anchor_ids.contains(t.pane_id.as_str())
|
|
813
|
-
});
|
|
814
|
-
if target_has_anchor {
|
|
815
|
-
target_spared_for_anchor = Some(target);
|
|
816
|
-
} else if sessions.is_empty()
|
|
817
|
-
|| sessions
|
|
818
|
-
.iter()
|
|
819
|
-
.any(|session| session.as_str() == target.as_str())
|
|
820
|
-
{
|
|
821
|
-
to_kill.push(target);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
let mut spared = sessions
|
|
825
|
-
.iter()
|
|
826
|
-
.filter(|session| {
|
|
827
|
-
!to_kill
|
|
828
|
-
.iter()
|
|
829
|
-
.any(|target| target.as_str() == session.as_str())
|
|
830
|
-
})
|
|
831
|
-
.cloned()
|
|
832
|
-
.collect::<Vec<_>>();
|
|
833
|
-
if let Some(anchored) = target_spared_for_anchor {
|
|
834
|
-
if !spared.iter().any(|s| s.as_str() == anchored.as_str()) {
|
|
835
|
-
spared.push(anchored);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
let _ = event_log.write(
|
|
839
|
-
"shutdown.kill_server_skipped_managed_leader",
|
|
840
|
-
json!({
|
|
841
|
-
"reason": "managed_leader_topology",
|
|
842
|
-
"spared_sessions": spared.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
|
|
843
|
-
"killed_sessions": to_kill.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
|
|
844
|
-
}),
|
|
845
|
-
);
|
|
846
|
-
let mut error = None;
|
|
847
|
-
for session in &to_kill {
|
|
848
|
-
// 0.3.28 Step 6 (warn-only invariant): the kill list MUST NOT
|
|
849
|
-
// contain a leader session. `sessions_to_kill` already excludes
|
|
850
|
-
// leader-prefixed sessions, but the managed-leader cleanup path
|
|
851
|
-
// (this function) computes its own `to_kill` from `target`. Any
|
|
852
|
-
// leader-prefixed entry here is a topology violation introduced
|
|
853
|
-
// somewhere upstream. Log loudly and skip the kill.
|
|
854
|
-
if session
|
|
855
|
-
.as_str()
|
|
856
|
-
.starts_with(crate::leader::LEADER_SESSION_PREFIX)
|
|
857
|
-
{
|
|
858
|
-
eprintln!(
|
|
859
|
-
"team_agent::layout shutdown_invariant_violation kind=KillListContainsLeaderSession \
|
|
860
|
-
session=`{}` action=skipping_kill (post-Step-9 will hard-fail)",
|
|
861
|
-
session.as_str()
|
|
862
|
-
);
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
let targets = vec![session.as_str().to_string()];
|
|
866
|
-
if let Err(audit_error) = crate::kill_audit::pre_kill_audit(
|
|
867
|
-
event_log,
|
|
868
|
-
transport,
|
|
869
|
-
"shutdown.managed_worker_session",
|
|
870
|
-
crate::kill_audit::KILL_SESSION,
|
|
871
|
-
&targets,
|
|
872
|
-
) {
|
|
873
|
-
error.get_or_insert_with(|| format!("pre-kill audit failed: {audit_error}"));
|
|
874
|
-
continue;
|
|
875
|
-
}
|
|
876
|
-
if let Err(err) = transport.kill_session(session) {
|
|
877
|
-
if !tmux_absent_error(&err.to_string()) {
|
|
878
|
-
error.get_or_insert_with(|| err.to_string());
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
ShutdownSocketCleanup {
|
|
883
|
-
killed_sessions: to_kill,
|
|
884
|
-
spared_sessions: spared,
|
|
885
|
-
error,
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
845
|
fn owned_shutdown_endpoint(
|
|
890
846
|
workspace: &Path,
|
|
891
847
|
state: &Value,
|
|
@@ -970,40 +926,15 @@ pub mod lifecycle_port {
|
|
|
970
926
|
);
|
|
971
927
|
return cleanup;
|
|
972
928
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
crate::kill_audit::KILL_SERVER,
|
|
978
|
-
&[],
|
|
979
|
-
) {
|
|
980
|
-
cleanup.error = Some(format!("pre-kill audit failed: {error}"));
|
|
981
|
-
return cleanup;
|
|
982
|
-
}
|
|
983
|
-
if let Err(error) = transport.kill_server() {
|
|
984
|
-
cleanup.error = Some(error.to_string());
|
|
985
|
-
}
|
|
986
|
-
if let Some(path) = endpoint.socket_file.as_ref() {
|
|
987
|
-
if path.exists() {
|
|
988
|
-
match std::fs::remove_file(path) {
|
|
989
|
-
Ok(()) => cleanup.removed_files.push(path.display().to_string()),
|
|
990
|
-
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
|
991
|
-
Err(error) => {
|
|
992
|
-
cleanup.error.get_or_insert_with(|| error.to_string());
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
if path.exists() {
|
|
997
|
-
cleanup.residual_files.push(path.display().to_string());
|
|
998
|
-
}
|
|
999
|
-
}
|
|
929
|
+
// Endpoint ownership is not sufficient authority to tear down a tmux
|
|
930
|
+
// server: an endpoint can be shared after launch. Leave both the
|
|
931
|
+
// server and its socket file intact; only explicitly owned sessions
|
|
932
|
+
// are removed above.
|
|
1000
933
|
let _ = event_log.write(
|
|
1001
|
-
"shutdown.
|
|
934
|
+
"shutdown.owned_endpoint_cleanup_skipped",
|
|
1002
935
|
json!({
|
|
1003
936
|
"endpoint": endpoint.endpoint,
|
|
1004
|
-
"
|
|
1005
|
-
"residual_files": cleanup.residual_files.clone(),
|
|
1006
|
-
"error": cleanup.error.clone(),
|
|
937
|
+
"reason": "server_and_socket_cleanup_disabled_fail_closed",
|
|
1007
938
|
}),
|
|
1008
939
|
);
|
|
1009
940
|
cleanup
|
|
@@ -1081,10 +1012,18 @@ pub mod lifecycle_port {
|
|
|
1081
1012
|
root_pids.extend(pane_pids);
|
|
1082
1013
|
root_pids.sort_unstable();
|
|
1083
1014
|
root_pids.dedup();
|
|
1084
|
-
let root_pgids = process_pgids(&root_pids, &protected, &entry_table);
|
|
1085
1015
|
deadline.check("reap_process_tree")?;
|
|
1086
|
-
|
|
1087
|
-
|
|
1016
|
+
let process_event_log = crate::event_log::EventLog::new(&run_workspace);
|
|
1017
|
+
reap_process_tree(
|
|
1018
|
+
&run_workspace,
|
|
1019
|
+
&state,
|
|
1020
|
+
reap_scope,
|
|
1021
|
+
&root_pids,
|
|
1022
|
+
&protected,
|
|
1023
|
+
&entry_table,
|
|
1024
|
+
&process_event_log,
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1088
1027
|
let mut kill_error: Option<String> = None;
|
|
1089
1028
|
let mut killed_sessions = Vec::new();
|
|
1090
1029
|
let mut spared_sessions = Vec::new();
|
|
@@ -1100,57 +1039,111 @@ pub mod lifecycle_port {
|
|
|
1100
1039
|
// session per `list_targets`. When managed-leader, switch to per-pane
|
|
1101
1040
|
// cleanup: kill the workers individually and SPARE the leader's pane.
|
|
1102
1041
|
//
|
|
1103
|
-
// External
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1042
|
+
// External and managed leaders both pass the same positive ownership
|
|
1043
|
+
// gate; topology only decides whether a verified session is reduced
|
|
1044
|
+
// to worker panes or removed as a whole.
|
|
1106
1045
|
let leader_anchor_ids = collect_state_leader_anchor_pane_ids(&state);
|
|
1107
|
-
let live_targets_now = transport.list_targets()
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1046
|
+
let (live_targets_now, live_targets_known) = match transport.list_targets() {
|
|
1047
|
+
Ok(targets) => (targets, true),
|
|
1048
|
+
Err(error) => {
|
|
1049
|
+
push_unique_session(&mut spared_sessions, session.clone());
|
|
1050
|
+
kill_error.get_or_insert_with(|| format!("session ownership probe failed: {error}"));
|
|
1051
|
+
(Vec::new(), false)
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
let ownership = if live_targets_known {
|
|
1055
|
+
session_ownership(
|
|
1056
|
+
&run_workspace,
|
|
1118
1057
|
&state,
|
|
1119
|
-
|
|
1120
|
-
|
|
1058
|
+
transport,
|
|
1059
|
+
session,
|
|
1121
1060
|
&live_targets_now,
|
|
1122
|
-
)
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1061
|
+
)
|
|
1062
|
+
} else {
|
|
1063
|
+
SessionOwnership::Unknown
|
|
1064
|
+
};
|
|
1065
|
+
if ownership != SessionOwnership::Owned {
|
|
1066
|
+
push_unique_session(&mut spared_sessions, session.clone());
|
|
1067
|
+
if ownership == SessionOwnership::Unknown {
|
|
1068
|
+
kill_error.get_or_insert_with(|| "session ownership is unknown; destructive cleanup refused".to_string());
|
|
1129
1069
|
}
|
|
1130
|
-
let _ =
|
|
1131
|
-
crate::lifecycle::display::close_team_display_backends(&run_workspace, session);
|
|
1132
1070
|
} else {
|
|
1133
|
-
let
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
&
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1071
|
+
let session_has_leader_anchor = live_targets_now.iter().any(|target| {
|
|
1072
|
+
target.session.as_str() == session.as_str()
|
|
1073
|
+
&& leader_anchor_ids.contains(target.pane_id.as_str())
|
|
1074
|
+
});
|
|
1075
|
+
if crate::state::projection::state_is_managed_leader(&state)
|
|
1076
|
+
&& session_has_leader_anchor
|
|
1077
|
+
{
|
|
1078
|
+
let _ = event_log_write_session_spared(&run_workspace, session, &leader_anchor_ids);
|
|
1079
|
+
push_unique_session(&mut spared_sessions, session.clone());
|
|
1080
|
+
let worker_panes = collect_session_worker_panes(
|
|
1081
|
+
&state,
|
|
1082
|
+
session.as_str(),
|
|
1083
|
+
&leader_anchor_ids,
|
|
1084
|
+
&live_targets_now,
|
|
1085
|
+
);
|
|
1086
|
+
let event_log = crate::event_log::EventLog::new(&run_workspace);
|
|
1087
|
+
let identity_state = state_for_session(&state, session.as_str()).unwrap_or(&state);
|
|
1088
|
+
for pane in &worker_panes {
|
|
1089
|
+
let pane_target = vec![pane.as_str().to_string()];
|
|
1090
|
+
let audit = crate::kill_audit::pre_kill_audit_scoped(
|
|
1091
|
+
&event_log,
|
|
1092
|
+
transport,
|
|
1093
|
+
"shutdown.managed_worker_pane",
|
|
1094
|
+
"kill-pane",
|
|
1095
|
+
&pane_target,
|
|
1096
|
+
&run_workspace,
|
|
1097
|
+
identity_state.get("team_key").and_then(Value::as_str),
|
|
1098
|
+
state_generation(identity_state),
|
|
1099
|
+
"session_marker+state_worker_pane+pane_path",
|
|
1100
|
+
"positive workspace/team ownership",
|
|
1101
|
+
);
|
|
1102
|
+
if let Err(error) = audit {
|
|
1103
|
+
kill_error.get_or_insert_with(|| format!("pre-kill audit failed: {error}"));
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
if let Err(error) = transport.kill_pane(pane) {
|
|
1107
|
+
if !tmux_absent_error(&error.to_string()) {
|
|
1108
|
+
kill_error.get_or_insert_with(|| error.to_string());
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1148
1111
|
}
|
|
1112
|
+
let _ = crate::lifecycle::display::close_team_display_backends(
|
|
1113
|
+
&run_workspace,
|
|
1114
|
+
session,
|
|
1115
|
+
);
|
|
1149
1116
|
} else {
|
|
1150
|
-
|
|
1117
|
+
let targets = vec![session.as_str().to_string()];
|
|
1118
|
+
let event_log = crate::event_log::EventLog::new(&run_workspace);
|
|
1119
|
+
if let Err(error) = crate::kill_audit::pre_kill_audit_scoped(
|
|
1120
|
+
&event_log,
|
|
1121
|
+
transport,
|
|
1122
|
+
"shutdown.team_session",
|
|
1123
|
+
crate::kill_audit::KILL_SESSION,
|
|
1124
|
+
&targets,
|
|
1125
|
+
&run_workspace,
|
|
1126
|
+
state_for_session(&state, session.as_str())
|
|
1127
|
+
.and_then(|identity| identity.get("team_key").and_then(Value::as_str)),
|
|
1128
|
+
state_for_session(&state, session.as_str()).and_then(state_generation),
|
|
1129
|
+
"session_marker+state_session+pane_path",
|
|
1130
|
+
"positive workspace/team ownership",
|
|
1131
|
+
) {
|
|
1132
|
+
kill_error = Some(format!("pre-kill audit failed: {error}"));
|
|
1133
|
+
} else if let Err(error) = transport.kill_session(session) {
|
|
1134
|
+
if tmux_absent_error(&error.to_string()) {
|
|
1135
|
+
push_unique_session(&mut killed_sessions, session.clone());
|
|
1136
|
+
} else {
|
|
1137
|
+
kill_error = Some(error.to_string());
|
|
1138
|
+
}
|
|
1139
|
+
} else {
|
|
1140
|
+
push_unique_session(&mut killed_sessions, session.clone());
|
|
1141
|
+
}
|
|
1142
|
+
let _ = crate::lifecycle::display::close_team_display_backends(
|
|
1143
|
+
&run_workspace,
|
|
1144
|
+
session,
|
|
1145
|
+
);
|
|
1151
1146
|
}
|
|
1152
|
-
let _ =
|
|
1153
|
-
crate::lifecycle::display::close_team_display_backends(&run_workspace, session);
|
|
1154
1147
|
}
|
|
1155
1148
|
}
|
|
1156
1149
|
deadline.check("reap_workspace_residuals")?;
|
|
@@ -1158,7 +1151,6 @@ pub mod lifecycle_port {
|
|
|
1158
1151
|
&run_workspace,
|
|
1159
1152
|
&state,
|
|
1160
1153
|
&root_pids,
|
|
1161
|
-
&root_pgids,
|
|
1162
1154
|
transport,
|
|
1163
1155
|
reap_scope,
|
|
1164
1156
|
&mut probe_degraded,
|
|
@@ -1166,7 +1158,7 @@ pub mod lifecycle_port {
|
|
|
1166
1158
|
if team.is_none() {
|
|
1167
1159
|
deadline.check("shared_socket_cleanup")?;
|
|
1168
1160
|
let event_log = crate::event_log::EventLog::new(&run_workspace);
|
|
1169
|
-
let cleanup = bare_shutdown_socket_cleanup(transport, &state, &event_log);
|
|
1161
|
+
let cleanup = bare_shutdown_socket_cleanup(&run_workspace, transport, &state, &event_log);
|
|
1170
1162
|
for session in cleanup.killed_sessions {
|
|
1171
1163
|
push_unique_session(&mut killed_sessions, session);
|
|
1172
1164
|
}
|
|
@@ -1175,6 +1167,7 @@ pub mod lifecycle_port {
|
|
|
1175
1167
|
kill_error.get_or_insert(error);
|
|
1176
1168
|
}
|
|
1177
1169
|
}
|
|
1170
|
+
spared_sessions.retain(|session| transport.has_session(session).unwrap_or(true));
|
|
1178
1171
|
deadline.check("session_residuals")?;
|
|
1179
1172
|
let (session_residuals, session_residual_error) = session_residuals_after_reap_many(
|
|
1180
1173
|
transport,
|
|
@@ -1208,7 +1201,6 @@ pub mod lifecycle_port {
|
|
|
1208
1201
|
&run_workspace,
|
|
1209
1202
|
&state,
|
|
1210
1203
|
&root_pids,
|
|
1211
|
-
&root_pgids,
|
|
1212
1204
|
&protected,
|
|
1213
1205
|
reap_scope,
|
|
1214
1206
|
&verify_table,
|
|
@@ -1838,12 +1830,25 @@ pub mod lifecycle_port {
|
|
|
1838
1830
|
/// pids; Gap 37 escalation order TERM -> grace -> KILL preserved), then a single
|
|
1839
1831
|
/// bounded wait for the whole union. kill/wait sets derive from the SAME snapshot
|
|
1840
1832
|
/// as the protected set (N39).
|
|
1841
|
-
fn reap_process_tree(
|
|
1833
|
+
fn reap_process_tree(
|
|
1834
|
+
workspace: &Path,
|
|
1835
|
+
state: &Value,
|
|
1836
|
+
scope: ShutdownReapScope,
|
|
1837
|
+
root_pids: &[u32],
|
|
1838
|
+
protected: &ShutdownProtection,
|
|
1839
|
+
table: &[ProcessInfo],
|
|
1840
|
+
event_log: &crate::event_log::EventLog,
|
|
1841
|
+
) {
|
|
1842
1842
|
let mut pids = Vec::new();
|
|
1843
1843
|
let mut seen = std::collections::BTreeSet::new();
|
|
1844
|
+
let spawn_cwds = state_spawn_cwds(state, scope);
|
|
1844
1845
|
for root in root_pids {
|
|
1845
1846
|
for pid in process_tree_from_table(*root, table) {
|
|
1846
|
-
|
|
1847
|
+
let owned = table
|
|
1848
|
+
.iter()
|
|
1849
|
+
.find(|process| process.pid == pid)
|
|
1850
|
+
.is_some_and(|process| process_matches_workspace(process, workspace, &spawn_cwds));
|
|
1851
|
+
if owned && !protected.contains_pid(pid) && seen.insert(pid) {
|
|
1847
1852
|
pids.push(pid);
|
|
1848
1853
|
}
|
|
1849
1854
|
}
|
|
@@ -1851,50 +1856,58 @@ pub mod lifecycle_port {
|
|
|
1851
1856
|
if pids.is_empty() {
|
|
1852
1857
|
return;
|
|
1853
1858
|
}
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
+
let audit_targets = pids.iter().map(u32::to_string).collect::<Vec<_>>();
|
|
1860
|
+
if event_log
|
|
1861
|
+
.write(
|
|
1862
|
+
"transport.pre_kill_audit",
|
|
1863
|
+
json!({
|
|
1864
|
+
"phase": "pre_call",
|
|
1865
|
+
"caller": "shutdown.process_tree",
|
|
1866
|
+
"caller_pid": std::process::id(),
|
|
1867
|
+
"action": "signal-pid",
|
|
1868
|
+
"endpoint": Value::Null,
|
|
1869
|
+
"targets": audit_targets,
|
|
1870
|
+
"workspace": workspace.to_string_lossy(),
|
|
1871
|
+
"team": state.get("team_key").and_then(Value::as_str),
|
|
1872
|
+
"generation": state_generation(state),
|
|
1873
|
+
"owner_evidence": "argv+cwd+state_pid_tree",
|
|
1874
|
+
"reason": "positive per-pid process ownership",
|
|
1875
|
+
}),
|
|
1876
|
+
)
|
|
1877
|
+
.is_err()
|
|
1878
|
+
{
|
|
1879
|
+
return;
|
|
1859
1880
|
}
|
|
1860
|
-
std::thread::sleep(std::time::Duration::from_millis(150));
|
|
1861
1881
|
for pid in pids.iter().rev() {
|
|
1862
1882
|
let _ = crate::platform::process::terminate_pid(
|
|
1863
1883
|
*pid,
|
|
1864
|
-
crate::platform::process::SignalKind::TerminateForce,
|
|
1865
|
-
);
|
|
1866
|
-
}
|
|
1867
|
-
wait_for_processes_gone(&pids, std::time::Duration::from_secs(1));
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
fn reap_process_groups(pgids: &[u32], protected: &ShutdownProtection) {
|
|
1871
|
-
// 0.5.x Windows portability Batch 3: routes group termination
|
|
1872
|
-
// through `platform::process::terminate_group`. Unix keeps
|
|
1873
|
-
// `kill(-pgid, SIGTERM|SIGKILL)` semantics byte-for-byte;
|
|
1874
|
-
// Windows returns AlreadyGone (no pgid concept — Job Object
|
|
1875
|
-
// teardown is the shim-side concern per design §Route B).
|
|
1876
|
-
// The `pgid_t <= 1 || protected.contains_pgid(...)` filter
|
|
1877
|
-
// stays in place so pgid 0/1 (kernel/init) and the caller's
|
|
1878
|
-
// own group are never targeted.
|
|
1879
|
-
for pgid in pgids {
|
|
1880
|
-
if *pgid <= 1 || protected.contains_pgid(*pgid) {
|
|
1881
|
-
continue;
|
|
1882
|
-
}
|
|
1883
|
-
let _ = crate::platform::process::terminate_group(
|
|
1884
|
-
*pgid,
|
|
1885
1884
|
crate::platform::process::SignalKind::TerminateGraceful,
|
|
1886
1885
|
);
|
|
1887
1886
|
}
|
|
1888
1887
|
std::thread::sleep(std::time::Duration::from_millis(150));
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1888
|
+
// Re-probe identity before escalation. If the probe fails, refuse the
|
|
1889
|
+
// force signal rather than risking a recycled PID.
|
|
1890
|
+
if let Ok(live_table) = probed_process_table() {
|
|
1891
|
+
let spawn_cwds = state_spawn_cwds(state, scope);
|
|
1892
|
+
for pid in pids.iter().rev() {
|
|
1893
|
+
let Some(before) = table.iter().find(|process| process.pid == *pid) else {
|
|
1894
|
+
continue;
|
|
1895
|
+
};
|
|
1896
|
+
let Some(after) = live_table.iter().find(|process| process.pid == *pid) else {
|
|
1897
|
+
continue;
|
|
1898
|
+
};
|
|
1899
|
+
if before.ppid == after.ppid
|
|
1900
|
+
&& before.command == after.command
|
|
1901
|
+
&& process_matches_workspace(after, workspace, &spawn_cwds)
|
|
1902
|
+
{
|
|
1903
|
+
let _ = crate::platform::process::terminate_pid(
|
|
1904
|
+
*pid,
|
|
1905
|
+
crate::platform::process::SignalKind::TerminateForce,
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1892
1908
|
}
|
|
1893
|
-
let _ = crate::platform::process::terminate_group(
|
|
1894
|
-
*pgid,
|
|
1895
|
-
crate::platform::process::SignalKind::TerminateForce,
|
|
1896
|
-
);
|
|
1897
1909
|
}
|
|
1910
|
+
wait_for_processes_gone(&pids, std::time::Duration::from_secs(1));
|
|
1898
1911
|
}
|
|
1899
1912
|
|
|
1900
1913
|
/// PERF-6 C-①-2 + C-②-5: every residual round fetches ONE fresh snapshot (reap
|
|
@@ -1904,7 +1917,6 @@ pub mod lifecycle_port {
|
|
|
1904
1917
|
workspace: &Path,
|
|
1905
1918
|
state: &Value,
|
|
1906
1919
|
root_pids: &[u32],
|
|
1907
|
-
root_pgids: &[u32],
|
|
1908
1920
|
transport: &dyn crate::transport::Transport,
|
|
1909
1921
|
scope: ShutdownReapScope,
|
|
1910
1922
|
probe_degraded: &mut bool,
|
|
@@ -1917,7 +1929,6 @@ pub mod lifecycle_port {
|
|
|
1917
1929
|
workspace,
|
|
1918
1930
|
state,
|
|
1919
1931
|
root_pids,
|
|
1920
|
-
root_pgids,
|
|
1921
1932
|
&protected,
|
|
1922
1933
|
scope,
|
|
1923
1934
|
&round_table,
|
|
@@ -1929,12 +1940,15 @@ pub mod lifecycle_port {
|
|
|
1929
1940
|
.iter()
|
|
1930
1941
|
.map(|process| process.pid)
|
|
1931
1942
|
.collect::<Vec<_>>();
|
|
1932
|
-
reap_process_tree(
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1943
|
+
reap_process_tree(
|
|
1944
|
+
workspace,
|
|
1945
|
+
state,
|
|
1946
|
+
scope,
|
|
1947
|
+
&residual_pids,
|
|
1948
|
+
&protected,
|
|
1949
|
+
&round_table,
|
|
1950
|
+
&crate::event_log::EventLog::new(workspace),
|
|
1951
|
+
);
|
|
1938
1952
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
1939
1953
|
}
|
|
1940
1954
|
}
|
|
@@ -2116,9 +2130,9 @@ pub mod lifecycle_port {
|
|
|
2116
2130
|
if leader_anchor_ids.contains(pane_id) {
|
|
2117
2131
|
continue;
|
|
2118
2132
|
}
|
|
2119
|
-
let in_live = live_targets
|
|
2120
|
-
.
|
|
2121
|
-
|
|
2133
|
+
let in_live = live_targets.iter().any(|info| {
|
|
2134
|
+
info.pane_id.as_str() == pane_id && info.session.as_str() == session
|
|
2135
|
+
});
|
|
2122
2136
|
if in_live && seen.insert(pane_id.to_string()) {
|
|
2123
2137
|
out.push(crate::transport::PaneId::new(pane_id));
|
|
2124
2138
|
continue;
|
|
@@ -2386,44 +2400,16 @@ pub mod lifecycle_port {
|
|
|
2386
2400
|
}
|
|
2387
2401
|
}
|
|
2388
2402
|
|
|
2389
|
-
fn process_pgids(
|
|
2390
|
-
pids: &[u32],
|
|
2391
|
-
protected: &ShutdownProtection,
|
|
2392
|
-
table: &[ProcessInfo],
|
|
2393
|
-
) -> Vec<u32> {
|
|
2394
|
-
let mut pgids = pids
|
|
2395
|
-
.iter()
|
|
2396
|
-
.filter_map(|pid| table.iter().find(|process| process.pid == *pid))
|
|
2397
|
-
.filter_map(|process| process.pgid)
|
|
2398
|
-
.filter(|pgid| {
|
|
2399
|
-
// 0.5.x Windows portability Batch 4: `libc::pid_t` was
|
|
2400
|
-
// used here as a signed-integer conversion gate to
|
|
2401
|
-
// reject values > INT_MAX. Replace with the equivalent
|
|
2402
|
-
// `i32::try_from` (pgid_t is `c_int` on every Unix we
|
|
2403
|
-
// support). Windows has no pgid concept so `pgids`
|
|
2404
|
-
// is empty in practice — the filter is dead code on
|
|
2405
|
-
// Windows but must still compile.
|
|
2406
|
-
i32::try_from(*pgid)
|
|
2407
|
-
.map(|pgid_int| pgid_int > 1 && !protected.contains_pgid(*pgid))
|
|
2408
|
-
.unwrap_or(false)
|
|
2409
|
-
})
|
|
2410
|
-
.collect::<Vec<_>>();
|
|
2411
|
-
pgids.sort_unstable();
|
|
2412
|
-
pgids.dedup();
|
|
2413
|
-
pgids
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
2403
|
fn process_residuals(
|
|
2417
2404
|
workspace: &Path,
|
|
2418
2405
|
state: &Value,
|
|
2419
2406
|
root_pids: &[u32],
|
|
2420
|
-
root_pgids: &[u32],
|
|
2421
2407
|
protected: &ShutdownProtection,
|
|
2422
2408
|
scope: ShutdownReapScope,
|
|
2423
2409
|
table: &[ProcessInfo],
|
|
2424
2410
|
) -> Vec<Value> {
|
|
2425
2411
|
let mut residuals = matched_processes(
|
|
2426
|
-
workspace, state, root_pids,
|
|
2412
|
+
workspace, state, root_pids, protected, scope, table,
|
|
2427
2413
|
);
|
|
2428
2414
|
let mut seen = residuals
|
|
2429
2415
|
.iter()
|
|
@@ -2461,7 +2447,6 @@ pub mod lifecycle_port {
|
|
|
2461
2447
|
workspace: &Path,
|
|
2462
2448
|
state: &Value,
|
|
2463
2449
|
root_pids: &[u32],
|
|
2464
|
-
root_pgids: &[u32],
|
|
2465
2450
|
protected: &ShutdownProtection,
|
|
2466
2451
|
scope: ShutdownReapScope,
|
|
2467
2452
|
table: &[ProcessInfo],
|
|
@@ -2471,33 +2456,14 @@ pub mod lifecycle_port {
|
|
|
2471
2456
|
.flat_map(|pid| process_tree_from_table(*pid, table))
|
|
2472
2457
|
.filter(|pid| !protected.contains_pid(*pid))
|
|
2473
2458
|
.collect::<std::collections::BTreeSet<_>>();
|
|
2474
|
-
let root_pgids = root_pgids
|
|
2475
|
-
.iter()
|
|
2476
|
-
.copied()
|
|
2477
|
-
.collect::<std::collections::BTreeSet<_>>();
|
|
2478
2459
|
let spawn_cwds = state_spawn_cwds(state, scope);
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
let matches_workspace = scope == ShutdownReapScope::Workspace
|
|
2487
|
-
&& process_matches_workspace(
|
|
2488
|
-
process,
|
|
2489
|
-
&workspace_text,
|
|
2490
|
-
&spawn_cwds,
|
|
2491
|
-
&mut cwd_probe_budget,
|
|
2492
|
-
);
|
|
2493
|
-
if matches_workspace
|
|
2494
|
-
|| root_tree.contains(&process.pid)
|
|
2495
|
-
|| process.pgid.is_some_and(|pgid| root_pgids.contains(&pgid))
|
|
2496
|
-
{
|
|
2497
|
-
out.push(process.clone());
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
out
|
|
2460
|
+
table
|
|
2461
|
+
.iter()
|
|
2462
|
+
.filter(|process| !protected.contains_pid(process.pid))
|
|
2463
|
+
.filter(|process| root_tree.contains(&process.pid))
|
|
2464
|
+
.filter(|process| process_matches_workspace(process, workspace, &spawn_cwds))
|
|
2465
|
+
.cloned()
|
|
2466
|
+
.collect()
|
|
2501
2467
|
}
|
|
2502
2468
|
|
|
2503
2469
|
fn process_tree_from_table(root_pid: u32, table: &[ProcessInfo]) -> Vec<u32> {
|
|
@@ -2550,59 +2516,21 @@ pub mod lifecycle_port {
|
|
|
2550
2516
|
|
|
2551
2517
|
fn process_matches_workspace(
|
|
2552
2518
|
process: &ProcessInfo,
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
cwd_probe_budget: &mut usize,
|
|
2519
|
+
workspace: &Path,
|
|
2520
|
+
_spawn_cwds: &[PathBuf],
|
|
2556
2521
|
) -> bool {
|
|
2557
|
-
let
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
}
|
|
2570
|
-
*cwd_probe_budget -= 1;
|
|
2571
|
-
let Some(cwd) = process_cwd(process.pid) else {
|
|
2572
|
-
return false;
|
|
2573
|
-
};
|
|
2574
|
-
spawn_cwds
|
|
2575
|
-
.iter()
|
|
2576
|
-
.any(|spawn_cwd| path_is_under(&cwd, spawn_cwd))
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
fn process_cwd(pid: u32) -> Option<PathBuf> {
|
|
2580
|
-
let proc_cwd = PathBuf::from(format!("/proc/{pid}/cwd"));
|
|
2581
|
-
if let Ok(path) = std::fs::read_link(proc_cwd) {
|
|
2582
|
-
return Some(path);
|
|
2583
|
-
}
|
|
2584
|
-
if crate::os_probe::probe_timed_out() {
|
|
2585
|
-
return None;
|
|
2586
|
-
}
|
|
2587
|
-
let output = crate::os_probe::bounded_command_output_with_probe(
|
|
2588
|
-
std::process::Command::new("lsof").args([
|
|
2589
|
-
"-a",
|
|
2590
|
-
"-p",
|
|
2591
|
-
&pid.to_string(),
|
|
2592
|
-
"-d",
|
|
2593
|
-
"cwd",
|
|
2594
|
-
"-Fn",
|
|
2595
|
-
]),
|
|
2596
|
-
"lsof_cwd",
|
|
2597
|
-
Some(pid),
|
|
2598
|
-
)
|
|
2599
|
-
.ok()?;
|
|
2600
|
-
if !output.status.success() {
|
|
2601
|
-
return None;
|
|
2602
|
-
}
|
|
2603
|
-
String::from_utf8_lossy(&output.stdout)
|
|
2604
|
-
.lines()
|
|
2605
|
-
.find_map(|line| line.strip_prefix('n').map(PathBuf::from))
|
|
2522
|
+
let workspace = workspace
|
|
2523
|
+
.canonicalize()
|
|
2524
|
+
.unwrap_or_else(|_| workspace.to_path_buf());
|
|
2525
|
+
crate::platform::argv::argv_tokens(process.pid).is_some_and(|tokens| {
|
|
2526
|
+
tokens.windows(2).any(|pair| {
|
|
2527
|
+
pair[0] == "--workspace"
|
|
2528
|
+
&& Path::new(&pair[1])
|
|
2529
|
+
.canonicalize()
|
|
2530
|
+
.map(|path| path == workspace)
|
|
2531
|
+
.unwrap_or(false)
|
|
2532
|
+
})
|
|
2533
|
+
})
|
|
2606
2534
|
}
|
|
2607
2535
|
|
|
2608
2536
|
fn path_is_under(path: &Path, root: &Path) -> bool {
|
|
@@ -4488,6 +4416,7 @@ pub mod lifecycle_port {
|
|
|
4488
4416
|
match status {
|
|
4489
4417
|
crate::coordinator::StopOutcome::Missing => "missing",
|
|
4490
4418
|
crate::coordinator::StopOutcome::InvalidPidRemoved => "invalid_pid_removed",
|
|
4419
|
+
crate::coordinator::StopOutcome::NotOwned => "not_owned",
|
|
4491
4420
|
crate::coordinator::StopOutcome::KillFailed => "kill_failed",
|
|
4492
4421
|
crate::coordinator::StopOutcome::Stopped => "stopped",
|
|
4493
4422
|
}
|