@@ -872,57 +872,129 @@ pub mod module {
872872 run_at_forkers ( after_forkers_parent, false , vm) ;
873873 }
874874
875- /// Warn if forking from a multi-threaded process
876- fn warn_if_multi_threaded ( name : & str , vm : & VirtualMachine ) {
877- // Only check threading if it was already imported
878- // Avoid vm.import() which can execute arbitrary Python code in the fork path
879- let threading = match vm
880- . sys_module
881- . get_attr ( "modules" , vm)
882- . and_then ( |m| m. get_item ( "threading" , vm) )
875+ /// Best-effort number of OS threads in this process.
876+ /// Returns <= 0 when unavailable, mirroring CPython fallback behavior.
877+ fn get_number_of_os_threads ( ) -> isize {
878+ #[ cfg( target_os = "macos" ) ]
883879 {
884- Ok ( m) => m,
885- Err ( _) => return ,
886- } ;
887- let active = threading. get_attr ( "_active" , vm) . ok ( ) ;
888- let limbo = threading. get_attr ( "_limbo" , vm) . ok ( ) ;
880+ type MachPortT = libc:: c_uint ;
881+ type KernReturnT = libc:: c_int ;
882+ type MachMsgTypeNumberT = libc:: c_uint ;
883+ type ThreadActArrayT = * mut MachPortT ;
884+ const KERN_SUCCESS : KernReturnT = 0 ;
885+ unsafe extern "C" {
886+ fn mach_task_self ( ) -> MachPortT ;
887+ fn task_for_pid (
888+ task : MachPortT ,
889+ pid : libc:: c_int ,
890+ target_task : * mut MachPortT ,
891+ ) -> KernReturnT ;
892+ fn task_threads (
893+ target_task : MachPortT ,
894+ act_list : * mut ThreadActArrayT ,
895+ act_list_cnt : * mut MachMsgTypeNumberT ,
896+ ) -> KernReturnT ;
897+ fn vm_deallocate (
898+ target_task : MachPortT ,
899+ address : libc:: uintptr_t ,
900+ size : libc:: uintptr_t ,
901+ ) -> KernReturnT ;
902+ }
889903
890- let count_dict = |obj : Option < crate :: PyObjectRef > | -> usize {
891- obj. and_then ( |o| o. length_opt ( vm) )
892- . and_then ( |r| r. ok ( ) )
893- . unwrap_or ( 0 )
894- } ;
904+ let self_task = unsafe { mach_task_self ( ) } ;
905+ let mut proc_task: MachPortT = 0 ;
906+ if unsafe { task_for_pid ( self_task, libc:: getpid ( ) , & mut proc_task) } == KERN_SUCCESS {
907+ let mut threads: ThreadActArrayT = core:: ptr:: null_mut ( ) ;
908+ let mut n_threads: MachMsgTypeNumberT = 0 ;
909+ if unsafe { task_threads ( proc_task, & mut threads, & mut n_threads) } == KERN_SUCCESS
910+ {
911+ if !threads. is_null ( ) {
912+ let _ = unsafe {
913+ vm_deallocate (
914+ self_task,
915+ threads as libc:: uintptr_t ,
916+ ( n_threads as usize * core:: mem:: size_of :: < MachPortT > ( ) )
917+ as libc:: uintptr_t ,
918+ )
919+ } ;
920+ }
921+ return n_threads as isize ;
922+ }
923+ }
924+ 0
925+ }
926+ #[ cfg( target_os = "linux" ) ]
927+ {
928+ use std:: io:: Read as _;
929+ let mut file = match std:: fs:: File :: open ( "/proc/self/stat" ) {
930+ Ok ( f) => f,
931+ Err ( _) => return 0 ,
932+ } ;
933+ let mut buf = [ 0u8 ; 160 ] ;
934+ let n = match file. read ( & mut buf) {
935+ Ok ( n) => n,
936+ Err ( _) => return 0 ,
937+ } ;
938+ let line = match std:: str:: from_utf8 ( & buf[ ..n] ) {
939+ Ok ( s) => s,
940+ Err ( _) => return 0 ,
941+ } ;
942+ if let Some ( field) = line. split_whitespace ( ) . nth ( 19 ) {
943+ return field. parse :: < isize > ( ) . unwrap_or ( 0 ) ;
944+ }
945+ return 0 ;
946+ }
947+ #[ cfg( not( any( target_os = "macos" , target_os = "linux" ) ) ) ]
948+ {
949+ 0
950+ }
951+ }
895952
896- let num_threads = count_dict ( active) + count_dict ( limbo) ;
897- if num_threads > 1 {
898- // Use Python warnings module to ensure filters are applied correctly
899- let Ok ( warnings) = vm. import ( "warnings" , 0 ) else {
900- return ;
953+ /// Warn if forking from a multi-threaded process.
954+ /// `num_os_threads` should be captured before parent after-fork hooks run.
955+ fn warn_if_multi_threaded ( name : & str , num_os_threads : isize , vm : & VirtualMachine ) {
956+ let num_threads = if num_os_threads > 0 {
957+ num_os_threads as usize
958+ } else {
959+ // CPython fallback: if OS-level count isn't available, use the
960+ // threading module's active+limbo view.
961+ // Only check threading if it was already imported. Avoid vm.import()
962+ // which can execute arbitrary Python code in the fork path.
963+ let threading = match vm
964+ . sys_module
965+ . get_attr ( "modules" , vm)
966+ . and_then ( |m| m. get_item ( "threading" , vm) )
967+ {
968+ Ok ( m) => m,
969+ Err ( _) => return ,
901970 } ;
902- let Ok ( warn_fn) = warnings. get_attr ( "warn" , vm) else {
903- return ;
971+ let active = threading. get_attr ( "_active" , vm) . ok ( ) ;
972+ let limbo = threading. get_attr ( "_limbo" , vm) . ok ( ) ;
973+
974+ // Match threading module internals and avoid sequence overcounting:
975+ // count only dict-backed _active/_limbo containers.
976+ let count_dict = |obj : Option < crate :: PyObjectRef > | -> usize {
977+ obj. and_then ( |o| {
978+ o. downcast_ref :: < crate :: builtins:: PyDict > ( )
979+ . map ( |d| d. __len__ ( ) )
980+ } )
981+ . unwrap_or ( 0 )
904982 } ;
905983
984+ count_dict ( active) + count_dict ( limbo)
985+ } ;
986+
987+ if num_threads > 1 {
906988 let pid = unsafe { libc:: getpid ( ) } ;
907989 let msg = format ! (
908990 "This process (pid={}) is multi-threaded, use of {}() may lead to deadlocks in the child." ,
909991 pid, name
910992 ) ;
911993
912- // Call warnings.warn(message, DeprecationWarning, stacklevel=2)
913- // stacklevel=2 to point to the caller of fork()
914- let args = crate :: function:: FuncArgs :: new (
915- vec ! [
916- vm. ctx. new_str( msg) . into( ) ,
917- vm. ctx. exceptions. deprecation_warning. as_object( ) . to_owned( ) ,
918- ] ,
919- crate :: function:: KwArgs :: new (
920- [ ( "stacklevel" . to_owned ( ) , vm. ctx . new_int ( 2 ) . into ( ) ) ]
921- . into_iter ( )
922- . collect ( ) ,
923- ) ,
924- ) ;
925- let _ = warn_fn. call ( args, vm) ;
994+ // Match PyErr_WarnFormat(..., stacklevel=1) in CPython.
995+ // Best effort: ignore failures like CPython does in this path.
996+ let _ =
997+ crate :: stdlib:: warnings:: warn ( vm. ctx . exceptions . deprecation_warning , msg, 1 , vm) ;
926998 }
927999 }
9281000
@@ -953,9 +1025,12 @@ pub mod module {
9531025 if pid == 0 {
9541026 py_os_after_fork_child ( vm) ;
9551027 } else {
1028+ // Match CPython timing: capture this before parent after-fork hooks
1029+ // in case those hooks start threads.
1030+ let num_os_threads = get_number_of_os_threads ( ) ;
9561031 py_os_after_fork_parent ( vm) ;
9571032 // Match CPython timing: warn only after parent callback path resumes world.
958- warn_if_multi_threaded ( "fork" , vm) ;
1033+ warn_if_multi_threaded ( "fork" , num_os_threads , vm) ;
9591034 }
9601035 if pid == -1 {
9611036 Err ( nix:: Error :: from_raw ( saved_errno) . into_pyexception ( vm) )
0 commit comments