devbits 1.3.2__tar.gz → 1.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devbits-1.3.2 → devbits-1.3.3}/PKG-INFO +1 -1
- {devbits-1.3.2 → devbits-1.3.3}/devbits/__init__.py +1 -1
- {devbits-1.3.2 → devbits-1.3.3}/devbits/gui.py +2 -2
- {devbits-1.3.2 → devbits-1.3.3}/devbits/network.py +53 -7
- {devbits-1.3.2 → devbits-1.3.3}/devbits.egg-info/PKG-INFO +1 -1
- {devbits-1.3.2 → devbits-1.3.3}/pyproject.toml +1 -1
- {devbits-1.3.2 → devbits-1.3.3}/tests/test_cli.py +43 -0
- {devbits-1.3.2 → devbits-1.3.3}/LICENSE +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/README.md +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits/cache.py +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits/cli.py +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits/image.py +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits/media.py +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits/project.py +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits/scripts.py +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits/tui.py +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits/utils.py +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits/wifi.py +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits.egg-info/SOURCES.txt +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits.egg-info/dependency_links.txt +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits.egg-info/entry_points.txt +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits.egg-info/requires.txt +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/devbits.egg-info/top_level.txt +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/setup.cfg +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/tests/test_gui_cli.py +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/tests/test_tui.py +0 -0
- {devbits-1.3.2 → devbits-1.3.3}/tests/test_wifi.py +0 -0
|
@@ -1763,7 +1763,7 @@ def _has_audio(path: Path) -> bool:
|
|
|
1763
1763
|
out = subprocess.run(
|
|
1764
1764
|
[probe, "-v", "error", "-select_streams", "a",
|
|
1765
1765
|
"-show_entries", "stream=codec_type", "-of", "csv=p=0", str(path)],
|
|
1766
|
-
capture_output=True, text=True, timeout=15,
|
|
1766
|
+
capture_output=True, text=True, errors="replace", timeout=15,
|
|
1767
1767
|
)
|
|
1768
1768
|
return "audio" in out.stdout
|
|
1769
1769
|
except (subprocess.SubprocessError, OSError):
|
|
@@ -2116,7 +2116,7 @@ class _Handler(BaseHTTPRequestHandler):
|
|
|
2116
2116
|
global _export_progress
|
|
2117
2117
|
proc = subprocess.Popen(
|
|
2118
2118
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
2119
|
-
text=True, bufsize=1,
|
|
2119
|
+
text=True, errors="replace", bufsize=1,
|
|
2120
2120
|
)
|
|
2121
2121
|
try:
|
|
2122
2122
|
for line in proc.stdout:
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import codecs
|
|
3
4
|
import concurrent.futures
|
|
5
|
+
import functools
|
|
4
6
|
import ipaddress
|
|
7
|
+
import locale
|
|
5
8
|
import platform
|
|
6
9
|
import re
|
|
7
10
|
import socket
|
|
@@ -34,6 +37,48 @@ class Device:
|
|
|
34
37
|
is_gateway: bool = False
|
|
35
38
|
|
|
36
39
|
|
|
40
|
+
@functools.lru_cache(maxsize=1)
|
|
41
|
+
def _console_encoding() -> str:
|
|
42
|
+
"""The encoding the OS command-line tools print in.
|
|
43
|
+
|
|
44
|
+
On Windows this is the console output code page (e.g. ``cp950`` on a
|
|
45
|
+
Traditional Chinese install), *not* UTF-8. Elsewhere the locale's preferred
|
|
46
|
+
encoding is right.
|
|
47
|
+
"""
|
|
48
|
+
if platform.system().lower() == "windows":
|
|
49
|
+
try:
|
|
50
|
+
import ctypes
|
|
51
|
+
|
|
52
|
+
codepage = ctypes.windll.kernel32.GetConsoleOutputCP() or ctypes.windll.kernel32.GetOEMCP()
|
|
53
|
+
codecs.lookup(f"cp{codepage}") # reject code pages Python can't handle
|
|
54
|
+
return f"cp{codepage}"
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
try:
|
|
58
|
+
return codecs.lookup(locale.getpreferredencoding(False)).name
|
|
59
|
+
except Exception:
|
|
60
|
+
return "utf-8"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _run_text(cmd: list[str], timeout: float) -> tuple[int, str]:
|
|
64
|
+
"""Run ``cmd`` and return ``(returncode, stdout)``, decoded defensively.
|
|
65
|
+
|
|
66
|
+
Deliberately *not* ``text=True``: that decodes with UTF-8 strictly, so on a
|
|
67
|
+
non-English Windows the UnicodeDecodeError is raised inside subprocess'
|
|
68
|
+
reader threads, which leaves ``stdout`` as ``None`` and produces a confusing
|
|
69
|
+
``'NoneType' object has no attribute ...`` far from the real cause. Decoding
|
|
70
|
+
the bytes here with ``errors="replace"`` cannot fail, and everything we parse
|
|
71
|
+
out of these tools (IPs, MACs, ``TTL=``) is ASCII anyway.
|
|
72
|
+
"""
|
|
73
|
+
proc = subprocess.run(cmd, capture_output=True, timeout=timeout)
|
|
74
|
+
raw = proc.stdout
|
|
75
|
+
if raw is None:
|
|
76
|
+
return proc.returncode, ""
|
|
77
|
+
if isinstance(raw, str): # a text-mode fake / caller override
|
|
78
|
+
return proc.returncode, raw
|
|
79
|
+
return proc.returncode, raw.decode(_console_encoding(), "replace")
|
|
80
|
+
|
|
81
|
+
|
|
37
82
|
def local_ip() -> str:
|
|
38
83
|
"""Best-effort primary IPv4 address of this machine.
|
|
39
84
|
|
|
@@ -69,7 +114,7 @@ def gateway_ip() -> str | None:
|
|
|
69
114
|
system = platform.system().lower()
|
|
70
115
|
try:
|
|
71
116
|
if system == "windows":
|
|
72
|
-
out =
|
|
117
|
+
out = _run_text(["route", "print", "0.0.0.0"], timeout=5)[1]
|
|
73
118
|
for line in out.splitlines():
|
|
74
119
|
if line.strip().startswith("0.0.0.0"):
|
|
75
120
|
ips = _IP_RE.findall(line)
|
|
@@ -77,11 +122,11 @@ def gateway_ip() -> str | None:
|
|
|
77
122
|
return ips[2] # destination, netmask, gateway
|
|
78
123
|
return None
|
|
79
124
|
if system == "darwin":
|
|
80
|
-
out =
|
|
125
|
+
out = _run_text(["route", "-n", "get", "default"], timeout=5)[1]
|
|
81
126
|
match = re.search(r"gateway:\s*(" + _IP_RE.pattern + ")", out)
|
|
82
127
|
return match.group(1) if match else None
|
|
83
128
|
# Linux and other Unixes
|
|
84
|
-
out =
|
|
129
|
+
out = _run_text(["ip", "route"], timeout=5)[1]
|
|
85
130
|
match = re.search(r"default via (" + _IP_RE.pattern + ")", out)
|
|
86
131
|
return match.group(1) if match else None
|
|
87
132
|
except Exception:
|
|
@@ -170,7 +215,7 @@ def arp_table() -> dict[str, str]:
|
|
|
170
215
|
|
|
171
216
|
for command in commands:
|
|
172
217
|
try:
|
|
173
|
-
out =
|
|
218
|
+
out = _run_text(command, timeout=10)[1]
|
|
174
219
|
except Exception:
|
|
175
220
|
continue
|
|
176
221
|
table: dict[str, str] = {}
|
|
@@ -193,15 +238,16 @@ def _ping(ip: str, timeout: float = 1.0) -> bool:
|
|
|
193
238
|
else:
|
|
194
239
|
cmd = ["ping", "-c", "1", "-W", str(max(1, int(round(timeout)))), ip] # -W is seconds on Linux
|
|
195
240
|
try:
|
|
196
|
-
|
|
241
|
+
returncode, out = _run_text(cmd, timeout=timeout + 2)
|
|
197
242
|
except Exception:
|
|
198
243
|
return False
|
|
199
|
-
if
|
|
244
|
+
if returncode != 0:
|
|
200
245
|
return False
|
|
201
246
|
if system == "windows":
|
|
202
247
|
# Windows ping can exit 0 while printing "Destination host unreachable"
|
|
203
248
|
# (a reply from the gateway, not the target). Require a real echo reply.
|
|
204
|
-
|
|
249
|
+
# "TTL=" is ASCII in every locale, so this survives localized output.
|
|
250
|
+
return "ttl=" in out.lower()
|
|
205
251
|
return True
|
|
206
252
|
|
|
207
253
|
|
|
@@ -70,6 +70,7 @@ def test_arp_table_windows_command(monkeypatch) -> None:
|
|
|
70
70
|
calls = []
|
|
71
71
|
|
|
72
72
|
class _Result:
|
|
73
|
+
returncode = 0
|
|
73
74
|
stdout = " 192.168.0.1 aa-bb-cc-dd-ee-ff dynamic"
|
|
74
75
|
|
|
75
76
|
def fake_run(cmd, **kwargs):
|
|
@@ -95,6 +96,7 @@ def test_arp_table_linux_falls_back_to_ip_neigh(monkeypatch) -> None:
|
|
|
95
96
|
raise FileNotFoundError("arp not installed")
|
|
96
97
|
|
|
97
98
|
class _Result:
|
|
99
|
+
returncode = 0
|
|
98
100
|
stdout = "192.168.0.1 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE"
|
|
99
101
|
|
|
100
102
|
return _Result()
|
|
@@ -106,6 +108,47 @@ def test_arp_table_linux_falls_back_to_ip_neigh(monkeypatch) -> None:
|
|
|
106
108
|
assert table == {"192.168.0.1": "aa:bb:cc:dd:ee:ff"}
|
|
107
109
|
|
|
108
110
|
|
|
111
|
+
def test_subprocess_output_survives_non_utf8_locale(monkeypatch) -> None:
|
|
112
|
+
# A Traditional Chinese Windows prints cp950 bytes, which are not valid
|
|
113
|
+
# UTF-8. Decoding must not raise, and ping must still detect the reply.
|
|
114
|
+
import subprocess
|
|
115
|
+
|
|
116
|
+
import devbits.network as net
|
|
117
|
+
|
|
118
|
+
# "回覆自 192.168.50.138: 位元組=32 時間<1ms TTL=64" in cp950.
|
|
119
|
+
cp950 = (
|
|
120
|
+
"回覆自 192.168.50.138: 位元組=32 時間<1ms TTL=64".encode("cp950")
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def fake_run(cmd, **kwargs):
|
|
124
|
+
assert not kwargs.get("text"), "text=True decodes as UTF-8 and crashes here"
|
|
125
|
+
return subprocess.CompletedProcess(cmd, 0, cp950, b"")
|
|
126
|
+
|
|
127
|
+
monkeypatch.setattr(net.platform, "system", lambda: "Windows")
|
|
128
|
+
monkeypatch.setattr(net.subprocess, "run", fake_run)
|
|
129
|
+
net._console_encoding.cache_clear()
|
|
130
|
+
monkeypatch.setattr(net, "_console_encoding", lambda: "cp950")
|
|
131
|
+
|
|
132
|
+
assert net._ping("192.168.50.138") is True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_ping_tolerates_undecodable_bytes(monkeypatch) -> None:
|
|
136
|
+
# Even if the code page guess is wrong, no byte sequence may raise.
|
|
137
|
+
import subprocess
|
|
138
|
+
|
|
139
|
+
import devbits.network as net
|
|
140
|
+
|
|
141
|
+
def fake_run(cmd, **kwargs):
|
|
142
|
+
return subprocess.CompletedProcess(cmd, 0, b"\xa4\xa8 TTL=64 \xff\xfe", b"")
|
|
143
|
+
|
|
144
|
+
monkeypatch.setattr(net.platform, "system", lambda: "Windows")
|
|
145
|
+
monkeypatch.setattr(net.subprocess, "run", fake_run)
|
|
146
|
+
net._console_encoding.cache_clear()
|
|
147
|
+
monkeypatch.setattr(net, "_console_encoding", lambda: "utf-8")
|
|
148
|
+
|
|
149
|
+
assert net._ping("192.168.50.138") is True
|
|
150
|
+
|
|
151
|
+
|
|
109
152
|
def test_private_mac_detection() -> None:
|
|
110
153
|
from devbits.network import is_private_mac
|
|
111
154
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|