Почему IO :: WaitReadable вызывается для STDOUT иначе, чем STDERR? - PullRequest
5 голосов
/ 20 августа 2011

Учитывая, что я хочу проверить неблокирующее чтение из длинной команды, я создал следующий скрипт, сохранил его как long, сделал его исполняемым с chmod 755 и поместил его в мой путь (сохраненный как * 1003). * где ~/bin на моем пути).

Я использую * nix-вариант с ruby 1.9.2p290 (2011-07-09 revision 32553) [x86_64-darwin11.0.0], скомпилированным со значениями по умолчанию RVM. Я не пользуюсь Windows, и поэтому не уверен, что тестовый скрипт подойдет вам.

#!/usr/bin/env ruby

3.times do
  STDOUT.puts 'message on stdout'
  STDERR.puts 'message on stderr'
  sleep 1
end

Почему long_err создает каждое сообщение STDERR, когда оно печатается как "long"

def long_err( bash_cmd = 'long', maxlen = 4096)
  stdin, stdout, stderr = Open3.popen3(bash_cmd)
  begin
    begin
      puts 'err -> ' + stderr.read_nonblock(maxlen)
    end while true
  rescue IO::WaitReadable
    IO.select([stderr])
    retry
  rescue EOFError
    puts 'EOF'
  end
end

в то время как long_out остается заблокированным, пока не будут напечатаны все сообщения STDOUT?

def long_out( bash_cmd = 'long', maxlen = 4096)
  stdin, stdout, stderr = Open3.popen3(bash_cmd)
  begin
    begin
      puts 'out -> ' + stdout.read_nonblock(maxlen)
    end while true
  rescue IO::WaitReadable
    IO.select([stdout])
    retry
  rescue EOFError
    puts 'EOF'
  end
end

Я предполагаю, что вы будете require 'open3' перед проверкой любой функции.

Почему IO::WaitReadable вызывается иначе для STDOUT, чем для STDERR?

Обходные пути с использованием других способов запуска подпроцессов также приветствуется, если они у вас есть.

Ответы [ 2 ]

4 голосов
/ 20 августа 2011

В большинстве ОС STDOUT буферизуется , а STDERR - нет. popen3 в основном открывает канал между исполняемым файлом и Ruby, который вы запускаете.

Любые выходные данные, находящиеся в буферизованном режиме, не передаются через этот канал до тех пор, пока:

  1. Буфер заполнен (что приводит к сбросу).
  2. Отправляющее приложение завершается (EOF достигнут, вызывая сброс).
  3. Поток явно сброшен.

Причина, по которой STDERR не буферизируется, заключается в том, что обычно считается, что сообщения об ошибках должны появляться мгновенно, а не повышать эффективность за счет буферизации.

Итак, зная это, вы можете эмулировать поведение STDERR с помощью STDOUT следующим образом:

#!/usr/bin/env ruby

3.times do
  STDOUT.puts 'message on stdout'
  STDOUT.flush 
  STDERR.puts 'message on stderr'
  sleep 1
end

и вы увидите разницу.

Возможно, вы также захотите проверить « Общие сведения о буферизации Ruby и OS ».

0 голосов
/ 10 февраля 2014

Вот лучшее, что у меня есть для запуска подпроцессов.Я запускаю много сетевых команд, поэтому мне нужен был способ их тайм-аута, если они возвращаются слишком долго.Это должно быть удобно в любой ситуации, когда вы хотите контролировать свой путь выполнения.

Я адаптировал это из Gist , добавив код для проверки состояния выхода команды в течение 3результаты:

  1. Успешное завершение (состояние выхода 0)
  2. Ошибка завершения (состояние выхода не равно нулю) - возникает исключение
  3. Команда истекла и была убита- вызывает исключение

Также исправлено условие гонки, упрощены параметры, добавлено еще несколько комментариев и добавлен код отладки, чтобы помочь мне понять, что происходит с выходами и сигналами.

Вызовите функцию следующим образом:

output = run_with_timeout("command that might time out", 15)

выход будет содержать объединенные STDOUT и STDERR команды, если она завершится успешно.Если команда не завершится в течение 15 секунд, она будет уничтожена и сгенерировано исключение.

Вот функция (2 константы, которые вам нужно определить вверху):

DEBUG = false        # change to true for some debugging info
BUFFER_SIZE = 4096   # in bytes, this should be fine for many applications

def run_with_timeout(command, timeout)
  output = ''
  tick = 1
  begin
    # Start task in another thread, which spawns a process
    stdin, stderrout, thread = Open3.popen2e(command)
    # Get the pid of the spawned process
    pid = thread[:pid]
    start = Time.now

    while (Time.now - start) < timeout and thread.alive?
      # Wait up to `tick' seconds for output/error data
      Kernel.select([stderrout], nil, nil, tick)
      # Try to read the data
      begin
        output << stderrout.read_nonblock(BUFFER_SIZE)
        puts "we read some data..." if DEBUG
      rescue IO::WaitReadable
        # No data was ready to be read during the `tick' which is fine
        print "."       # give feedback each tick that we're waiting
      rescue EOFError
        # Command has completed, not really an error...
        puts "got EOF." if DEBUG
        # Wait briefly for the thread to exit...
        # We don't want to kill the process if it's about to exit on its
        # own. We decide success or failure based on whether the process
        # completes successfully.
        sleep 1
        break
      end
    end

    if thread.alive?
      # The timeout has been reached and the process is still running so
      # we need to kill the process, because killing the thread leaves
      # the process alive but detached.
      Process.kill("TERM", pid)
    end

  ensure
    stdin.close if stdin
    stderrout.close if stderrout
  end

  status = thread.value         # returns Process::Status when process ends

  if DEBUG
    puts "thread.alive?: #{thread.alive?}"
    puts "status: #{status}"
    puts "status.class: #{status.class}"
    puts "status.exited?: #{status.exited?}"
    puts "status.exitstatus: #{status.exitstatus}"
    puts "status.signaled?: #{status.signaled?}"
    puts "status.termsig: #{status.termsig}"
    puts "status.stopsig: #{status.stopsig}"
    puts "status.stopped?: #{status.stopped?}"
    puts "status.success?: #{status.success?}"
  end

  # See how process ended: .success? => true, false or nil if exited? !true
  if status.success? == true       # process exited (0)
    return output
  elsif status.success? == false   # process exited (non-zero)
    raise "command `#{command}' returned non-zero exit status (#{status.exitstatus}), see below output\n#{output}"
  elsif status.signaled?           # we killed the process (timeout reached)
    raise "shell command `#{command}' timed out and was killed (timeout = #{timeout}s): #{status}"
  else
    raise "process didn't exit and wasn't signaled. We shouldn't get to here."
  end
end

Надеюсь, что это полезно.

...