Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions nixie/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ async def wait_for_proc(
"""Wait for a process to complete and return its success status and stderr."""
try:
_, stderr = await asyncio.wait_for(proc.communicate(), timeout)
except TimeoutError:
except asyncio.TimeoutError: # noqa: UP041 # TODO(leynos): remove once ruff issue 8565 is fixed https://github.com/astral-sh/ruff/issues/8565
proc.kill()
await proc.wait()
print(f"{path}: diagram {idx} timed out", file=sys.stderr)
Expand All @@ -172,8 +172,7 @@ async def _run_mermaid_cli(
stdout=asyncio_subprocess.PIPE,
stderr=asyncio_subprocess.PIPE,
)

return await wait_for_proc(proc, path, idx, timeout)
return await wait_for_proc(proc, path, idx, timeout)


async def _render_diagram(
Expand Down Expand Up @@ -219,18 +218,8 @@ async def _render_diagram(
mmd.write_text(block)

cmd = get_mmdc_cmd(mmd, svg, cfg_path)
if not cmd or cmd[0] not in ALLOWED_EXECUTABLES:
raise UnexpectedExecutableError(cmd[0] if cmd else "")
LOGGER.info(shlex.join(cmd))

async with semaphore:
# nosemgrep: python.lang.security.audit.dangerous-asyncio-create-exec-audit
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
success, stderr = await wait_for_proc(proc, path, idx, timeout)
success, stderr = await _run_mermaid_cli(cmd, semaphore, path, idx, timeout)
if not success:
error_message = (
f"Error running command {shlex.join(cmd)} for file '{path}' "
Expand Down Expand Up @@ -299,6 +288,12 @@ async def render_block(
),
cli,
)
except NoNodeEnvironmentAvailableError:
LOGGER.error( # noqa: TRY400 # user-facing error; suppress stack trace
"No supported node environment found. Install mmdc directly, or install "
"Node.js (npx) or Bun to use @mermaid-js/mermaid-cli.",
exc_info=False,
)
except RuntimeError:
LOGGER.exception("Runtime error while rendering diagram")
except Exception as exc:
Expand Down
6 changes: 6 additions & 0 deletions nixie/unittests/test_puppeteer_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,22 @@
from nixie.cli import create_puppeteer_config

if typ.TYPE_CHECKING:
from pathlib import Path

import pytest


def test_create_puppeteer_config_as_root(monkeypatch: pytest.MonkeyPatch) -> None:
"""Include sandbox-disabling args when running as root."""
monkeypatch.setattr(os, "geteuid", lambda: 0)
path: Path | None = None
with create_puppeteer_config() as cfg:
assert cfg is not None
path = cfg
data = json.loads(cfg.read_text())
assert data["args"] == ["--no-sandbox", "--disable-setuid-sandbox"]
assert path is not None
assert not path.exists()


def test_create_puppeteer_config_non_root(monkeypatch: pytest.MonkeyPatch) -> None:
Expand Down
40 changes: 40 additions & 0 deletions nixie/unittests/test_wait_for_proc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Tests for :func:`nixie.cli.wait_for_proc`."""

from __future__ import annotations

import asyncio
import asyncio.subprocess as asyncio_subprocess
import sys
import typing as typ

import pytest

from nixie.cli import wait_for_proc

if typ.TYPE_CHECKING:
from pathlib import Path


@pytest.mark.asyncio
async def test_wait_for_proc_handles_asyncio_timeout_error(tmp_path: Path) -> None:
"""Handle :class:`asyncio.TimeoutError` raised by ``asyncio.wait_for``."""
cmd = [sys.executable, "-c", "import time; time.sleep(1)"]

proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio_subprocess.PIPE,
stderr=asyncio_subprocess.PIPE,
)
with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(proc.communicate(), 0.01)
proc.kill()
await proc.wait()

proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio_subprocess.PIPE,
stderr=asyncio_subprocess.PIPE,
)
success, stderr = await wait_for_proc(proc, tmp_path / "dummy.md", 1, timeout=0.01)
assert not success
assert stderr == b""