diff --git a/filter/tex/lib.php b/filter/tex/lib.php
index a1d296ab9e4..86f7ab96710 100644
--- a/filter/tex/lib.php
+++ b/filter/tex/lib.php
@@ -26,10 +26,22 @@
defined('MOODLE_INTERNAL') || die();
+// Default timeout in seconds for mimetex command execution.
+defined('FILTER_TEX_MIMETEX_TIMEOUT') || define('FILTER_TEX_MIMETEX_TIMEOUT', 5);
+
+/**
+ * Check if the current operating system is Windows.
+ *
+ * @return bool True if running on Windows, false otherwise.
+ */
+function filter_tex_is_windows(): bool {
+ return (PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows");
+}
+
function filter_tex_get_executable($debug=false) {
global $CFG;
- if ((PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) {
+ if (filter_tex_is_windows()) {
return "$CFG->dirroot/filter/tex/mimetex.exe";
}
@@ -134,7 +146,7 @@ function filter_tex_get_cmd($pathname, $texexp) {
$texexp = escapeshellarg($texexp);
$executable = filter_tex_get_executable(false);
- if ((PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) {
+ if (filter_tex_is_windows()) {
$executable = str_replace(' ', '^ ', $executable);
return "$executable ++ -e \"$pathname\" -- $texexp";
@@ -143,6 +155,173 @@ function filter_tex_get_cmd($pathname, $texexp) {
}
}
+/**
+ * Run mimetex command with a timeout on Windows.
+ *
+ * @param string $cmd Command string to execute.
+ * @param int $timeoutmicros Timeout in microseconds.
+ * @return array Array with keys: code, timedout, status, errors.
+ */
+function filter_tex_exec_windows(string $cmd, int $timeoutmicros): array {
+ // Create temporary file for stderr.
+ $temperr = tempnam(sys_get_temp_dir(), 'err_');
+
+ $descriptors = [
+ 0 => ['file', 'NUL', 'r'], // STDIN.
+ 1 => ['file', 'NUL', 'w'], // STDOUT.
+ 2 => ['file', $temperr, 'w'], // STDERR.
+ ];
+
+ $process = proc_open($cmd, $descriptors, $pipes);
+ if (!is_resource($process)) {
+ unlink($temperr);
+ return [
+ 'code' => 127, // Command not found.
+ 'timedout' => false,
+ 'status' => [],
+ 'errors' => '',
+ ];
+ }
+
+ $timedout = false;
+ while ($timeoutmicros > 0) {
+ $start = microtime(true);
+ $status = proc_get_status($process);
+
+ if (!$status['running']) {
+ break;
+ }
+
+ $timeoutmicros -= (microtime(true) - $start) * 1000000;
+ if ($timeoutmicros <= 0) {
+ $timedout = true;
+ $pid = (int)($status['pid'] ?? 0);
+ exec('taskkill /F /T /PID ' . $pid . ' 2>NUL');
+ break;
+ }
+
+ usleep(50000); // Sleep for 50ms.
+ }
+
+ $status = proc_get_status($process);
+ $code = proc_close($process);
+
+ // Capture stderr from temp file.
+ $errors = file_get_contents($temperr);
+ unlink($temperr);
+
+ return [
+ 'code' => $code,
+ 'timedout' => $timedout,
+ 'status' => $status,
+ 'errors' => $errors,
+ ];
+}
+
+/**
+ * Run mimetex command with a timeout on Unix-like systems.
+ *
+ * @param string $cmd Command string to execute.
+ * @param int $timeoutmicros Timeout in microseconds.
+ * @return array Array with keys: code, timedout, status, errors.
+ */
+function filter_tex_exec_unix(string $cmd, int $timeoutmicros): array {
+ // File descriptors passed to the process.
+ $descriptors = [
+ 0 => ['pipe', 'r'], // STDIN.
+ 1 => ['pipe', 'w'], // STDOUT.
+ 2 => ['pipe', 'w'], // STDERR.
+ ];
+
+ $process = proc_open('exec ' . $cmd, $descriptors, $pipes);
+ if (!is_resource($process)) {
+ return [
+ 'code' => 127, // Command not found.
+ 'timedout' => false,
+ 'status' => [],
+ 'errors' => '',
+ ];
+ }
+
+ fclose($pipes[0]);
+ stream_set_blocking($pipes[1], false);
+ stream_set_blocking($pipes[2], false);
+
+ $errors = '';
+ $timedout = false;
+ while ($timeoutmicros > 0) {
+ $start = microtime(true);
+
+ $read = [$pipes[1], $pipes[2]];
+ $other = [];
+ stream_select($read, $other, $other, 0, (int)$timeoutmicros);
+
+ $status = proc_get_status($process);
+
+ stream_get_contents($pipes[1]); // Discard stdout to prevent pipe blocking.
+ $errors .= stream_get_contents($pipes[2]);
+
+ if (!$status['running']) {
+ break;
+ }
+
+ $timeoutmicros -= (microtime(true) - $start) * 1000000;
+ if ($timeoutmicros <= 0) {
+ $timedout = true;
+ proc_terminate($process);
+ break;
+ }
+
+ usleep(50000); // Sleep for 50ms.
+ }
+
+ // Read any remaining data from pipes.
+ stream_get_contents($pipes[1]); // Discard remaining stdout.
+ $errors .= stream_get_contents($pipes[2]);
+
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+
+ $status = proc_get_status($process);
+ $code = proc_close($process);
+
+ return [
+ 'code' => $code,
+ 'timedout' => $timedout,
+ 'status' => $status,
+ 'errors' => $errors,
+ ];
+}
+
+/**
+ * Run mimetex command with a timeout.
+ *
+ * @param string $cmd Command string to execute.
+ * @param int|null &$code Exit code (passed by reference, set by function).
+ * @return void
+ */
+function filter_tex_exec(string $cmd, ?int &$code): void {
+ $timeoutmicros = FILTER_TEX_MIMETEX_TIMEOUT * 1000000;
+
+ if (filter_tex_is_windows()) {
+ $result = filter_tex_exec_windows($cmd, $timeoutmicros);
+ } else {
+ $result = filter_tex_exec_unix($cmd, $timeoutmicros);
+ }
+
+ if ($result['errors']) {
+ debugging('filter_tex_exec errors: ' . $result['errors'], DEBUG_DEVELOPER);
+ }
+
+ if ($result['timedout']) {
+ $code = 124;
+ } else if ($result['code'] === -1 && isset($result['status']['exitcode']) && $result['status']['exitcode'] !== -1) {
+ $code = $result['status']['exitcode'];
+ } else {
+ $code = $result['code'];
+ }
+}
+
/**
* Purge all caches when settings changed.
*/
diff --git a/filter/tex/pix.php b/filter/tex/pix.php
index e4697cf48ff..a30317aef4d 100644
--- a/filter/tex/pix.php
+++ b/filter/tex/pix.php
@@ -18,7 +18,6 @@ define('NO_MOODLE_COOKIES', true); // Because it interferes with caching
require_once($CFG->dirroot.'/filter/tex/latex.php');
$cmd = ''; // Initialise these variables
- $status = '';
$relativepath = get_file_argument();
@@ -58,7 +57,7 @@ define('NO_MOODLE_COOKIES', true); // Because it interferes with caching
$texexp = preg_replace('!\r\n?!', ' ', $texexp);
$texexp = '\Large '.$texexp;
$cmd = filter_tex_get_cmd($pathname, $texexp);
- system($cmd, $status);
+ filter_tex_exec($cmd, $status);
}
}
}
diff --git a/filter/tex/texdebug.php b/filter/tex/texdebug.php
index 25481b3101e..6570ac64cd8 100644
--- a/filter/tex/texdebug.php
+++ b/filter/tex/texdebug.php
@@ -153,7 +153,7 @@
$texexp = '\Large '.$texexp;
$commandpath = filter_tex_get_executable(true);
$cmd = filter_tex_get_cmd($pathname, $texexp);
- system($cmd, $status);
+ filter_tex_exec($cmd, $status);
if ($return) {
return $image;
@@ -172,6 +172,10 @@
echo "Status corresponds to bus error
\n";
} else if ($status == 22) {
echo "Status corresponds to abnormal termination
\n";
+ } else if ($status == 124) {
+ echo "Status corresponds to timeout
\n";
+ } else if ($status == 127) {
+ echo "Status corresponds to command not found
\n";
}
if (file_exists($commandpath)) {
echo "File size of mimetex executable $commandpath is " . filesize($commandpath) . "
";