mirror of
https://github.com/penpot/penpot.git
synced 2026-07-21 05:27:50 +00:00
* ✨ Add font processing resource limits via prlimit Font processing tools (fontforge, sfnt2woff, woff2sfnt, woff2_decompress) were invoked via clojure.java.shell/sh with no timeouts or resource limits. This adds process-level resource limits using prlimit(1) and the shell/exec! infrastructure from the ImageMagick hardening work. shell/exec! changes: - Add :prlimit parameter that prepends prlimit(1) to the command - :prlimit takes {:mem <MiB> :cpu <seconds>} for address space and CPU time limits, enforced by the kernel's RLIMIT subsystem - prlimit-cmd builds the prlimit command prefix (private helper) Font processing changes: - Replace all clojure.java.shell/sh calls with shell/exec! via exec-font! - exec-font! applies font-prlimit (512 MiB, 30s CPU, 60s wall-clock) - All 5 conversion functions (ttf->otf, otf->ttf, ttf-or-otf->woff, woff->sfnt, woff2->sfnt) use try/finally for explicit temp file cleanup - Remove clojure.java.shell require from media.clj Tests: - Add exec-prlimit-normal, exec-prlimit-cpu, exec-prlimit-memory tests Closes #10234 Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app> * ✨ Make font processing resource limits configurable Replace hardcoded font-prlimit map and wall-clock timeout with config-driven values under the PENPOT_FONT_PROCESS_* namespace. The prlimit implementation detail is not exposed in config keys. Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app> --------- Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app> Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app>
105 lines
3.6 KiB
Clojure
105 lines
3.6 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
|
|
|
(ns app.util.shell
|
|
"A penpot specific, modern api for executing external (shell)
|
|
subprocesses"
|
|
(:require
|
|
[app.common.exceptions :as ex]
|
|
[app.worker :as-alias wrk]
|
|
[datoteka.io :as io]
|
|
[promesa.exec :as px])
|
|
(:import
|
|
java.io.InputStream
|
|
java.io.OutputStream
|
|
java.util.concurrent.TimeUnit
|
|
java.util.List
|
|
org.apache.commons.io.IOUtils))
|
|
|
|
(set! *warn-on-reflection* true)
|
|
|
|
(defn- prlimit-cmd
|
|
"Build a prlimit command prefix from a resource limits map.
|
|
Returns nil if limits is nil/empty."
|
|
[limits]
|
|
(when (seq limits)
|
|
(let [prefix (cond-> ["prlimit"]
|
|
(:mem limits)
|
|
(conj (str "--as=" (* (long (:mem limits)) 1024 1024)))
|
|
|
|
(:cpu limits)
|
|
(conj (str "--cpu=" (long (:cpu limits)))))]
|
|
(conj prefix "--"))))
|
|
|
|
(defn- read-as-bytes
|
|
[in]
|
|
(with-open [^InputStream input (io/input-stream in)]
|
|
(io/read input)))
|
|
|
|
(defn- read-as-string
|
|
([in] (read-as-string in "UTF-8"))
|
|
([in enc]
|
|
(IOUtils/toString ^InputStream in ^String enc)))
|
|
|
|
(defn- read-with-enc
|
|
[stream enc]
|
|
(if (= enc :bytes)
|
|
(read-as-bytes stream)
|
|
(read-as-string stream enc)))
|
|
|
|
(defn- set-env
|
|
[penv k v]
|
|
(.put ^java.util.Map penv
|
|
^String k
|
|
^String v)
|
|
penv)
|
|
|
|
(defn exec!
|
|
[system & {:keys [cmd in out-enc in-enc env prlimit timeout]
|
|
:or {out-enc "UTF-8"
|
|
in-enc "UTF-8"}}]
|
|
(assert (vector? cmd) "a command parameter should be a vector")
|
|
(assert (every? string? cmd) "the command should be a vector of strings")
|
|
|
|
(let [executor (::wrk/executor system)
|
|
_ (assert (some? executor) "executor is required, check ::wrk/executor")
|
|
full-cmd (cond->> cmd
|
|
(seq prlimit)
|
|
(into (prlimit-cmd prlimit)))
|
|
builder (ProcessBuilder. ^List full-cmd)
|
|
env-map (.environment ^ProcessBuilder builder)
|
|
_ (reduce-kv set-env env-map env)
|
|
process (.start builder)]
|
|
|
|
(if in
|
|
(px/run! executor
|
|
(fn []
|
|
(with-open [^OutputStream stdin (.getOutputStream ^Process process)]
|
|
(io/write stdin in :encoding in-enc))))
|
|
(io/close (.getOutputStream ^Process process)))
|
|
|
|
(with-open [stdout (.getInputStream ^Process process)
|
|
stderr (.getErrorStream ^Process process)]
|
|
(let [out (px/submit! executor (fn [] (try (read-with-enc stdout out-enc)
|
|
(catch java.io.IOException _ ""))))
|
|
err (px/submit! executor (fn [] (try (read-as-string stderr)
|
|
(catch java.io.IOException _ ""))))
|
|
ext (if timeout
|
|
(let [completed (.waitFor ^Process process (long timeout) TimeUnit/SECONDS)]
|
|
(if completed
|
|
(.exitValue ^Process process)
|
|
(do
|
|
(.destroyForcibly ^Process process)
|
|
(ex/raise :type :internal
|
|
:code :process-timeout
|
|
:hint (str "process timed out after " timeout " seconds")
|
|
:cmd cmd
|
|
:timeout timeout))))
|
|
(.waitFor ^Process process))]
|
|
{:exit ext
|
|
:out @out
|
|
:err @err}))))
|