88import subprocess
99import sys
1010import time
11- from typing import Any , Dict , List , Optional , Tuple
11+ from typing import Any , Dict , List , Optional , Set , Tuple
1212
1313from common .wincli_escape import netsh_interface_name_arg
1414
@@ -17,6 +17,42 @@ def _subprocess_flags() -> int:
1717 return subprocess .CREATE_NO_WINDOW if sys .platform == "win32" else 0
1818
1919
20+ def _win_console_encoding () -> str :
21+ """wmic/route/netsh 在中文 Windows 上多为系统 ANSI(如 cp936),勿用默认 UTF-8 解码。"""
22+ if sys .platform != "win32" :
23+ return "utf-8"
24+ try :
25+ import ctypes
26+
27+ cp = ctypes .windll .kernel32 .GetACP ()
28+ if cp :
29+ return "cp%d" % int (cp )
30+ except Exception :
31+ pass
32+ return "cp936"
33+
34+
35+ def _run_text_cmd (args : List [str ], timeout : float = 30.0 ) -> subprocess .CompletedProcess [str ]:
36+ """非 PowerShell 的控制台子进程:Windows 用系统代码页解码。"""
37+ if sys .platform == "win32" :
38+ return subprocess .run (
39+ args ,
40+ capture_output = True ,
41+ text = True ,
42+ encoding = _win_console_encoding (),
43+ errors = "replace" ,
44+ creationflags = _subprocess_flags (),
45+ timeout = timeout ,
46+ )
47+ return subprocess .run (
48+ args ,
49+ capture_output = True ,
50+ text = True ,
51+ creationflags = 0 ,
52+ timeout = timeout ,
53+ )
54+
55+
2056def is_admin () -> bool :
2157 if sys .platform != "win32" :
2258 return False
@@ -531,21 +567,95 @@ def _parse_route_print_default_gateway(route_text: str) -> Optional[str]:
531567
532568def _route_print_ipv4_default_gateway () -> str :
533569 try :
534- p = subprocess .run (
535- ["route" , "print" , "-4" ],
536- capture_output = True ,
537- text = True ,
538- creationflags = _subprocess_flags (),
539- timeout = 30 ,
540- )
541- except (OSError , subprocess .TimeoutExpired ):
570+ p = _run_text_cmd (["route" , "print" , "-4" ], timeout = 30 )
571+ except (OSError , subprocess .TimeoutExpired , ValueError ):
542572 return ""
543573 if p .returncode != 0 :
544574 return ""
545575 g = _parse_route_print_default_gateway (p .stdout or "" )
546576 return g if g else ""
547577
548578
579+ def _parse_netsh_dns_ipv4_list (text : str ) -> List [str ]:
580+ """从 netsh show dns 输出里按出现顺序取前几个公网/私网 IPv4 DNS。"""
581+ out : List [str ] = []
582+ seen : Set [str ] = set ()
583+ for m in re .finditer (r"\b(\d{1,3}(?:\.\d{1,3}){3})\b" , text ):
584+ ip = m .group (1 )
585+ if not _is_dotted_ipv4 (ip ):
586+ continue
587+ low = ip .lower ()
588+ if low .startswith (("0." , "127." , "169.254." , "224." , "255." )):
589+ continue
590+ if ip not in seen :
591+ seen .add (ip )
592+ out .append (ip )
593+ if len (out ) >= 4 :
594+ break
595+ return out [:2 ]
596+
597+
598+ def _netsh_ipv4_enrich_from_ifindex (if_idx : int ) -> Dict [str , Any ]:
599+ """
600+ 用「接口索引」调用 netsh,不依赖中文连接名;补缺子网掩码与 DNS(Win7/编码异常时尤其有效)。
601+ """
602+ out : Dict [str , Any ] = {}
603+ if sys .platform != "win32" or if_idx < 0 :
604+ return out
605+ try :
606+ p = _run_text_cmd (
607+ ["netsh" , "interface" , "ipv4" , "show" , "addresses" , str (if_idx )],
608+ timeout = 25 ,
609+ )
610+ except (OSError , subprocess .TimeoutExpired , ValueError ):
611+ return out
612+ if p .returncode != 0 :
613+ return out
614+ text = p .stdout or ""
615+
616+ mmask = re .search (
617+ r"\(\s*mask\s+(\d{1,3}(?:\.\d{1,3}){3})\s*\)" ,
618+ text ,
619+ re .I ,
620+ )
621+ if mmask :
622+ out ["mask" ] = mmask .group (1 )
623+ if not out .get ("mask" ):
624+ mp = re .search (
625+ r"(?:Subnet\s+Prefix|子网前缀)[^\n\r]*?(\d{1,3}(?:\.\d{1,3}){3})/(\d{1,2})" ,
626+ text ,
627+ re .I ,
628+ )
629+ if mp :
630+ ml = mask_from_prefix (mp .group (2 ))
631+ if ml :
632+ out ["mask" ] = ml
633+ if not out .get ("mask" ):
634+ mp2 = re .search (
635+ r"(\d{1,3}(?:\.\d{1,3}){3})/(\d{1,2})\b[^\n\r]*\(mask\s+(\d{1,3}(?:\.\d{1,3}){3})\)" ,
636+ text ,
637+ re .I ,
638+ )
639+ if mp2 :
640+ out ["mask" ] = mp2 .group (3 )
641+
642+ try :
643+ p2 = _run_text_cmd (
644+ ["netsh" , "interface" , "ipv4" , "show" , "dns" , str (if_idx )],
645+ timeout = 25 ,
646+ )
647+ except (OSError , subprocess .TimeoutExpired , ValueError ):
648+ return out
649+ if p2 .returncode != 0 :
650+ return out
651+ dns = _parse_netsh_dns_ipv4_list (p2 .stdout or "" )
652+ if dns :
653+ out ["dns_primary" ] = dns [0 ]
654+ if len (dns ) > 1 :
655+ out ["dns_secondary" ] = dns [1 ]
656+ return out
657+
658+
549659def _wmic_nic_configuration_row_for_ipv4 (target_ipv4 : str ) -> Optional [Dict [str , str ]]:
550660 """
551661 Win7 等环境下路由表 InterfaceIndex 与 NICConfiguration.InterfaceIndex 可能不一致;
@@ -555,7 +665,7 @@ def _wmic_nic_configuration_row_for_ipv4(target_ipv4: str) -> Optional[Dict[str,
555665 if not target or not _is_dotted_ipv4 (target ):
556666 return None
557667 try :
558- p = subprocess . run (
668+ p = _run_text_cmd (
559669 [
560670 "wmic" ,
561671 "path" ,
@@ -566,12 +676,9 @@ def _wmic_nic_configuration_row_for_ipv4(target_ipv4: str) -> Optional[Dict[str,
566676 "InterfaceIndex,IPAddress,IPSubnet,DefaultIPGateway,DNSServerSearchOrder,DHCPEnabled,IPEnabled" ,
567677 "/format:list" ,
568678 ],
569- capture_output = True ,
570- text = True ,
571- creationflags = _subprocess_flags (),
572679 timeout = 45 ,
573680 )
574- except (OSError , subprocess .TimeoutExpired ):
681+ except (OSError , subprocess .TimeoutExpired , ValueError ):
575682 return None
576683 if p .returncode != 0 :
577684 return None
@@ -591,13 +698,10 @@ def _wmic_nic_configuration_row_for_ipv4(target_ipv4: str) -> Optional[Dict[str,
591698
592699def _interface_from_route_print_only () -> Tuple [Optional [str ], str ]:
593700 """Win7+:route print -4 + WMI 反查(不依赖系统语言列标题)。"""
594- p = subprocess .run (
595- ["route" , "print" , "-4" ],
596- capture_output = True ,
597- text = True ,
598- creationflags = _subprocess_flags (),
599- timeout = 30 ,
600- )
701+ try :
702+ p = _run_text_cmd (["route" , "print" , "-4" ], timeout = 30 )
703+ except (OSError , subprocess .TimeoutExpired , ValueError ):
704+ return None , "route print failed"
601705 if p .returncode != 0 :
602706 return None , (p .stderr or p .stdout or "route print failed" ).strip ()[:300 ]
603707 ip = _parse_route_print_default_interface_ip (p .stdout or "" )
@@ -791,7 +895,7 @@ def _wmic_interface_index_for_netconnection_id(target: str) -> Optional[int]:
791895 if not want :
792896 return None
793897 try :
794- p = subprocess . run (
898+ p = _run_text_cmd (
795899 [
796900 "wmic" ,
797901 "path" ,
@@ -802,12 +906,9 @@ def _wmic_interface_index_for_netconnection_id(target: str) -> Optional[int]:
802906 "NetConnectionID,InterfaceIndex" ,
803907 "/format:list" ,
804908 ],
805- capture_output = True ,
806- text = True ,
807- creationflags = _subprocess_flags (),
808909 timeout = 30 ,
809910 )
810- except (OSError , subprocess .TimeoutExpired ):
911+ except (OSError , subprocess .TimeoutExpired , ValueError ):
811912 return None
812913 if p .returncode != 0 :
813914 return None
@@ -825,7 +926,7 @@ def _wmic_nic_configuration_by_interface_index(if_idx: int) -> Optional[Dict[str
825926 """Win7 上部分环境需用 Index= 而非 InterfaceIndex=,两种都试。"""
826927 for where in ("InterfaceIndex=%d" % if_idx , "Index=%d" % if_idx ):
827928 try :
828- p = subprocess . run (
929+ p = _run_text_cmd (
829930 [
830931 "wmic" ,
831932 "path" ,
@@ -836,12 +937,9 @@ def _wmic_nic_configuration_by_interface_index(if_idx: int) -> Optional[Dict[str
836937 "IPAddress,IPSubnet,DefaultIPGateway,DNSServerSearchOrder,DHCPEnabled,IPEnabled" ,
837938 "/format:list" ,
838939 ],
839- capture_output = True ,
840- text = True ,
841- creationflags = _subprocess_flags (),
842940 timeout = 30 ,
843941 )
844- except (OSError , subprocess .TimeoutExpired ):
942+ except (OSError , subprocess .TimeoutExpired , ValueError ):
845943 continue
846944 if p .returncode != 0 :
847945 continue
@@ -884,7 +982,7 @@ def _ipv4_subnet_pairs_from_wmi_row(row: Dict[str, str]) -> List[Tuple[str, str]
884982def _wmic_default_route_interface_index () -> Optional [int ]:
885983 """与 _interface_from_wmi_ip4_route_table 相同:默认 IPv4 路由所在 InterfaceIndex。"""
886984 try :
887- p = subprocess . run (
985+ p = _run_text_cmd (
888986 [
889987 "wmic" ,
890988 "path" ,
@@ -895,12 +993,9 @@ def _wmic_default_route_interface_index() -> Optional[int]:
895993 "InterfaceIndex,Metric1" ,
896994 "/format:list" ,
897995 ],
898- capture_output = True ,
899- text = True ,
900- creationflags = _subprocess_flags (),
901996 timeout = 30 ,
902997 )
903- except (OSError , subprocess .TimeoutExpired ):
998+ except (OSError , subprocess .TimeoutExpired , ValueError ):
904999 return None
9051000 if p .returncode != 0 :
9061001 return None
@@ -1115,6 +1210,17 @@ def get_default_ipv4_detail_snapshot(ttl_sec: float = 20.0) -> Dict[str, Any]:
11151210 if gw_rp :
11161211 out ["gateway" ] = gw_rp
11171212
1213+ idx_netsh = widx
1214+ if idx_netsh is None and iface_name :
1215+ alt_i = _wmic_interface_index_for_netconnection_id (iface_name )
1216+ if alt_i is not None :
1217+ idx_netsh = alt_i
1218+ if idx_netsh is not None and (
1219+ not str (out .get ("mask" ) or "" ).strip ()
1220+ or not str (out .get ("dns_primary" ) or "" ).strip ()
1221+ ):
1222+ _merge_ipv4_detail_fields (out , _netsh_ipv4_enrich_from_ifindex (idx_netsh ))
1223+
11181224 _detail_mono = now
11191225 _detail_payload = dict (out )
11201226 return dict (out )
0 commit comments