From 677e905a9627a1adad7c25843d597c908df6b95e Mon Sep 17 00:00:00 2001 From: MAEDA Go Date: Wed, 26 Aug 2026 18:56:42 +0900 Subject: [PATCH 2/2] Fix the gs process surviving when PDF thumbnail generation times out. --- lib/redmine/thumbnail.rb | 14 ++++++++++++-- test/unit/attachment_test.rb | 27 +++++++++++++++++++++------ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/lib/redmine/thumbnail.rb b/lib/redmine/thumbnail.rb index 8d89ae496..d6f354aff 100644 --- a/lib/redmine/thumbnail.rb +++ b/lib/redmine/thumbnail.rb @@ -58,7 +58,12 @@ module Redmine pid = nil begin Timeout.timeout(Redmine::Configuration['thumbnails_generation_timeout'].to_i) do - pid = Process.spawn(cmd) + # Run the command in its own process group so that the whole + # process tree (eg. the gs process spawned by convert for PDF + # files) can be killed on timeout. Process groups are not + # available on Windows. + spawn_options = Redmine::Platform.mswin? ? {} : {:pgroup => true} + pid = Process.spawn(cmd, spawn_options) _, status = Process.wait2(pid) unless status.success? logger.error("Creating thumbnail failed (#{status.exitstatus}):\nCommand: #{cmd}") @@ -67,7 +72,12 @@ module Redmine end rescue Timeout::Error if pid - Process.kill('KILL', pid) + begin + # A negative pid sends the signal to the whole process group + Process.kill('KILL', Redmine::Platform.mswin? ? pid : -pid) + rescue Errno::ESRCH + # The process is already gone + end Process.detach(pid) end logger.error("Creating thumbnail timed out:\nCommand: #{cmd}") diff --git a/test/unit/attachment_test.rb b/test/unit/attachment_test.rb index 820feafde..7cc658c8e 100644 --- a/test/unit/attachment_test.rb +++ b/test/unit/attachment_test.rb @@ -787,24 +787,39 @@ class AttachmentTest < ActiveSupport::TestCase set_tmp_attachments_directory end - def test_thumbnail_should_timeout + def test_thumbnail_should_kill_and_detach_process_group_on_timeout dummy_pid = 37530 - Process.stubs(:spawn).returns(dummy_pid) - Process.stubs(:wait2).raises(Timeout::Error) - Process.stubs(:kill).returns(1) - Process.stubs(:wait).returns(dummy_pid) + + Redmine::Platform.stubs(:mswin?).returns(false) + Redmine::Thumbnail.stubs(:gs_available?).returns(true) + Process.expects(:spawn). + with(anything, {:pgroup => true}). + returns(dummy_pid) + Process.expects(:wait2). + with(dummy_pid). + raises(Timeout::Error) + # A negative pid sends the signal to the whole process group so that + # child processes of convert (eg. gs) are killed as well + Process.expects(:kill). + with('KILL', -dummy_pid). + returns(1) + Process.expects(:detach). + with(dummy_pid) Rails.logger.expects(:error).with(regexp_matches(/Creating thumbnail timed out/)) set_fixtures_attachments_directory Attachment.clear_thumbnails - attachment = Attachment.find(16) + # Use a PDF attachment because convert spawns a Ghostscript (gs) + # child process for PDFs, which must not survive the timeout + attachment = Attachment.find(23) thumbnail = attachment.thumbnail assert_nil thumbnail ensure set_tmp_attachments_directory end + else puts '(ImageMagick convert not available)' end -- 2.55.0