Skip to content

Commit b6c834a

Browse files
committed
send PGID in SignalMessage to prevent double signaling
1 parent 10e52bd commit b6c834a

File tree

5 files changed

+133
-21
lines changed

5 files changed

+133
-21
lines changed

src/shell/commands/executable.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ impl ShellCommand for ExecutableCommand {
6868
// avoid deadlock since this is holding onto the pipes
6969
drop(sub_command);
7070

71+
// Get the child's PGID for signal forwarding decisions
72+
#[cfg(unix)]
73+
let child_pgid = kill_signal.current_child_pgid();
74+
7175
loop {
7276
tokio::select! {
7377
result = child.wait() => match result {
@@ -88,14 +92,16 @@ impl ShellCommand for ExecutableCommand {
8892
signal = kill_signal.wait_any() => {
8993
if let Some(_id) = child.id() {
9094
#[cfg(unix)]
91-
kill(_id as i32, signal);
95+
if signal.should_forward_to(child_pgid) {
96+
kill(_id as i32, signal.kind);
97+
}
9298

93-
if cfg!(not(unix)) && signal.causes_abort() {
99+
if cfg!(not(unix)) && signal.kind.causes_abort() {
94100
let _ = child.start_kill();
95101
let status = child.wait().await.ok();
96102
kill_signal.clear_child_process();
97103
return ExecuteResult::from_exit_code(
98-
status.and_then(|s| s.code()).unwrap_or(signal.aborted_code()),
104+
status.and_then(|s| s.code()).unwrap_or(signal.kind.aborted_code()),
99105
);
100106
}
101107
}

src/shell/commands/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ macro_rules! execute_with_cancellation {
131131
result
132132
},
133133
signal = $kill_signal.wait_aborted() => {
134-
ExecuteResult::from_exit_code(signal.aborted_code())
134+
ExecuteResult::from_exit_code(signal.kind.aborted_code())
135135
}
136136
}
137137
};

src/shell/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub use types::ShellPipeReader;
1515
pub use types::ShellPipeWriter;
1616
pub use types::ShellState;
1717
pub use types::SignalKind;
18+
pub use types::SignalMessage;
1819
pub use types::pipe;
1920
pub use which::CommandPathResolutionError;
2021

src/shell/types.rs

Lines changed: 122 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -518,26 +518,26 @@ struct KillSignalInner {
518518
// then awaited after. If an abort happened between that then
519519
// it could be missed.
520520
aborted_code: RefCell<Option<i32>>,
521-
sender: broadcast::Sender<SignalKind>,
521+
sender: broadcast::Sender<SignalMessage>,
522522
children: RefCell<Vec<Weak<KillSignalInner>>>,
523523
/// Information about the current child process.
524524
child_process: Cell<ChildProcessInfo>,
525525
}
526526

527527
impl KillSignalInner {
528-
pub fn send(&self, signal_kind: SignalKind) {
529-
if signal_kind.causes_abort() {
528+
pub fn send(&self, signal: SignalMessage) {
529+
if signal.kind.causes_abort() {
530530
let mut stored_aborted_code = self.aborted_code.borrow_mut();
531531
if stored_aborted_code.is_none() {
532-
*stored_aborted_code = Some(signal_kind.aborted_code());
532+
*stored_aborted_code = Some(signal.kind.aborted_code());
533533
}
534534
}
535-
_ = self.sender.send(signal_kind);
535+
_ = self.sender.send(signal);
536536

537537
// notify children
538538
self.children.borrow_mut().retain(|weak_child| {
539539
if let Some(child) = weak_child.upgrade() {
540-
child.send(signal_kind);
540+
child.send(signal);
541541
true
542542
} else {
543543
false // clean-up dropped children
@@ -599,25 +599,35 @@ impl KillSignal {
599599
}
600600
}
601601

602-
/// Send a signal to commands being run.
602+
/// Send a signal to commands being run (programmatic, always forwarded).
603603
pub fn send(&self, signal: SignalKind) {
604-
self.0.send(signal)
604+
self.0.send(SignalMessage::new(signal))
605+
}
606+
607+
/// Send a signal that originated from a specific process group.
608+
///
609+
/// Use this when forwarding signals that came from the terminal (e.g., Ctrl+C).
610+
/// Children in the same process group as `origin_pgid` will not receive
611+
/// a duplicate signal, since the terminal already sent it to them directly.
612+
#[cfg(unix)]
613+
pub fn send_from_pgid(&self, signal: SignalKind, origin_pgid: i32) {
614+
self.0.send(SignalMessage::from_pgid(signal, origin_pgid))
605615
}
606616

607617
/// Waits for only signals deemed to abort a command.
608-
pub async fn wait_aborted(&self) -> SignalKind {
618+
pub async fn wait_aborted(&self) -> SignalMessage {
609619
let mut receiver = self.0.sender.subscribe();
610620
loop {
611621
// unwrap is ok because we're holding a sender in `self`
612622
let signal = receiver.recv().await.unwrap();
613-
if signal.causes_abort() {
623+
if signal.kind.causes_abort() {
614624
return signal;
615625
}
616626
}
617627
}
618628

619629
/// Waits for any signal to be received.
620-
pub async fn wait_any(&self) -> SignalKind {
630+
pub async fn wait_any(&self) -> SignalMessage {
621631
let mut receiver = self.0.sender.subscribe();
622632
// unwrap is ok because we're holding a sender in `self`
623633
receiver.recv().await.unwrap()
@@ -700,6 +710,62 @@ pub enum SignalKind {
700710
Other(i32),
701711
}
702712

713+
/// A signal message that includes origin information to prevent double-signaling.
714+
///
715+
/// When a signal originates from the terminal (e.g., Ctrl+C), it's sent to all
716+
/// processes in the foreground process group. If the shell also forwards the
717+
/// signal programmatically, children would receive it twice. The `origin_pgid`
718+
/// field allows recipients to skip forwarding if the child is in the same
719+
/// process group as the signal origin.
720+
#[derive(Debug, Clone, Copy)]
721+
pub struct SignalMessage {
722+
pub kind: SignalKind,
723+
/// The process group ID where this signal originated.
724+
/// If Some, children in the same PGID already received this signal
725+
/// from the terminal and should not be signaled again.
726+
/// If None, the signal is programmatic and should always be forwarded.
727+
#[cfg(unix)]
728+
pub origin_pgid: Option<i32>,
729+
}
730+
731+
impl SignalMessage {
732+
/// Create a new signal message for programmatic signals (always forwarded).
733+
pub fn new(kind: SignalKind) -> Self {
734+
Self {
735+
kind,
736+
#[cfg(unix)]
737+
origin_pgid: None,
738+
}
739+
}
740+
741+
/// Create a new signal message originating from a specific process group.
742+
/// Children in the same PGID will not receive a duplicate signal.
743+
#[cfg(unix)]
744+
pub fn from_pgid(kind: SignalKind, pgid: i32) -> Self {
745+
Self {
746+
kind,
747+
origin_pgid: Some(pgid),
748+
}
749+
}
750+
751+
/// Check if a signal should be forwarded to a child process.
752+
/// Returns true if the signal should be forwarded, false if the child
753+
/// already received it from the terminal.
754+
#[cfg(unix)]
755+
pub fn should_forward_to(&self, child_pgid: Option<i32>) -> bool {
756+
match (self.origin_pgid, child_pgid) {
757+
(Some(origin), Some(child)) => origin != child,
758+
_ => true, // Forward if we can't determine (programmatic or unknown PGID)
759+
}
760+
}
761+
762+
/// On non-Unix platforms, always forward signals.
763+
#[cfg(not(unix))]
764+
pub fn should_forward_to(&self, _child_pgid: Option<i32>) -> bool {
765+
true
766+
}
767+
}
768+
703769
impl SignalKind {
704770
pub fn causes_abort(&self) -> bool {
705771
match self {
@@ -787,7 +853,7 @@ mod test {
787853

788854
// Wait for the signal in the main task
789855
let signal = kill_signal.wait_any().await;
790-
assert_eq!(signal, SignalKind::SIGTERM);
856+
assert_eq!(signal.kind, SignalKind::SIGTERM);
791857
}
792858

793859
#[tokio::test]
@@ -810,7 +876,7 @@ mod test {
810876
);
811877

812878
for signal in [signals.0, signals.1, signals.2].into_iter() {
813-
assert_eq!(signal, SignalKind::SIGKILL);
879+
assert_eq!(signal.kind, SignalKind::SIGKILL);
814880
}
815881
assert_eq!(child_signal.aborted_code(), Some(128 + 9));
816882
assert_eq!(sibling_signal.aborted_code(), Some(128 + 9));
@@ -846,7 +912,7 @@ mod test {
846912

847913
// Wait for the aborting signal in the main task
848914
let signal = kill_signal.wait_aborted().await;
849-
assert_eq!(signal, SignalKind::SIGABRT);
915+
assert_eq!(signal.kind, SignalKind::SIGABRT);
850916
assert!(kill_signal.aborted_code().is_some());
851917
}
852918

@@ -868,7 +934,7 @@ mod test {
868934

869935
// Wait for the signal in the child
870936
let signal = child_signal.wait_aborted().await;
871-
assert_eq!(signal, SignalKind::SIGQUIT);
937+
assert_eq!(signal.kind, SignalKind::SIGQUIT);
872938
assert_eq!(parent_signal.aborted_code(), Some(128 + 3));
873939
assert_eq!(child_signal.aborted_code(), Some(128 + 3));
874940
}
@@ -893,7 +959,7 @@ mod test {
893959

894960
// Verify no panic occurred and the parent still functions
895961
let signal = parent_signal.wait_any().await;
896-
assert_eq!(signal, SignalKind::SIGTERM);
962+
assert_eq!(signal.kind, SignalKind::SIGTERM);
897963
}
898964

899965
#[tokio::test]
@@ -956,4 +1022,44 @@ mod test {
9561022
// Parent should still see its own child process
9571023
assert_eq!(parent_signal.current_child_pid(), Some(1234));
9581024
}
1025+
1026+
#[cfg(unix)]
1027+
#[test]
1028+
fn test_signal_message_should_forward_to() {
1029+
use super::SignalMessage;
1030+
1031+
// Programmatic signal (no origin_pgid) should always forward
1032+
let programmatic = SignalMessage::new(SignalKind::SIGINT);
1033+
assert!(programmatic.should_forward_to(Some(1000)));
1034+
assert!(programmatic.should_forward_to(Some(2000)));
1035+
assert!(programmatic.should_forward_to(None));
1036+
1037+
// Signal from PGID 1000 should NOT forward to child in same PGID
1038+
let from_tty = SignalMessage::from_pgid(SignalKind::SIGINT, 1000);
1039+
assert!(!from_tty.should_forward_to(Some(1000))); // Same PGID - don't forward
1040+
assert!(from_tty.should_forward_to(Some(2000))); // Different PGID - forward
1041+
assert!(from_tty.should_forward_to(None)); // Unknown PGID - forward (safe default)
1042+
}
1043+
1044+
#[cfg(unix)]
1045+
#[tokio::test]
1046+
async fn test_send_from_pgid() {
1047+
let kill_signal = KillSignal::default();
1048+
1049+
// Spawn a task to send a signal from a specific PGID
1050+
let signal_sender = kill_signal.clone();
1051+
deno_unsync::spawn(async move {
1052+
signal_sender.send_from_pgid(SignalKind::SIGINT, 12345);
1053+
});
1054+
1055+
// Wait for the signal
1056+
let signal = kill_signal.wait_any().await;
1057+
assert_eq!(signal.kind, SignalKind::SIGINT);
1058+
assert_eq!(signal.origin_pgid, Some(12345));
1059+
1060+
// Same PGID should not forward
1061+
assert!(!signal.should_forward_to(Some(12345)));
1062+
// Different PGID should forward
1063+
assert!(signal.should_forward_to(Some(99999)));
1064+
}
9591065
}

tests/integration_test.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1631,4 +1631,3 @@ fn no_such_file_error_text() -> &'static str {
16311631
"No such file or directory (os error 2)"
16321632
}
16331633
}
1634-

0 commit comments

Comments
 (0)