Liên tục đọc từ STDOUT của tiến trình bên ngoài trong Ruby


86

Tôi muốn chạy máy xay sinh tố từ dòng lệnh thông qua một tập lệnh ruby, sau đó sẽ xử lý đầu ra do máy xay sinh tố từng dòng để cập nhật thanh tiến trình trong GUI. Nó không thực sự quan trọng rằng máy xay sinh tố là quá trình bên ngoài mà tôi cần đọc.

Tôi dường như không thể bắt được thông báo tiến trình mà máy xay sinh tố thường in vào vỏ khi quá trình máy xay vẫn đang chạy và tôi đã thử một số cách. Tôi dường như luôn truy cập vào bề mặt của máy xay sinh tố sau khi máy xay sinh tố đã ngừng hoạt động, chứ không phải khi nó vẫn đang chạy.

Đây là một ví dụ về một lần thử không thành công. Nó nhận và in 25 dòng đầu tiên của đầu ra của máy xay sinh tố, nhưng chỉ sau khi quá trình máy xay sinh tố đã thoát ra:

blender = nil
t = Thread.new do
  blender = open "| blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1"
end
puts "Blender is doing its job now..."
25.times { puts blender.gets}

Biên tập:

Để làm cho nó rõ ràng hơn một chút, lệnh gọi máy xay sinh tố trả lại một luồng đầu ra trong trình bao, cho biết tiến trình (đã hoàn thành phần 1-16, v.v.). Có vẻ như bất kỳ lệnh gọi nào để "lấy" đầu ra đều bị chặn cho đến khi máy xay sinh tố ngừng hoạt động. Vấn đề là làm thế nào để truy cập vào đầu ra này trong khi máy xay sinh tố vẫn đang chạy, vì máy xay sinh tố in đầu ra của nó ra vỏ.

Câu trả lời:


175

Tôi đã thành công trong việc giải quyết vấn đề này của mình. Dưới đây là thông tin chi tiết, kèm theo một số giải thích, trong trường hợp bất kỳ ai gặp vấn đề tương tự tìm thấy trang này. Nhưng nếu bạn không quan tâm đến chi tiết, đây là câu trả lời ngắn gọn :

Sử dụng PTY.spawn theo cách sau (tất nhiên là với lệnh của riêng bạn):

require 'pty'
cmd = "blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1" 
begin
  PTY.spawn( cmd ) do |stdout, stdin, pid|
    begin
      # Do stuff with the output here. Just printing to show it works
      stdout.each { |line| print line }
    rescue Errno::EIO
      puts "Errno:EIO error, but this probably just means " +
            "that the process has finished giving output"
    end
  end
rescue PTY::ChildExited
  puts "The child process exited!"
end

đây là câu trả lời dài , với quá nhiều chi tiết:

Vấn đề thực sự dường như là nếu một quy trình không xóa rõ ràng stdout của nó, thì bất kỳ thứ gì được viết vào stdout sẽ được lưu vào bộ đệm thay vì thực sự được gửi, cho đến khi quá trình được thực hiện, để giảm thiểu IO (đây rõ ràng là một chi tiết triển khai của nhiều Thư viện C, được tạo ra để thông lượng được tối đa hóa thông qua IO ít thường xuyên hơn). Nếu bạn có thể dễ dàng sửa đổi quy trình để nó xả cặn thường xuyên, thì đó sẽ là giải pháp của bạn. Trong trường hợp của tôi, đó là máy xay sinh tố, vì vậy hơi đáng sợ đối với một noob hoàn chỉnh như tôi phải sửa đổi nguồn.

Nhưng khi bạn chạy các quy trình này từ shell, chúng hiển thị stdout sang shell trong thời gian thực và stdout dường như không được lưu vào bộ đệm. Tôi tin rằng nó chỉ được đệm khi được gọi từ một quy trình khác, nhưng nếu một shell đang được xử lý, thì stdout sẽ được nhìn thấy trong thời gian thực, không bị đệm.

Hành vi này thậm chí có thể được quan sát với một quy trình ruby ​​vì quy trình con mà đầu ra của nó phải được thu thập trong thời gian thực. Chỉ cần tạo một tập lệnh, random.rb, với dòng sau:

5.times { |i| sleep( 3*rand ); puts "#{i}" }

Sau đó, một tập lệnh ruby ​​để gọi nó và trả về đầu ra của nó:

IO.popen( "ruby random.rb") do |random|
  random.each { |line| puts line }
end

Bạn sẽ thấy rằng bạn không nhận được kết quả trong thời gian thực như bạn có thể mong đợi, nhưng tất cả ngay sau đó. STDOUT đang được lưu vào bộ đệm, mặc dù nếu bạn tự chạy random.rb, nó không được lưu vào bộ đệm. Điều này có thể được giải quyết bằng cách thêm một STDOUT.flushcâu lệnh bên trong khối trong random.rb. Nhưng nếu bạn không thể thay đổi nguồn, bạn phải giải quyết vấn đề này. Bạn không thể xả nó từ bên ngoài quy trình.

Nếu quy trình con có thể in ra shell trong thời gian thực, thì cũng phải có cách để nắm bắt điều này với Ruby trong thời gian thực. Và có. Tôi tin rằng bạn phải sử dụng mô-đun PTY, có trong lõi ruby ​​(1.8.6). Điều đáng buồn là nó không được ghi lại. Nhưng tôi may mắn tìm thấy một số ví dụ về việc sử dụng.

Đầu tiên, để giải thích PTY là gì, nó là viết tắt của pseudo terminal . Về cơ bản, nó cho phép tập lệnh ruby ​​tự trình bày với quy trình con như thể người dùng thực sự vừa nhập lệnh vào một trình bao. Vì vậy, bất kỳ hành vi thay đổi nào chỉ xảy ra khi người dùng đã bắt đầu quá trình thông qua một trình bao (chẳng hạn như STDOUT không được lưu vào bộ đệm, trong trường hợp này) sẽ xảy ra. Việc che giấu sự thật rằng một quá trình khác đã bắt đầu quá trình này cho phép bạn thu thập STDOUT trong thời gian thực, vì nó không được lưu vào bộ đệm.

Để làm cho điều này hoạt động với tập lệnh random.rb ở dạng con, hãy thử mã sau:

require 'pty'
begin
  PTY.spawn( "ruby random.rb" ) do |stdout, stdin, pid|
    begin
      stdout.each { |line| print line }
    rescue Errno::EIO
    end
  end
rescue PTY::ChildExited
  puts "The child process exited!"
end

7
Điều này thật tuyệt, nhưng tôi tin rằng các tham số khối stdin và stdout nên được hoán đổi. Xem: ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/…
Mike Conigliaro

1
Làm thế nào để đóng pty? Giết pid?
Boris B.

Câu trả lời tuyệt vời. Bạn đã giúp tôi cải thiện kịch bản triển khai cào của tôi cho heroku. Nó hiển thị 'git push' nhật ký trong thời gian thực và hủy bỏ nhiệm vụ nếu 'chết người:' tìm thấy gist.github.com/sseletskyy/9248357
Serge Seletskyy

1
Ban đầu tôi đã cố gắng sử dụng phương pháp này nhưng 'pty' không khả dụng trong Windows. Hóa ra STDOUT.sync = truelà tất cả những gì cần thiết (câu trả lời của mveerman bên dưới). Đây là một chủ đề khác với một số mã ví dụ .
Pakman

12

sử dụng IO.popen. Đây là một ví dụ điển hình.

Mã của bạn sẽ trở thành một cái gì đó giống như:

blender = nil
t = Thread.new do
  IO.popen("blender -b mball.blend -o //renders/ -F JPEG -x 1 -f 1") do |blender|
    blender.each do |line|
      puts line
    end
  end
end

Tôi đã thử cái này. Vấn đề là như nhau. Tôi có quyền truy cập vào đầu ra sau đó. Tôi tin rằng IO.popen bắt đầu bằng cách chạy đối số đầu tiên dưới dạng lệnh và đợi nó kết thúc. Trong trường hợp của tôi, đầu ra được cung cấp bởi máy xay sinh tố trong khi máy xay sinh tố vẫn đang xử lý. Và sau đó khối được gọi ra sau đó, điều này không giúp được gì cho tôi.
ehsanul

Đây là những gì tôi đã thử. Nó trả về đầu ra sau khi xay xong: IO.popen ("máy xay sinh tố -b mball.blend // renders / -F JPEG -x 1 -f 1", "w +") do | máy xay sinh tố | máy xay sinh tố.each {| line | đặt dòng; output + = line;} end
ehsanul 20-07-09

3
Tôi không chắc điều gì đang xảy ra trong trường hợp của bạn. Tôi đã thử nghiệm đoạn mã trên với yesmột ứng dụng dòng lệnh không bao giờ kết thúc và nó đã hoạt động. Mã này là như sau: IO.popen('yes') { |p| p.each { |f| puts f } }. Tôi nghi ngờ đó là một cái gì đó liên quan đến máy xay sinh tố, chứ không phải ruby. Có lẽ máy xay sinh tố không phải lúc nào cũng xả sạch STDOUT của nó.
Sinan Taifour,

Ok, tôi chỉ thử nó với một quy trình ruby ​​bên ngoài để kiểm tra, và bạn đã đúng. Có vẻ là một vấn đề máy xay sinh tố. Cảm ơn câu trả lời dù sao.
ehsanul

Hóa ra có một cách để có được đầu ra thông qua ruby, mặc dù máy xay sinh tố không làm sạch bề mặt của nó. Chi tiết ngay trong một câu trả lời riêng biệt, trong trường hợp bạn quan tâm.
ehsanul

6

STDOUT.flush hoặc STDOUT.sync = true


vâng, đây là một câu trả lời khập khiễng. Câu trả lời của bạn đã tốt hơn.
mveerman,

Không khập khiễng! Đã làm cho tôi.
Cầu đất sét vào

Chính xác hơn:STDOUT.sync = true; system('<whatever-command>')
caram

4

Máy xay sinh tố có thể không in ngắt dòng cho đến khi nó kết thúc chương trình. Thay vào đó, nó đang in ký tự xuống dòng (\ r). Giải pháp dễ nhất có lẽ là tìm kiếm tùy chọn kỳ diệu in ngắt dòng với chỉ báo tiến trình.

Vấn đề là IO#gets(và nhiều phương pháp IO khác) sử dụng dấu ngắt dòng làm dấu phân cách. Họ sẽ đọc luồng cho đến khi họ nhấn vào ký tự "\ n" (mà máy xay sinh tố không gửi).

Hãy thử đặt dấu phân tách đầu vào $/ = "\r"hoặc sử dụng blender.gets("\r")thay thế.

BTW, đối với những vấn đề như thế này, bạn phải luôn kiểm tra puts someobj.inspecthoặc p someobj(cả hai đều làm điều tương tự) để xem bất kỳ ký tự ẩn nào trong chuỗi.


1
Tôi vừa kiểm tra đầu ra được đưa ra và có vẻ như máy xay sinh tố sử dụng ngắt dòng (\ n), vì vậy đó không phải là vấn đề. Dù sao cũng cảm ơn mẹo này, tôi sẽ ghi nhớ điều đó vào lần tới khi tôi gỡ lỗi một cái gì đó như thế này.
ehsanul

0

Tôi không biết liệu tại thời điểm ehsanul trả lời câu hỏi, đã Open3::pipeline_rw()có sẵn chưa, nhưng nó thực sự làm cho mọi thứ đơn giản hơn.

Tôi không hiểu công việc của ehsanul với Máy xay sinh tố, vì vậy tôi đã làm một ví dụ khác với tarxz. tarsẽ thêm (các) tệp đầu vào vào luồng stdout, sau đó xzlấy stdouttệp đó và nén lại vào một tệp stdout khác. Công việc của chúng tôi là thực hiện stdout cuối cùng và ghi nó vào tệp cuối cùng của chúng tôi:

require 'open3'

if __FILE__ == $0
    cmd_tar = ['tar', '-cf', '-', '-T', '-']
    cmd_xz = ['xz', '-z', '-9e']
    list_of_files = [...]

    Open3.pipeline_rw(cmd_tar, cmd_xz) do |first_stdin, last_stdout, wait_threads|
        list_of_files.each { |f| first_stdin.puts f }
        first_stdin.close

        # Now start writing to target file
        open(target_file, 'wb') do |target_file_io|
            while (data = last_stdout.read(1024)) do
                target_file_io.write data
            end
        end # open
    end # pipeline_rw
end

0

Câu hỏi cũ, nhưng có vấn đề tương tự.

Nếu không thực sự thay đổi mã Ruby của tôi, một điều đã giúp là gói đường dẫn của tôi bằng stdbuf , như vậy:

cmd = "stdbuf -oL -eL -i0  openssl s_client -connect #{xAPI_ADDRESS}:#{xAPI_PORT}"

@xSess = IO.popen(cmd.split " ", mode = "w+")  

Trong ví dụ của tôi, lệnh thực sự mà tôi muốn tương tác như thể nó là một trình bao, là openssl .

-oL -eL yêu cầu nó chỉ đệm STDOUT và STDERR tối đa một dòng mới. Thay thế Lbằng 0để bỏ bộ đệm hoàn toàn.

Tuy nhiên, điều này không phải lúc nào cũng hoạt động: đôi khi quy trình đích thực thi loại bộ đệm luồng của riêng nó, giống như một câu trả lời khác đã chỉ ra.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.