อ่านอย่างต่อเนื่องจาก STDOUT ของกระบวนการภายนอกใน Ruby


86

ฉันต้องการเรียกใช้เครื่องปั่นจากบรรทัดคำสั่งผ่านสคริปต์ทับทิมซึ่งจะประมวลผลผลลัพธ์ที่ได้รับจากเครื่องปั่นทีละบรรทัดเพื่ออัปเดตแถบความคืบหน้าใน GUI ไม่สำคัญจริงๆที่เครื่องปั่นเป็นกระบวนการภายนอกที่ฉันต้องอ่าน stdout

ดูเหมือนว่าฉันไม่สามารถจับข้อความความคืบหน้าเครื่องปั่นที่พิมพ์ไปยังเชลล์ได้ตามปกติเมื่อกระบวนการเครื่องปั่นยังคงทำงานอยู่และฉันได้ลองหลายวิธีแล้ว ฉันดูเหมือนจะเข้าถึง stdout ของเครื่องปั่นเสมอหลังจากที่เครื่องปั่นเลิกทำงานไม่ใช่ในขณะที่ยังทำงานอยู่

นี่คือตัวอย่างของความพยายามที่ล้มเหลว รับและพิมพ์ 25 บรรทัดแรกของผลลัพธ์ของเครื่องปั่น แต่หลังจากออกจากกระบวนการเครื่องปั่นแล้วเท่านั้น:

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}

แก้ไข:

เพื่อให้ชัดเจนขึ้นเล็กน้อยคำสั่งที่เรียกใช้เครื่องปั่นจะส่งกระแสข้อมูลกลับมาในเชลล์ซึ่งระบุความคืบหน้า (ส่วนที่ 1-16 เสร็จสมบูรณ์เป็นต้น) ดูเหมือนว่าการเรียกให้ "รับ" เอาต์พุตจะถูกบล็อกจนกว่าเครื่องปั่นจะหยุดทำงาน ปัญหาคือวิธีเข้าถึงเอาต์พุตนี้ในขณะที่เครื่องปั่นยังทำงานอยู่เนื่องจากเครื่องปั่นพิมพ์ออกไปยังเชลล์

คำตอบ:


175

ฉันประสบความสำเร็จในการแก้ปัญหาของฉัน นี่คือรายละเอียดพร้อมคำอธิบายเผื่อว่าใครมีปัญหาคล้าย ๆ กันเจอหน้านี้ แต่ถ้าคุณไม่สนใจรายละเอียดนี่คือคำตอบสั้น ๆ :

ใช้ PTY.spawn ในลักษณะต่อไปนี้ (ด้วยคำสั่งของคุณเอง):

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

และนี่คือคำตอบยาว ๆพร้อมรายละเอียดมากเกินไป:

ปัญหาที่แท้จริงดูเหมือนว่าหากกระบวนการไม่ได้ล้าง stdout อย่างชัดเจนสิ่งที่เขียนไปยัง stdout จะถูกบัฟเฟอร์แทนที่จะส่งจริงจนกว่ากระบวนการจะเสร็จสิ้นเพื่อลด IO ให้น้อยที่สุด ( เห็นได้ชัดว่าเป็นรายละเอียดการใช้งานของหลาย ๆ ไลบรารี C สร้างขึ้นเพื่อให้ทรูพุตขยายใหญ่สุดผ่าน IO ที่ไม่บ่อย) หากคุณสามารถปรับเปลี่ยนกระบวนการได้อย่างง่ายดายเพื่อให้ล้าง stdout เป็นประจำนั่นจะเป็นทางออกของคุณ ในกรณีของฉันมันเป็นเครื่องปั่นดังนั้นค่อนข้างน่ากลัวสำหรับ noob ที่สมบูรณ์เช่นตัวฉันเองในการแก้ไขแหล่งที่มา

แต่เมื่อคุณรันกระบวนการเหล่านี้จากเชลล์กระบวนการเหล่านี้จะแสดง stdout ไปยังเชลล์แบบเรียลไทม์และ stdout ดูเหมือนจะไม่ถูกบัฟเฟอร์ มันบัฟเฟอร์เมื่อถูกเรียกจากกระบวนการอื่นที่ฉันเชื่อเท่านั้น แต่ถ้ามีการจัดการกับเชลล์ stdout จะเห็นในแบบเรียลไทม์โดยไม่มีบัฟเฟอร์

พฤติกรรมนี้สามารถสังเกตได้ด้วยกระบวนการทับทิมเช่นเดียวกับกระบวนการย่อยที่ต้องรวบรวมผลลัพธ์แบบเรียลไทม์ เพียงสร้างสคริปต์ random.rb โดยมีบรรทัดต่อไปนี้:

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

จากนั้นสคริปต์ทับทิมเพื่อเรียกมันและส่งคืนผลลัพธ์:

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

คุณจะเห็นว่าคุณไม่ได้รับผลลัพธ์แบบเรียลไทม์อย่างที่คุณคาดหวัง แต่ทั้งหมดในครั้งเดียวหลังจากนั้น STDOUT กำลังถูกบัฟเฟอร์แม้ว่าคุณจะเรียกใช้ random.rb ด้วยตัวเอง แต่ก็ไม่ได้บัฟเฟอร์ สิ่งนี้สามารถแก้ไขได้โดยการเพิ่มSTDOUT.flushคำสั่งภายในบล็อกใน random.rb แต่ถ้าคุณไม่สามารถเปลี่ยนแหล่งที่มาได้คุณต้องหลีกเลี่ยงสิ่งนี้ คุณไม่สามารถล้างออกจากภายนอกกระบวนการได้

หากกระบวนการย่อยสามารถพิมพ์ไปยังเชลล์แบบเรียลไทม์ได้ก็ต้องมีวิธีจับสิ่งนี้ด้วย Ruby แบบเรียลไทม์เช่นกัน และมี. คุณต้องใช้โมดูล PTY ซึ่งรวมอยู่ในแกนทับทิมฉันเชื่อว่า (1.8.6 อย่างไรก็ตาม) สิ่งที่น่าเศร้าคือไม่มีเอกสาร แต่ฉันพบบางตัวอย่างของการใช้งานโชคดี

ครั้งแรกที่จะอธิบายสิ่ง PTY คือมันย่อมาจากขั้วหลอก โดยทั่วไปจะอนุญาตให้สคริปต์ Ruby นำเสนอตัวเองไปยังกระบวนการย่อยราวกับว่าเป็นผู้ใช้จริงที่เพิ่งพิมพ์คำสั่งลงในเชลล์ ดังนั้นพฤติกรรมที่เปลี่ยนแปลงใด ๆ ที่เกิดขึ้นเฉพาะเมื่อผู้ใช้เริ่มต้นกระบวนการผ่านเชลล์ (เช่น STDOUT ที่ไม่ถูกบัฟเฟอร์ในกรณีนี้) จะเกิดขึ้น การปกปิดข้อเท็จจริงที่ว่ากระบวนการอื่นได้เริ่มต้นกระบวนการนี้ทำให้คุณสามารถรวบรวม STDOUT แบบเรียลไทม์ได้เนื่องจากไม่ได้ถูกบัฟเฟอร์

เพื่อให้ทำงานกับสคริปต์ random.rb ในฐานะลูกให้ลองใช้รหัสต่อไปนี้:

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
นี่เป็นเรื่องที่ดี แต่ฉันเชื่อว่าควรสลับพารามิเตอร์บล็อก stdin และ stdout ดู: ruby-doc.org/stdlib-1.9.3/libdoc/pty/rdoc/…
Mike Conigliaro

1
จะปิด pty ได้อย่างไร? ฆ่าพีด?
Boris B.

คำตอบที่ยอดเยี่ยม คุณช่วยฉันปรับปรุงสคริปต์การปรับใช้คราดของฉันสำหรับ heroku มันแสดงบันทึก 'git push' ในแบบเรียลไทม์และยกเลิกงานหากพบ 'fatal:' gist.github.com/sseletskyy/9248357
Serge Seletskyy

1
ตอนแรกฉันพยายามใช้วิธีนี้ แต่ 'pty' ไม่มีใน Windows ปรากฎว่าSTDOUT.sync = trueเป็นสิ่งที่จำเป็นทั้งหมด (คำตอบของ mveerman ด้านล่าง) นี่คือหัวข้ออื่นที่มีรหัสตัวอย่างบางส่วน
Pakman

12

ใช้IO.popen. นี่เป็นตัวอย่างที่ดี

รหัสของคุณจะกลายเป็น:

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

ฉันได้ลองสิ่งนี้แล้ว ปัญหาก็เหมือนกัน ฉันสามารถเข้าถึงผลลัพธ์ได้ในภายหลัง ฉันเชื่อว่า IO.popen เริ่มต้นด้วยการรันอาร์กิวเมนต์แรกเป็นคำสั่งและรอให้มันสิ้นสุด ในกรณีของฉันผลลัพธ์จะได้รับจากเครื่องปั่นในขณะที่เครื่องปั่นกำลังประมวลผล จากนั้นบล็อกจะถูกเรียกใช้หลังจากนั้นซึ่งไม่ได้ช่วยฉัน
ehsanul

นี่คือสิ่งที่ฉันพยายาม ส่งคืนผลลัพธ์หลังจากเครื่องปั่นเสร็จ: IO.popen ("blender -b mball.blend // renders / -F JPEG -x 1 -f 1", "w +") do | blender | blender.each {| line | วางเส้น; output + = line;} end
ehsanul

3
ฉันไม่แน่ใจว่าเกิดอะไรขึ้นในกรณีของคุณ ฉันทดสอบโค้ดด้านบนด้วยyesแอปพลิเคชันบรรทัดคำสั่งที่ไม่สิ้นสุดและใช้งานได้ รหัสมีดังต่อไปนี้: IO.popen('yes') { |p| p.each { |f| puts f } }. ฉันสงสัยว่ามันเป็นอะไรที่เกี่ยวข้องกับเครื่องปั่นไม่ใช่ทับทิม เครื่องปั่นอาจไม่ได้ล้าง STDOUT เสมอไป
Sinan Taifour

โอเคฉันเพิ่งลองใช้กระบวนการทับทิมภายนอกเพื่อทดสอบและคุณพูดถูก ดูเหมือนจะเป็นปัญหาเครื่องปั่น ขอบคุณสำหรับคำตอบอย่างไรก็ตาม
ehsanul

ปรากฎว่ามีวิธีรับผลลัพธ์ผ่านทับทิมแม้ว่าเครื่องปั่นจะไม่ล้างออก รายละเอียดในคำตอบแยกต่างหากในไม่ช้าในกรณีที่คุณสนใจ
ehsanul

6

STDOUT.flush หรือ STDOUT.sync = true


ใช่นี่เป็นคำตอบที่ง่อย คำตอบของคุณดีกว่า
mveerman

ไม่ง่อย! ทำงานให้ฉัน
Clay Bridges

อย่างแม่นยำมากขึ้น:STDOUT.sync = true; system('<whatever-command>')
caram

4

Blender อาจไม่พิมพ์ตัวแบ่งบรรทัดจนกว่าจะสิ้นสุดโปรแกรม แต่เป็นการพิมพ์อักขระส่งคืนแคร่ (\ r) วิธีแก้ปัญหาที่ง่ายที่สุดคือการค้นหาตัวเลือกมายากลที่พิมพ์ตัวแบ่งบรรทัดพร้อมตัวบ่งชี้ความคืบหน้า

ปัญหาคือIO#gets(และวิธีการอื่น ๆ ของ IO) ใช้ตัวแบ่งบรรทัดเป็นตัวคั่น พวกเขาจะอ่านสตรีมจนกว่าจะถึงอักขระ "\ n" (ซึ่งเครื่องปั่นไม่ได้ส่ง)

ลองตั้งค่าตัวคั่นอินพุต$/ = "\r"หรือใช้blender.gets("\r")แทน

BTW สำหรับปัญหาเช่นนี้คุณควรตรวจสอบputs someobj.inspectหรือp someobj(ทั้งสองอย่างทำสิ่งเดียวกัน) เพื่อดูอักขระที่ซ่อนอยู่ภายในสตริง


1
ฉันเพิ่งตรวจสอบผลลัพธ์ที่ได้รับและดูเหมือนว่าเครื่องปั่นจะใช้ตัวแบ่งบรรทัด (\ n) นั่นไม่ใช่ปัญหา ขอบคุณสำหรับเคล็ดลับอย่างไรก็ตามฉันจะจำไว้ในครั้งต่อไปที่ฉันแก้ไขข้อบกพร่องบางอย่างเช่นนี้
ehsanul

0

ฉันไม่รู้ว่าตอนนี้อีซานึลตอบคำถามนั้นหรือยังมีOpen3::pipeline_rw()ให้ แต่มันทำให้สิ่งต่างๆง่ายขึ้นจริงๆ

ฉันไม่เข้าใจงาน ehsanul กับปั่นดังนั้นฉันทำอีกตัวอย่างหนึ่งด้วยและtar จะเพิ่มไฟล์อินพุตไปยังสตรีม stdout จากนั้นนำไฟล์นั้นมาบีบอัดอีกครั้งไปยัง stdout อื่น งานของเราคือใช้ stdout สุดท้ายและเขียนลงในไฟล์สุดท้ายของเรา:xztarxzstdout

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

คำถามเก่า แต่มีปัญหาที่คล้ายกัน

โดยไม่ต้องเปลี่ยนรหัส Ruby ของฉันจริงๆสิ่งหนึ่งที่ช่วยได้คือการห่อท่อของฉันด้วยstdbufเช่น:

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

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

ในตัวอย่างของฉันคำสั่งที่เกิดขึ้นจริงผมต้องการที่จะมีปฏิสัมพันธ์กับราวกับว่ามันเป็นเปลือกเป็นOpenSSL

-oL -eL บอกให้บัฟเฟอร์ STDOUT และ STDERR ไม่เกินบรรทัดใหม่เท่านั้น แทนที่Lด้วย0เพื่อเลิกบัฟเฟอร์อย่างสมบูรณ์

แม้ว่าจะไม่ได้ผลเสมอไป: บางครั้งกระบวนการเป้าหมายบังคับใช้ประเภทบัฟเฟอร์สตรีมของตัวเองเช่นเดียวกับคำตอบอื่น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.