module Preview class Screenshot class Unavailable < StandardError; end class Busy < Unavailable; end class TimedOut < Unavailable; end LIMIT = ENV.fetch('PREVIEW_SCREENSHOT_CONCURRENCY', 2).to_i.clamp(1, 10) TIMEOUT = ENV.fetch('PREVIEW_SCREENSHOT_TIMEOUT', 20).to_i.clamp(5, 60) @mutex = Mutex.new @active = 0 def self.capture(url) acquire! tempfile = nil pid = nil begin tempfile = Tempfile.new(['preview-thumbnail', '.png']) tempfile.close status = nil pid = Process.spawn( 'node', Rails.root.join('lib/screenshot.js').to_s, url, tempfile.path, out: File::NULL, err: File::NULL, pgroup: true) Timeout.timeout(TIMEOUT) do _finished_pid, status = Process.wait2(pid) end raise Unavailable, 'スクリーンショットを生成できませんでした.' unless status.success? raise Unavailable, 'スクリーンショットを生成できませんでした.' unless File.size?(tempfile.path) File.binread(tempfile.path) rescue Timeout::Error terminate(pid) raise TimedOut, 'スクリーンショット生成が timeout しました.' ensure tempfile&.unlink release! end end def self.acquire! @mutex.synchronize do raise Busy, 'スクリーンショット生成が混雑してゐます.' if @active >= LIMIT @active += 1 end end def self.release! @mutex.synchronize { @active -= 1 } end def self.terminate(pid) return unless pid Process.kill('TERM', -pid) Process.wait(pid) rescue Errno::ESRCH, Errno::ECHILD nil end private_class_method :acquire!, :release!, :terminate end end