@@ -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
527527impl 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+
703769impl 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}
0 commit comments