From 81b32378cadafd414fabea9e6cff23f07306b6ca Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 31 Mar 2026 19:53:08 +0900 Subject: [PATCH] Support timeouts for ffmpeg and magick commands https://rhymix.org/qna/1935749 --- modules/file/file.admin.controller.php | 8 ++++++++ modules/file/file.controller.php | 24 ++++++++++++++++++++++++ modules/file/lang/en.php | 16 ++++++++++------ modules/file/lang/ko.php | 4 ++++ modules/file/tpl/upload_config.html | 16 ++++++++++++++++ 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/modules/file/file.admin.controller.php b/modules/file/file.admin.controller.php index 3cb11c989..ef3f8d523 100644 --- a/modules/file/file.admin.controller.php +++ b/modules/file/file.admin.controller.php @@ -122,6 +122,10 @@ class FileAdminController extends File $config->magick_command = escape(utf8_trim(Context::get('magick_command'))) ?: ''; } + // Timeouts + $config->ffmpeg_timeout = max(0, intval(Context::get('ffmpeg_timeout'))) ?: null; + $config->magick_timeout = max(0, intval(Context::get('magick_timeout'))) ?: null; + // Check maximum file size (probably not necessary anymore) if (PHP_INT_SIZE < 8) { @@ -441,6 +445,10 @@ class FileAdminController extends File '-limit memory 64MB -limit map 128MB -limit disk 1GB', escapeshellarg($temp_filename), ]); + if (!\RX_WINDOWS && isset($config->magick_timeout) && $config->magick_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->magick_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); $result = $return_var === 0 ? true : false; } diff --git a/modules/file/file.controller.php b/modules/file/file.controller.php index 81d1419db..6c73d5cb9 100644 --- a/modules/file/file.controller.php +++ b/modules/file/file.controller.php @@ -1166,6 +1166,10 @@ class FileController extends File { $command = \RX_WINDOWS ? escapeshellarg($config->magick_command) : $config->magick_command; $command .= ' identify ' . escapeshellarg($file_info['tmp_name']); + if (!\RX_WINDOWS && isset($config->magick_timeout) && $config->magick_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->magick_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); if ($return_var === 0 && preg_match('/([A-Z]+) ([0-9]+)x([0-9]+)/', substr(array_last($output), strlen($file_info['tmp_name'])), $matches)) { @@ -1335,6 +1339,10 @@ class FileController extends File $command .= ' -movflags +faststart -pix_fmt yuv420p -c:v libx264 -crf 23'; $command .= sprintf(' -vf "scale=%d:%d"', $adjusted['width'], $adjusted['height']); $command .= ' ' . escapeshellarg($output_name); + if (!\RX_WINDOWS && isset($config->ffmpeg_timeout) && $config->ffmpeg_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->ffmpeg_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); $result = $return_var === 0 ? true : false; @@ -1365,6 +1373,10 @@ class FileController extends File '-limit memory 64MB -limit map 128MB -limit disk 1GB', escapeshellarg($output_name), ]); + if (!\RX_WINDOWS && isset($config->magick_timeout) && $config->magick_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->magick_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); $result = $return_var === 0 ? true : false; } @@ -1386,6 +1398,10 @@ class FileController extends File '-limit memory 64MB -limit map 128MB -limit disk 1GB', escapeshellarg($output_name), ]); + if (!\RX_WINDOWS && isset($config->magick_timeout) && $config->magick_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->magick_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); $result = $return_var === 0 ? true : false; } @@ -1572,6 +1588,10 @@ class FileController extends File $command .= empty($stream_info['audio']) ? ' -an' : ' -acodec aac'; $command .= sprintf(' -vf "scale=%d:%d"', $adjusted['width'], $adjusted['height']); $command .= ' ' . escapeshellarg($output_name); + if (!\RX_WINDOWS && isset($config->ffmpeg_timeout) && $config->ffmpeg_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->ffmpeg_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); $result = $return_var === 0 ? true : false; @@ -1604,6 +1624,10 @@ class FileController extends File $command = \RX_WINDOWS ? escapeshellarg($config->ffmpeg_command) : $config->ffmpeg_command; $command .= sprintf(' -ss 00:00:00.%d -i %s -vframes 1', mt_rand(0, 99), escapeshellarg($file_info['tmp_name'])); $command .= ' -nostdin ' . escapeshellarg($thumbnail_name); + if (!\RX_WINDOWS && isset($config->ffmpeg_timeout) && $config->ffmpeg_timeout > 0) + { + $command = 'timeout -k1 ' . intval($config->ffmpeg_timeout) . ' ' . $command; + } @exec($command, $output, $return_var); if ($return_var === 0) { diff --git a/modules/file/lang/en.php b/modules/file/lang/en.php index 152af21e4..9598448d8 100644 --- a/modules/file/lang/en.php +++ b/modules/file/lang/en.php @@ -104,15 +104,15 @@ $lang->max_image_size_same_format_to_jpg = 'Convert to JPG'; $lang->max_image_size_same_format_to_webp = 'Convert to WebP'; $lang->max_image_size_admin = 'Also apply to administrator'; $lang->image_quality_adjustment = 'Image Quality'; -$lang->about_image_quality_adjustment = 'adjust the quality of images that will is converted by other settings.
If set to more than 75% (Standard), the file size may be larger than the original.'; +$lang->about_image_quality_adjustment = 'Adjust the quality of images that will be converted by other settings.
If set to more than 75% (Standard), the file size may be larger than the original.'; $lang->image_autorotate = 'Fix Image Rotation'; -$lang->about_image_autorotate = 'correct images that are rotated by mobile devices.'; +$lang->about_image_autorotate = 'Correct images that are rotated by mobile devices.'; $lang->image_remove_exif_data = 'Remove EXIF'; -$lang->about_image_remove_exif_data = 'remove EXIF data including camera, GPS information, and more in image file for privacy.
Even if this option is not used, EXIF ​​data may be removed when the image is converted by other settings.'; +$lang->about_image_remove_exif_data = 'Remove EXIF data including camera, GPS information, and more in image file for privacy.
Even if this option is not used, EXIF ​​data may be removed when the image is converted by other settings.'; $lang->image_always_reencode = 'Always Reencode'; $lang->about_image_always_reencode = 'Reencode images to a constant quality even if they do not meet one of the conditions above. This may help save disk space and traffic.'; $lang->image_autoconv_gif2mp4 = 'Convert GIF to MP4'; -$lang->about_image_autoconv_gif2mp4 = 'convert animated GIF images into MP4 videos to save storage and bandwidth.
This requires ffmpeg settings below. Videos may not play properly in older browsers.'; +$lang->about_image_autoconv_gif2mp4 = 'Convert animated GIF images into MP4 videos to save storage and bandwidth.
This requires ffmpeg settings below. Videos may not play properly in older browsers.'; $lang->max_video_size = 'Limit Video Size'; $lang->about_max_video_size = 'Limit the dimensions of uploaded videos. Note that this is only indirectly related to file size.'; $lang->max_video_duration = 'Limit Video Duration'; @@ -122,15 +122,19 @@ $lang->about_video_autoconv_any2mp4 = 'Convert all other types of videos to MP4 $lang->video_always_reencode = 'Always Reencode'; $lang->about_video_always_reencode = 'Reencode videos to a constant quality even if they do not meet one of the conditions above. This may help save disk space and traffic.'; $lang->video_thumbnail = 'Video Thumbnail'; -$lang->about_video_thumbnail = 'extract a thumbnail image from uploaded video.'; +$lang->about_video_thumbnail = 'Extract a thumbnail image from uploaded video.'; $lang->video_mp4_gif_time = 'Play Like GIF'; -$lang->about_video_mp4_gif_time = 'treat silent MP4 videos with duration less than the set time as GIF images, and play with auto and loop.'; +$lang->about_video_mp4_gif_time = 'Treat silent MP4 videos with duration less than the set time as GIF images, and play with auto and loop.'; $lang->external_program_paths = 'Paths to External Programs'; $lang->ffmpeg_path = 'Absolute Path to ffmpeg'; $lang->ffprobe_path = 'Absolute Path to ffprobe'; +$lang->ffmpeg_timeout = 'ffmpeg Timeout'; $lang->magick_path = 'Absolute Path to magick'; +$lang->magick_timeout = 'magick Timeout'; $lang->about_ffmpeg_path = 'Rhymix uses ffmpeg to convert video files.'; +$lang->about_ffmpeg_timeout = 'If the video conversion task is not completed within a certain time, it will be terminated.
Proper timeout settings can help manage server load.
However, if set longer than the PHP execution time limit (%d seconds), the conversion result will not be saved.'; $lang->about_magick_path = 'Rhymix uses magick to convert newer image formats such as AVIF and HEIC.
Note that the \'convert\' command from previous versions of ImageMagick doesn\'t support these formats.
The latest version can be downloaded from their official site.'; +$lang->about_magick_timeout = 'If the image conversion task is not completed within a certain time, it will be terminated.
Proper timeout settings can help manage server load.
However, if set longer than the PHP execution time limit (%d seconds), the conversion result will not be saved.'; $lang->msg_cannot_use_exec = 'The exec() function is disabled on this server.'; $lang->msg_cannot_use_ffmpeg = 'In order to use this feature, PHP must be able to execute \'ffmpeg\' and \'ffprobe\' commands.'; $lang->msg_cannot_use_exif = 'In order to use this feature, PHP must be installed with the \'exif\' extension.'; diff --git a/modules/file/lang/ko.php b/modules/file/lang/ko.php index d8d27ee9a..58392862f 100644 --- a/modules/file/lang/ko.php +++ b/modules/file/lang/ko.php @@ -128,9 +128,13 @@ $lang->about_video_mp4_gif_time = '설정된 시간 이하의 길이를 가진 $lang->external_program_paths = '외부 프로그램 경로'; $lang->ffmpeg_path = 'ffmpeg 절대경로'; $lang->ffprobe_path = 'ffprobe 절대경로'; +$lang->ffmpeg_timeout = 'ffmpeg 타임아웃'; $lang->magick_path = 'magick 절대경로'; +$lang->magick_timeout = 'magick 타임아웃'; $lang->about_ffmpeg_path = '동영상 변환에 사용합니다.'; +$lang->about_ffmpeg_timeout = '동영상 변환 작업이 일정 시간 안에 완료되지 않으면 강제로 종료합니다.
적절한 타임아웃 설정은 서버 부하 관리에 도움이 됩니다.
단, PHP 실행 제한 시간(%d초)보다 길게 설정할 경우 변환 결과가 저장되지 않습니다.'; $lang->about_magick_path = 'AVIF, HEIC 등 일부 이미지 변환에 사용합니다.
구 버전 ImageMagick의 convert 명령은 이러한 포맷을 지원하지 않습니다.
새 버전은 공식 사이트에서 다운받을 수 있습니다.'; +$lang->about_magick_timeout = '이미지 변환 작업이 일정 시간 안에 완료되지 않으면 강제로 종료합니다.
적절한 타임아웃 설정은 서버 부하 관리에 도움이 됩니다.
단, PHP 실행 제한 시간(%d초)보다 길게 설정할 경우 변환 결과가 저장되지 않습니다.'; $lang->msg_cannot_use_exec = '이 서버에서 exec() 함수를 사용할 수 없습니다.'; $lang->msg_cannot_use_ffmpeg = '이 기능을 사용하려면 PHP에서 ffmpeg 및 ffprobe 명령을 실행할 수 있어야 합니다.'; $lang->msg_cannot_use_exif = '이 기능을 사용하려면 PHP exif 확장모듈이 필요합니다.'; diff --git a/modules/file/tpl/upload_config.html b/modules/file/tpl/upload_config.html index bbdf9248e..932b6eb41 100644 --- a/modules/file/tpl/upload_config.html +++ b/modules/file/tpl/upload_config.html @@ -283,6 +283,14 @@

{$lang->msg_cannot_use_exec}

+
+ +
+ {$lang->unit_sec} +

{sprintf($lang->about_ffmpeg_timeout, ini_get('max_execution_time'))}

+

{$lang->msg_cannot_use_exec}

+
+
@@ -291,6 +299,14 @@

{$lang->msg_cannot_use_exec}

+
+ +
+ {$lang->unit_sec} +

{sprintf($lang->about_magick_timeout, ini_get('max_execution_time'))}

+

{$lang->msg_cannot_use_exec}

+
+