Skip to content

Commit

Permalink
Replace childprocess gem
Browse files Browse the repository at this point in the history
  • Loading branch information
kapoorlakshya committed Jul 19, 2024
1 parent 92cfe38 commit 1418c3d
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 62 deletions.
4 changes: 2 additions & 2 deletions lib/screen-recorder.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
require 'childprocess'
require 'logger'
require 'os'
require 'streamio-ffmpeg'
Expand Down Expand Up @@ -80,4 +79,5 @@ def self.logger
require 'screen-recorder/common'
require 'screen-recorder/screenshot'
require 'screen-recorder/desktop'
require 'screen-recorder/window'
require 'screen-recorder/window'
require 'screen-recorder/process'
71 changes: 13 additions & 58 deletions lib/screen-recorder/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module ScreenRecorder
#
# @api private
class Common
PROCESS_TIMEOUT = 5 # Seconds to wait for ffmpeg to quit
PROCESS_TIMEOUT = 10 # Seconds to wait for ffmpeg to quit

attr_reader :options, :video

Expand All @@ -21,7 +21,7 @@ def initialize(input:, output:, advanced: {})
#
def start
ScreenRecorder.logger.debug 'Starting recorder...'
@video = nil # New file
@video = nil # New file
@process = start_ffmpeg
ScreenRecorder.logger.info 'Recording...'
@process
Expand All @@ -32,12 +32,11 @@ def start
#
def stop
ScreenRecorder.logger.debug 'Stopping ffmpeg...'
exit_code = stop_ffmpeg
return if exit_code == 1 # recording failed

@process.stop
ScreenRecorder.logger.debug 'Stopped ffmpeg.'
ScreenRecorder.logger.info 'Recording complete.'
@video = prepare_video unless exit_code == 1
ScreenRecorder.logger.info 'Preparing video...'
@video = prepare_video
ScreenRecorder.logger.info 'Recording ready.'
end

#
Expand All @@ -58,7 +57,7 @@ def discard
# the given options.
#
def start_ffmpeg
process = execute_command(ffmpeg_command, options.log)
process = execute_command(ffmpeg_command)
sleep(1.5) # Takes ~1.5s to initialize ffmpeg
# Check if it exited unexpectedly
if process.exited?
Expand All @@ -69,17 +68,6 @@ def start_ffmpeg
process
end

#
# Sends 'q' to the ffmpeg binary to gracefully stop the process.
# Forcefully terminates it if it takes more than 5s.
#
def stop_ffmpeg
@process.io.stdin.puts 'q' # Gracefully exit ffmpeg
@process.io.stdin.close
@log_file.close
wait_for_process_exit(@process)
end

#
# Runs ffprobe on the output video file and returns
# a FFMPEG::Movie object.
Expand Down Expand Up @@ -139,47 +127,14 @@ def lines_from_log(position = :last, count = 2)

#
# Executes the given command and outputs to the
# optional logfile
#
def execute_command(cmd, logfile = nil)
def execute_command(cmd)
ScreenRecorder.logger.debug "Executing command: #{cmd}"
process = new_process(cmd)
process.duplex = true
if logfile
@log_file = File.new(logfile, 'w+')
process.io.stdout = process.io.stderr = @log_file
@log_file.sync = true
end
process.start
process
end

#
# Calls Childprocess.new with OS specific arguments
# to start the given process.
#
def new_process(process)
ChildProcess.posix_spawn = true if RUBY_PLATFORM == 'java' # Support JRuby.
if OS.windows?
ChildProcess.new('cmd.exe', '/c', process)
else
ChildProcess.new('sh', '-c', process)
end
end

#
# Waits for given process to exit.
# Forcefully kills the process if it does not exit within 5 seconds.
# Returns exit code.
#
def wait_for_process_exit(process)
process.poll_for_exit(PROCESS_TIMEOUT)
process.exit_code # 0
rescue ChildProcess::TimeoutError
ScreenRecorder.logger.error 'ffmpeg failed to stop. Force killing it...'
process.stop # Tries increasingly harsher methods to kill the process.
ScreenRecorder.logger.error 'Forcefully killed ffmpeg. Recording failed!'
1
@process = ScreenRecorder::Process.build(cmd)
@process.detach = true
@process.io = options.log
@process.start
@process
end
end
end
112 changes: 112 additions & 0 deletions lib/screen-recorder/process.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# frozen_string_literal: true

module ScreenRecorder
#
# @api private
#
# @since 1.7.0
#
# Original from https://github.com/SeleniumHQ/selenium/blob/trunk/rb/lib/selenium/webdriver/common/child_process.rb
class Process
TimeoutError = Class.new(StandardError)

SIGTERM = 'TERM'
SIGKILL = 'KILL'

POLL_INTERVAL = 0.1

attr_accessor :detach, :pid
attr_writer :io

def self.build(*command)
new(*command)
end

def initialize(*command)
@command = command
@detach = false
@pid = nil
@status = nil
end

def io
@io ||= ::IO.pipe
end

def start
options = { :in => io, %i[out err] => io }
options[:pgroup] = true unless OS.windows? # NOTE: this is a bug only in Windows 7

@pid = ::Process.spawn(*@command, options)
ScreenRecorder.logger.debug(" -> pid: #{@pid}")

::Process.detach(@pid) if detach
end

def stop(timeout = 3)
return unless @pid
return if exited?

ScreenRecorder.logger.debug("Sending TERM to process: #{@pid}")
terminate(-@pid)
poll_for_exit(timeout)

ScreenRecorder.logger.debug(" -> stopped #{@pid}")
rescue TimeoutError, Errno::EINVAL
ScreenRecorder.logger.debug(" -> sending KILL to process: #{@pid}")
kill(@pid)
wait
ScreenRecorder.logger.debug(" -> killed #{@pid}")
end

def alive?
@pid && !exited?
end

def exited?
return false unless @pid

ScreenRecorder.logger.debug("Checking if #{@pid} is exited:")
_, @status = waitpid2(@pid, ::Process::WNOHANG | ::Process::WUNTRACED) if @status.nil?
return false if @status.nil?

exit_code = @status.exitstatus || @status.termsig
ScreenRecorder.logger.debug(" -> exit code is #{exit_code.inspect}")

!!exit_code
end

def poll_for_exit(timeout)
ScreenRecorder.logger.debug("Polling #{timeout} seconds for exit of #{@pid}")

end_time = Time.now + timeout
sleep POLL_INTERVAL until exited? || Time.now > end_time

raise TimeoutError, " -> #{@pid} still alive after #{timeout} seconds" unless exited?
end

def wait
return if exited?

_, @status = waitpid2(@pid)
end

private

def terminate(pid)
::Process.kill(SIGTERM, pid)
end

def kill(pid)
::Process.kill(SIGKILL, pid)
rescue Errno::ECHILD, Errno::ESRCH
# already dead
end

def waitpid2(pid, flags = 0)
::Process.waitpid2(pid, flags)
rescue Errno::ECHILD
# already dead
end
end # Process
end # Common
1 change: 0 additions & 1 deletion screen-recorder.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ Gem::Specification.new do |spec|

spec.require_paths = ['lib']

spec.add_dependency 'childprocess', '>= 1.0', '< 5.0' # Roughly match Selenium
spec.add_dependency 'os', '~> 1.0'
spec.add_dependency 'streamio-ffmpeg', '~> 3.0'
end
3 changes: 2 additions & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require 'screen-recorder'
require 'watir'
require 'webdrivers'
require 'pry-byebug'

RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
Expand All @@ -30,7 +31,7 @@
config.after do |example|
if example.exception
# Print error from ffmpeg.log
log_file = `ls | grep *.log`.strip
log_file = 'ffmpeg.log'
File.open(log_file).readlines.last(10).join('\n') { puts "FFMPEG error: #{f}" } if log_file
end

Expand Down

0 comments on commit 1418c3d

Please sign in to comment.