diff options
author | 2022-11-21 10:54:43 -0500 | |
---|---|---|
committer | 2022-11-21 10:54:43 -0500 | |
commit | 73a5501e04706e190ee7d97ac006924b0e90463a (patch) | |
tree | 1f90d302e1ce2b31085d053a447dcc384eca7b8c | |
parent | Refactor config patches in tests (diff) |
Remove attachment size limit, added time limit
-rw-r--r-- | snekbox/memfs.py | 7 | ||||
-rw-r--r-- | snekbox/nsjail.py | 33 | ||||
-rw-r--r-- | snekbox/snekio.py | 19 | ||||
-rw-r--r-- | snekbox/utils/timed.py | 28 |
4 files changed, 55 insertions, 32 deletions
diff --git a/snekbox/memfs.py b/snekbox/memfs.py index 727bc4c..03251cd 100644 --- a/snekbox/memfs.py +++ b/snekbox/memfs.py @@ -104,18 +104,17 @@ class MemFS: return folder def attachments( - self, max_count: int, max_size: int | None = None + self, max_count: int, pattern: str = "output*" ) -> Generator[FileAttachment, None, None]: """Return a list of attachments in the tempdir.""" count = 0 - # Look for any file starting with `output` - for file in self.home.glob("output*"): + for file in self.home.glob(pattern): if count > max_count: log.info(f"Max attachments {max_count} reached, skipping remaining files") break if file.is_file(): count += 1 - yield FileAttachment.from_path(file, max_size) + yield FileAttachment.from_path(file) def cleanup(self) -> None: """Unmounts tmpfs, releases name.""" diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py index 69b6599..d4255e6 100644 --- a/snekbox/nsjail.py +++ b/snekbox/nsjail.py @@ -14,7 +14,8 @@ from snekbox.memfs import MemFS __all__ = ("NsJail",) from snekbox.process import EvalResult -from snekbox.snekio import AttachmentError, FileAttachment +from snekbox.snekio import FileAttachment +from snekbox.utils.timed import timed log = logging.getLogger(__name__) @@ -24,6 +25,15 @@ LOG_PATTERN = re.compile( ) +def parse_files( + fs: MemFS, + files_limit: int, + files_pattern: str, +) -> list[FileAttachment]: + """Parse files in a MemFS.""" + return sorted(fs.attachments(files_limit, files_pattern), key=lambda file: file.name) + + class NsJail: """ Core Snekbox functionality, providing safe execution of Python code. @@ -38,16 +48,18 @@ class NsJail: max_output_size: int = 1_000_000, read_chunk_size: int = 10_000, memfs_instance_size: int = 48 * 1024 * 1024, - max_attachments: int = 100, - max_attachment_size: int | None = None, + files_limit: int = 100, + files_timeout: float = 15, + files_pattern: str = "output*", ): self.nsjail_path = nsjail_path self.config_path = config_path self.max_output_size = max_output_size self.read_chunk_size = read_chunk_size self.memfs_instance_size = memfs_instance_size - self.max_attachments = max_attachments - self.max_attachment_size = max_attachment_size + self.files_limit = files_limit + self.files_timeout = files_timeout + self.files_pattern = files_pattern self.config = self._read_config(config_path) self.cgroup_version = utils.cgroup.init(self.config) @@ -227,13 +239,14 @@ class NsJail: # Parse attachments try: # Sort attachments by name lexically - attachments = sorted( - fs.attachments(self.max_attachments, self.max_attachment_size), - key=lambda a: a.name, + attachments = timed( + parse_files, + (fs, self.files_limit, self.files_pattern), + timeout=self.files_timeout, ) log.info(f"Found {len(attachments)} attachments.") - except AttachmentError as err: - log.info(f"Failed to parse attachments: {err}") + except TimeoutError as err: + log.info(f"Exceeded time limit in parsing attachments: {err}") return EvalResult(args, returncode, f"AttachmentError: {err}") log_lines = nsj_log.read().decode("utf-8").splitlines() diff --git a/snekbox/snekio.py b/snekbox/snekio.py index 020ca8c..d3b3dce 100644 --- a/snekbox/snekio.py +++ b/snekbox/snekio.py @@ -7,20 +7,9 @@ from dataclasses import dataclass from pathlib import Path from typing import Generic, TypeVar -RequestType = dict[str, str | bool | list[str | dict[str, str]]] - T = TypeVar("T", str, bytes) -def sizeof_fmt(num: int, suffix: str = "B") -> str: - """Return a human-readable file size.""" - for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"): - if abs(num) < 1024: - return f"{num:3.1f}{unit}{suffix}" - num /= 1024 - return f"{num:.1f}Yi{suffix}" - - class AttachmentError(ValueError): """Raised when an attachment is invalid.""" @@ -64,14 +53,8 @@ class FileAttachment(Generic[T]): return cls(name, content) @classmethod - def from_path(cls, file: Path, max_size: int | None = None) -> FileAttachment[bytes]: + def from_path(cls, file: Path) -> FileAttachment[bytes]: """Create an attachment from a file path.""" - size = file.stat().st_size - if max_size is not None and size > max_size: - raise AttachmentError( - f"File {file.name} too large: {sizeof_fmt(size)} " - f"exceeds the limit of {sizeof_fmt(max_size)}" - ) return cls(file.name, file.read_bytes()) @property diff --git a/snekbox/utils/timed.py b/snekbox/utils/timed.py new file mode 100644 index 0000000..e1602b5 --- /dev/null +++ b/snekbox/utils/timed.py @@ -0,0 +1,28 @@ +"""Calling functions with time limits.""" +from collections.abc import Callable, Iterable, Mapping +from multiprocessing import Pool +from typing import Any, TypeVar + +_T = TypeVar("_T") +_V = TypeVar("_V") + +__all__ = ("timed",) + + +def timed( + func: Callable[[_T], _V], + args: Iterable = (), + kwds: Mapping[str, Any] | None = None, + timeout: float | None = None, +) -> _V: + """ + Call a function with a time limit. + + Raises: + TimeoutError: If the function call takes longer than `timeout` seconds. + """ + if kwds is None: + kwds = {} + with Pool(1) as pool: + result = pool.apply_async(func, args, kwds) + return result.get(timeout) |