Skip to content

Commit 81ffc65

Browse files
committed
Better handling of errors in typst process
1 parent fe5a4bf commit 81ffc65

File tree

3 files changed

+27
-9
lines changed

3 files changed

+27
-9
lines changed

bot/exts/fun/typst.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
from io import BytesIO
66
from pathlib import Path
7+
from subprocess import CalledProcessError
78
from tempfile import TemporaryDirectory
89

910
import discord
@@ -112,7 +113,9 @@ async def _ensure_typst_executable(self) -> None:
112113
stderr=asyncio.subprocess.PIPE,
113114
)
114115
stdout, _ = await proc.communicate()
115-
log.info(f"Typst executable reports itself as {stdout.decode('utf-8').strip()!r}.")
116+
log.info(
117+
f"Typst executable reports itself as {stdout.decode('utf-8').strip()!r}."
118+
)
116119

117120
async def _download_typst_executable(self) -> None:
118121
if not Config.typst_archive_url:
@@ -200,7 +203,6 @@ async def typst(self, ctx: commands.Context, *, query: str) -> None:
200203
)
201204
return
202205
except TypstWorkerCrashedError:
203-
log.exception("typst worker crashed")
204206
await ctx.send(
205207
"Worker process crashed. "
206208
f"Perhaps the memory limit of {TYPST_MEMORY_LIMIT/1024**2:.1f}MB was exceeded?"
@@ -228,12 +230,19 @@ async def render_typst(
228230
mem_rlimit=TYPST_MEMORY_LIMIT,
229231
jobs=WORKER_JOBS,
230232
)
231-
except RuntimeError as e:
232-
raise InvalidTypstError(
233-
e.args[0] if e.args else "<no error message emitted>"
234-
)
235233
except TimeoutError:
236234
raise TypstTimeoutError
235+
except CalledProcessError as e:
236+
err = e.stderr.decode("utf-8")
237+
# when the memory limit is reached this usually shows up as signal 6 (SIGABRT), but it can vary
238+
if e.returncode < 0:
239+
log.debug(
240+
"Typst subprocess died due to a signal %s",
241+
str(e).split("died with")[-1].strip(),
242+
)
243+
raise TypstWorkerCrashedError
244+
# if in doubt we assume it's a normal error and return the logs
245+
raise InvalidTypstError(err)
237246

238247
raw_img = res.output
239248
if len(raw_img) > MAX_RAW_SIZE:

bot/utils/typst.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from dataclasses import dataclass
55
from functools import partial
66
from pathlib import Path
7+
from subprocess import CalledProcessError
78
from typing import Literal
89

910
from bot.constants import Typst as Config
@@ -41,7 +42,7 @@ async def compile_typst(
4142
"""
4243
typst_path = Path(Config.typst_path).resolve()
4344
if not typst_path.exists():
44-
raise FileNotFoundError("Typst executable was not found at path", typst_path)
45+
raise ValueError("Typst executable was not found at path", typst_path)
4546
if not root_path.is_dir():
4647
raise ValueError("Root directory was not a directory", root_path)
4748

@@ -70,9 +71,16 @@ async def compile_typst(
7071
)
7172

7273
stdout, stderr = await proc.communicate(input=source.encode("utf-8"))
73-
# if the task is cancelled or any other problem happens, make sure to kill the worker
74+
if proc.returncode is None:
75+
# shouldn't be possible
76+
raise RuntimeError("Process didn't terminate after communicate")
77+
if proc.returncode != 0:
78+
raise CalledProcessError(
79+
proc.returncode, [typst_path, *args], stdout, stderr
80+
)
81+
# if the task is cancelled or any other problem happens, make sure to kill the worker if it still exists
7482
except BaseException:
75-
with contextlib.suppress(UnboundLocalError):
83+
with contextlib.suppress(UnboundLocalError, ProcessLookupError):
7684
proc.kill()
7785
raise
7886
return TypstCompileResult(output=stdout, stderr=stderr)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ ignore = [
7474
"RUF029",
7575
"S311",
7676
"SIM102", "SIM108",
77+
"S404", # S404 is bugged and doesn't respect noqa
7778
]
7879

7980
[tool.ruff.lint.isort]

0 commit comments

Comments
 (0)