Skip to content

Commit 8617060

Browse files
committed
fix(agent,teacher): IPv4 live query, Win7 WMI console encoding, netsh idx fallback
Made-with: Cursor
1 parent 4fac746 commit 8617060

3 files changed

Lines changed: 243 additions & 62 deletions

File tree

agent/main.py

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ def _debug(msg: str) -> None:
6262
sys.stderr.write("%s\n" % msg)
6363

6464

65+
def _coerce_ipv4_detail_for_wire(snap: Any) -> Dict[str, Any]:
66+
"""保证 MSG_RESULT.ipv4_detail 可 JSON 序列化。"""
67+
if not isinstance(snap, dict):
68+
return {}
69+
out: Dict[str, Any] = {}
70+
for k in ("ip", "mask", "gateway", "dns_primary", "dns_secondary"):
71+
out[k] = str(snap.get(k) or "").strip()
72+
if "dhcp" in snap:
73+
out["dhcp"] = bool(snap.get("dhcp"))
74+
return out
75+
76+
6577
def _guess_report_ipv4() -> str:
6678
try:
6779
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -280,22 +292,42 @@ def _handle_command(self, cmd: Dict[str, Any]) -> None:
280292
msg = "%s; auto reboot in 1s" % msg
281293
self._schedule_reboot()
282294
elif cmd_type == MSG_COMMAND_QUERY_IPV4_DETAIL:
283-
snap = get_default_ipv4_detail_snapshot(ttl_sec=0.0)
284-
ok = True
285-
msg = "ipv4_detail"
286-
detail_payload = snap
287-
try:
288-
self._send(
289-
{
290-
"type": MSG_RESULT,
291-
"cmd_id": cmd_id,
292-
"ok": bool(ok),
293-
"message": str(msg),
294-
"ipv4_detail": detail_payload if isinstance(detail_payload, dict) else {},
295-
}
296-
)
297-
except (ConnectionError, OSError, ValueError):
298-
self._stop.set()
295+
# 快照采集较慢,勿在读线程同步执行:否则长时间不 read_frame,易与心跳/ACK 交错出问题甚至读线程因异常退出。
296+
cid = cmd_id
297+
298+
def _query_worker() -> None:
299+
try:
300+
snap = get_default_ipv4_detail_snapshot(ttl_sec=0.0)
301+
detail = _coerce_ipv4_detail_for_wire(snap)
302+
self._send(
303+
{
304+
"type": MSG_RESULT,
305+
"cmd_id": cid,
306+
"ok": True,
307+
"message": "ipv4_detail",
308+
"ipv4_detail": detail,
309+
}
310+
)
311+
except (ConnectionError, OSError, ValueError):
312+
self._stop.set()
313+
except Exception as e:
314+
_debug("query ipv4 detail failed: %r" % (e,))
315+
try:
316+
self._send(
317+
{
318+
"type": MSG_RESULT,
319+
"cmd_id": cid,
320+
"ok": False,
321+
"message": str(e)[:500],
322+
"ipv4_detail": {},
323+
}
324+
)
325+
except (ConnectionError, OSError, ValueError):
326+
self._stop.set()
327+
328+
threading.Thread(
329+
target=_query_worker, daemon=True, name="ipv4-detail-query"
330+
).start()
299331
return
300332
elif cmd_type == MSG_COMMAND_SET_IPV4:
301333
name, diag = get_default_ipv4_interface_name()

agent/network_config.py

Lines changed: 142 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import subprocess
99
import sys
1010
import time
11-
from typing import Any, Dict, List, Optional, Tuple
11+
from typing import Any, Dict, List, Optional, Set, Tuple
1212

1313
from 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+
2056
def 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

532568
def _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+
549659
def _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

592699
def _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]
884982
def _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

Comments
 (0)