module Autobuild::Subprocess

Constants

CONTROL_COMMAND_NOT_FOUND
CONTROL_INTERRUPT
CONTROL_UNEXPECTED

Public Class Methods

run(target, phase, *command) { |line| ... } click to toggle source

Run a subcommand and return its standard output

The command’s standard and error outputs, as well as the full command line and an environment dump are saved in a log file in either the valure returned by target#logdir, or Autobuild.logdir if the target does not respond to logdir.

The subprocess priority is controlled by Autobuild.nice

@param [String,(name,#logdir,#working_directory)] target the target we

run the subcommand for. In general, it will be a Package object (run from
Package#run)

@param [String] phase in which build phase this subcommand is executed @param [Array<String>] the command itself @yieldparam [String] line if a block is given, each output line from the

command's standard output are yield to it. This is meant for progress
display, and is disabled if Autobuild.verbose is set.

@param [Hash] options @option options [String] :working_directory the directory in which the

command should be started. If nil, runs in the current directory. The
default is to either use the value returned by #working_directory on
{target} if it responds to it, or nil.

@option options [Boolean] :retry (false) controls whether a failure to

execute this command should be retried by autobuild retry mechanisms (i.e.
in the importers) or not. {run} will not retry the command by itself, it
is passed as a hint for error handling clauses about whether the error
should be retried or not

@option options [Array<IO>] :input_streams list of input streams that

should be fed to the command standard input. If a file needs to be given,
the :input argument can be used as well as a shortcut

@option options [String] :input the path to a file whose content should be

fed to the command standard input

@return [String] the command standard output

# File lib/autobuild/subcommand.rb, line 215
def self.run(target, phase, *command)
    STDOUT.sync = true

    input_streams = []
    options = {
        retry: false, encoding: 'BINARY',
        env: ENV.to_hash, env_inherit: true
    }

    if command.last.kind_of?(Hash)
        options = command.pop
        options = Kernel.validate_options(
            options,
            input: nil, working_directory: nil, retry: false,
            input_streams: [],
            env: ENV.to_hash,
            env_inherit: true,
            encoding: 'BINARY'
        )

        input_streams << File.open(options[:input]) if options[:input]
        input_streams.concat(options[:input_streams]) if options[:input_streams]
    end

    start_time = Time.now

    # Filter nil and empty? in command
    command.reject! { |o| o.nil? || (o.respond_to?(:empty?) && o.empty?) }
    command.collect!(&:to_s)

    if target.respond_to?(:name)
        target_name = target.name
        target_type = target.class
    else
        target_name = target.to_str
        target_type = nil
    end
    logdir = if target.respond_to?(:logdir)
                 target.logdir
             else
                 Autobuild.logdir
             end

    if target.respond_to?(:working_directory)
        options[:working_directory] ||= target.working_directory
    end

    logname = File.join(logdir, "#{target_name.gsub(/:/, '_')}-"\
        "#{phase.to_s.gsub(/:/, '_')}.log")
    unless File.directory?(File.dirname(logname))
        FileUtils.mkdir_p File.dirname(logname)
    end

    if Autobuild.verbose
        Autobuild.message "#{target_name}: running #{command.join(' ')}\n"\
            "    (output goes to #{logname})"
    end

    open_flag = if Autobuild.keep_oldlogs then 'a'
                elsif Autobuild.registered_logfile?(logname) then 'a'
                else
                    'w'
                end
    open_flag << ":BINARY"

    Autobuild.register_logfile(logname)
    subcommand_output = Array.new

    env = options[:env].dup
    if options[:env_inherit]
        ENV.each do |k, v|
            env[k] = v unless env.key?(k)
        end
    end

    status = File.open(logname, open_flag) do |logfile|
        logfile.puts if Autobuild.keep_oldlogs
        logfile.puts
        logfile.puts "#{Time.now}: running"
        logfile.puts "    #{command.join(' ')}"
        logfile.puts "with environment:"
        env.keys.sort.each do |key|
            if (value = env[key])
                logfile.puts "  '#{key}'='#{value}'"
            end
        end
        logfile.puts
        logfile.puts "#{Time.now}: running"
        logfile.puts "    #{command.join(' ')}"
        logfile.flush
        logfile.sync = true

        unless input_streams.empty?
            pread, pwrite = IO.pipe # to feed subprocess stdin
        end

        outread, outwrite = IO.pipe
        outread.sync = true
        outwrite.sync = true

        cread, cwrite = IO.pipe # to control that exec goes well

        if Autobuild.windows?
            Dir.chdir(options[:working_directory]) do
                unless system(*command)
                    exit_code = $CHILD_STATUS.exitstatus
                    raise Failed.new(exit_code, nil),
                          "'#{command.join(' ')}' returned status #{exit_code}"
                end
            end
            return # rubocop:disable Lint/NonLocalExitFromIterator
        end

        cwrite.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)

        pid = fork do
                logfile.puts "in directory #{options[:working_directory] || Dir.pwd}"

                cwrite.sync = true
                if Autobuild.nice
                    Process.setpriority(Process::PRIO_PROCESS, 0, Autobuild.nice)
                end

                outread.close
                $stderr.reopen(outwrite.dup)
                $stdout.reopen(outwrite.dup)

                unless input_streams.empty?
                    pwrite.close
                    $stdin.reopen(pread)
                end

                exec(env, *command,
                     chdir: options[:working_directory] || Dir.pwd,
                     close_others: false)
        rescue Errno::ENOENT
                cwrite.write([CONTROL_COMMAND_NOT_FOUND].pack('I'))
                exit(100)
        rescue Interrupt
                cwrite.write([CONTROL_INTERRUPT].pack('I'))
                exit(100)
        rescue ::Exception => e
                STDERR.puts e
                STDERR.puts e.backtrace.join("\n  ")
                cwrite.write([CONTROL_UNEXPECTED].pack('I'))
                exit(100)
        end

        readbuffer = StringIO.new

        # Feed the input
        unless input_streams.empty?
            pread.close
            begin
                input_streams.each do |instream|
                    instream.each_line do |line|
                        while IO.select([outread], nil, nil, 0)
                            readbuffer.write(outread.readpartial(128))
                        end
                        pwrite.write(line)
                    end
                end
            rescue Errno::ENOENT => e
                raise Failed.new(nil, false),
                      "cannot open input files: #{e.message}", retry: false
            end
            pwrite.close
        end

        # Get control status
        cwrite.close
        value = cread.read(4)
        if value
            # An error occured
            value = value.unpack1('I')
            case value
            when CONTROL_COMMAND_NOT_FOUND
                raise Failed.new(nil, false), "command '#{command.first}' not found"
            when CONTROL_INTERRUPT
                raise Interrupt, "command '#{command.first}': interrupted by user"
            else
                raise Failed.new(nil, false), "something unexpected happened"
            end
        end

        transparent_prefix = "#{target_name}:#{phase}: "
        transparent_prefix = "#{target_type}:#{transparent_prefix}" if target_type

        # If the caller asked for process output, provide it to him
        # line-by-line.
        outwrite.close

        unless input_streams.empty?
            readbuffer.write(outread.read)
            readbuffer.seek(0)
            outread.close
            outread = readbuffer
        end

        outread.each_line do |line|
            line.force_encoding(options[:encoding])
            line = line.chomp
            subcommand_output << line

            logfile.puts line

            if Autobuild.verbose || transparent_mode?
                STDOUT.puts "#{transparent_prefix}#{line}"
            elsif block_given?
                # Do not yield
                # would mix the progress output with the actual command
                # output. Assume that if the user wants the command output,
                # the autobuild progress output is unnecessary
                yield(line)
            end
        end
        outread.close

        _, childstatus = Process.wait2(pid)
        logfile.puts "Exit: #{childstatus}"
        childstatus
    end

    if !status.exitstatus || status.exitstatus > 0
        if status.termsig == 2 # SIGINT == 2
            raise Interrupt, "subcommand #{command.join(' ')} interrupted"
        end

        if status.termsig
            raise Failed.new(status.exitstatus, nil),
                  "'#{command.join(' ')}' terminated by signal #{status.termsig}"
        else
            raise Failed.new(status.exitstatus, nil),
                  "'#{command.join(' ')}' returned status #{status.exitstatus}"
        end
    end

    duration = Time.now - start_time
    Autobuild.add_stat(target, phase, duration)
    FileUtils.mkdir_p(Autobuild.logdir)
    File.open(File.join(Autobuild.logdir, "stats.log"), 'a') do |io|
        formatted_msec = format('%.03i', start_time.tv_usec / 1000)
        formatted_time = "#{start_time.strftime('%F %H:%M:%S')}.#{formatted_msec}"
        io.puts "#{formatted_time} #{target_name} #{phase} #{duration}"
    end
    target.add_stat(phase, duration) if target.respond_to?(:add_stat)
    subcommand_output
rescue Failed => e
    error = Autobuild::SubcommandFailed.new(target, command.join(" "),
                                            logname, e.status, subcommand_output)
    error.retry = if e.retry?.nil? then options[:retry]
                  else
                      e.retry?
                  end
    error.phase = phase
    raise error, e.message
end
transparent_mode=(flag) click to toggle source
# File lib/autobuild/subcommand.rb, line 178
def self.transparent_mode=(flag)
    @transparent_mode = flag
end
transparent_mode?() click to toggle source
# File lib/autobuild/subcommand.rb, line 174
def self.transparent_mode?
    @transparent_mode
end