fix(core): decode subprocess output as UTF-8 with replacement

Strict UTF-8 decode of external tool output (ffmpeg/shaka/mp4decrypt) crashed reader threads on non-English Windows (cp950/Big5).
This commit is contained in:
imSp4rky
2026-07-07 09:23:05 -06:00
parent 5cd50544a8
commit 1dba4a8f82
15 changed files with 72 additions and 13 deletions
+12 -4
View File
@@ -842,19 +842,27 @@ class dl:
try:
if name == "shaka_packager":
r = subprocess.run(
[str(binary), "--version"], capture_output=True, text=True, timeout=5
[str(binary), "--version"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
)
version = (r.stdout or r.stderr or "").strip()
elif name in ("ffmpeg", "ffprobe"):
r = subprocess.run([str(binary), "-version"], capture_output=True, text=True, timeout=5)
r = subprocess.run(
[str(binary), "-version"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
)
version = (r.stdout or "").split("\n")[0].strip()
elif name == "mkvmerge":
r = subprocess.run(
[str(binary), "--version"], capture_output=True, text=True, timeout=5
[str(binary), "--version"],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
)
version = (r.stdout or "").strip()
elif name == "mp4decrypt":
r = subprocess.run([str(binary)], capture_output=True, text=True, timeout=5)
r = subprocess.run(
[str(binary)],
capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5,
)
output = (r.stdout or "") + (r.stderr or "")
lines = [line.strip() for line in output.split("\n") if line.strip()]
version = " | ".join(lines[:2]) if lines else None
+2
View File
@@ -282,6 +282,8 @@ def test(path: Path, map_: str) -> None:
],
stderr=subprocess.PIPE,
universal_newlines=True,
encoding="utf-8",
errors="replace",
)
reached_output = False
errors = 0
+3 -1
View File
@@ -1504,7 +1504,9 @@ def _binary_version(path: Any) -> Optional[str]:
for flag in ("--version", "-version"):
try:
proc = subprocess.run([str(path), flag], capture_output=True, text=True, timeout=5)
proc = subprocess.run(
[str(path), flag], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5
)
except (OSError, subprocess.SubprocessError, ValueError):
return None
match = _VERSION_RE.search(proc.stdout or "") or _VERSION_RE.search(proc.stderr or "")
+6 -1
View File
@@ -223,7 +223,10 @@ class ClearKeyCENC:
]
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8")
subprocess.run(
cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
encoding="utf-8", errors="replace",
)
except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else f"mp4decrypt failed with exit code {e.returncode}"
raise subprocess.CalledProcessError(e.returncode, cmd, output=e.stdout, stderr=error_msg)
@@ -262,6 +265,8 @@ class ClearKeyCENC:
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
universal_newlines=True,
encoding="utf-8",
errors="replace",
)
stream_skipped = False
+2
View File
@@ -247,6 +247,8 @@ class MonaLisa:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
startupinfo=startupinfo,
timeout=worker_timeout_s,
)
+4 -1
View File
@@ -438,7 +438,10 @@ class PlayReady:
]
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8")
subprocess.run(
cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
encoding="utf-8", errors="replace",
)
except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else f"mp4decrypt failed with exit code {e.returncode}"
raise subprocess.CalledProcessError(e.returncode, cmd, output=e.stdout, stderr=error_msg)
+6 -1
View File
@@ -378,7 +378,10 @@ class Widevine:
]
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8")
subprocess.run(
cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
encoding="utf-8", errors="replace",
)
except subprocess.CalledProcessError as e:
error_msg = e.stderr if e.stderr else f"mp4decrypt failed with exit code {e.returncode}"
raise subprocess.CalledProcessError(e.returncode, cmd, output=e.stdout, stderr=error_msg)
@@ -426,6 +429,8 @@ class Widevine:
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
universal_newlines=True,
encoding="utf-8",
errors="replace",
)
stream_skipped = False
+18
View File
@@ -544,6 +544,8 @@ class Gluetun(Proxy):
["docker", "ps", "--format", "{{.Ports}}"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
)
if result.returncode == 0:
@@ -892,6 +894,8 @@ class Gluetun(Proxy):
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
)
if result.returncode != 0:
@@ -926,6 +930,8 @@ class Gluetun(Proxy):
["docker", "inspect", container_name, "--format", "{{.NetworkSettings.Ports}}"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
)
@@ -1109,6 +1115,8 @@ class Gluetun(Proxy):
["docker", "inspect", container_name, "--format", "{{.State.ExitCode}}:{{.State.Error}}"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
)
if result.returncode == 0:
@@ -1310,6 +1318,8 @@ class Gluetun(Proxy):
["docker", "ps", "-a", "--filter", f"name=^{container_name}$", "--format", "{{.Names}}:{{.Status}}"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
)
@@ -1328,6 +1338,8 @@ class Gluetun(Proxy):
["docker", "rm", "-f", container_name],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=10,
)
return True
@@ -1346,6 +1358,8 @@ class Gluetun(Proxy):
["docker", "rm", "-f", container_name],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=10,
)
else:
@@ -1354,6 +1368,8 @@ class Gluetun(Proxy):
["docker", "stop", container_name],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=10,
)
except subprocess.TimeoutExpired:
@@ -1363,6 +1379,8 @@ class Gluetun(Proxy):
["docker", "rm", "-f", container_name],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=5,
)
except subprocess.TimeoutExpired:
+2 -2
View File
@@ -35,7 +35,7 @@ class Hola(Proxy):
p = subprocess.check_output(
[self.binary, "-country", query, "-list-proxies"], stderr=subprocess.STDOUT
).decode()
).decode("utf-8", errors="replace")
if "Transaction error: temporary ban detected." in p:
raise ConnectionError("Hola banned your IP temporarily from it's services. Try change your IP.")
@@ -55,6 +55,6 @@ class Hola(Proxy):
def get_countries(self) -> list[dict[str, str]]:
"""Get a list of available Countries."""
p = subprocess.check_output([self.binary, "-list-countries"]).decode("utf8")
p = subprocess.check_output([self.binary, "-list-countries"]).decode("utf-8", errors="replace")
return [{code: name} for country in p.splitlines() for (code, name) in [country.split(" - ", maxsplit=1)]]
+8
View File
@@ -225,6 +225,8 @@ class Hybrid:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
if result_duration.returncode != 0:
@@ -257,6 +259,8 @@ class Hybrid:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
if result_streams.returncode != 0:
@@ -300,6 +304,8 @@ class Hybrid:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
# cropdetect outputs crop=w:h:x:y
@@ -603,6 +609,8 @@ class Hybrid:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
)
max_mdl = 1000
+1 -1
View File
@@ -697,7 +697,7 @@ class Tracks:
errors = []
warnings = []
mux_start_time = time.monotonic()
p = subprocess.Popen(full_command, text=True, stdout=subprocess.PIPE)
p = subprocess.Popen(full_command, text=True, encoding="utf-8", errors="replace", stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, ""):
if line.startswith("#GUI#error") or line.startswith("#GUI#warning"):
errors.append(line)
+2
View File
@@ -456,6 +456,8 @@ class Video(Track):
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
vals = dict(line.split("=", 1) for line in probe.stdout.splitlines() if "=" in line)
current = (vals.get("color_primaries"), vals.get("color_transfer"), vals.get("color_space"))
+3 -1
View File
@@ -84,7 +84,9 @@ def info_summary(rpu: Path) -> str:
"""Return the textual summary (`dovi_tool info -i ... -s`) for an RPU file."""
tool = _require_dovi_tool()
info_start = time.monotonic()
p = subprocess.run([tool, "info", "-i", str(rpu), "-s"], capture_output=True, text=True)
p = subprocess.run(
[tool, "info", "-i", str(rpu), "-s"], capture_output=True, text=True, encoding="utf-8", errors="replace"
)
log_tool_run(
"dovi_tool info",
"dovi_tool",
+1 -1
View File
@@ -66,7 +66,7 @@ def ffprobe(uri: Union[bytes, Path]) -> dict:
context={"tool": "ffprobe", "duration_ms": round((time.monotonic() - start) * 1000, 1)},
)
return {}
result = json.loads(ff.stdout.decode("utf8"))
result = json.loads(ff.stdout.decode("utf-8", errors="replace"))
if dl:
dl.log(
level="DEBUG",
+2
View File
@@ -41,6 +41,8 @@ def apply_tags(path: Path, tags: dict[str, str]) -> None:
check=False,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
log_tool_run(
"mkvpropedit tags",