如何让Ruby的popen3捕获耗时bash脚本的完整输出?
解决Ruby调用慢bash脚本无法捕获完整输出的问题
我有一个执行耗时数秒的bash脚本,用Ruby的Open3.popen3调用时,只能捕获脚本开头的echo/printf输出,无法获取完整内容。尝试用线程等待脚本执行完毕,但问题依旧。
原Ruby代码(app.rb)
get '/' do script_path = "/path/to/bash/script" @output, @error = execute_script(script_path) erb :index end def execute_script(script, *args) command = "#{script} #{args.join(' ')}" output = "" error = "" Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| stdout_thread = Thread.new { output << stdout.read } stderr_thread = Thread.new { error << stderr.read } stdout_thread.join stderr_thread.join exit_status = wait_thr.value unless exit_status.success? error << "\nCommand failed with exit code #{exit_status.exitstatus}" end end return output, error end
原bash脚本
#!/bin/bash MAM_PATH="/opt/mam/current/bin/" case "$1" in "") line="%-20s %15s %15s %15s %15s " printf "$line" "Account" "Balance" "Reserved" "Available" printf "$line" "-----------------" "---------" "---------" "---------" array=() for ACCOUNT in $($MAM_PATH/mam-list-accounts -A --quiet --show Name);do read -r Allocated Remaining <<< $($MAM_PATH/mam-list-allocations -a $ACCOUNT -A --quiet --show Allocated,Remaining|awk '{ sum += $1; sum2 += $2 } END { print sum, sum2}') Reserved="0" if [[ -z $Remaining ]];then Allocated=0;Remaining=0;array+=($ACCOUNT) else printf "$line" "$ACCOUNT" "$Allocated" "$Reserved" "$Remaining" fi done printf " for more information about a specific allocation, run: 'allocations -a <allocation name>' " ;; -a) $MAM_PATH/mam-list-allocations -a $2 --show Description,StartTime,EndTime,Allocated,Remaining,PercentUsed,Active printf " " $MAM_PATH/mam-list-users -a $2 --show Name,Active,CommonName,EmailAddress,DefaultAccount printf "PI: $(mam-list-accounts -a $2 --show Users | sed -n '3p'| cut -d '^' -f2 | cut -d ',' -f1) " ;; -u) echo "FundName Active Description StartTime EndTime Allocated Remaining" echo "------------------------------------------------------------------------------------" sudo $MAM_PATH/mam-list-allocations -u $2 --quiet --show FundName,Active,Description,StartTime,EndTime,Allocated,Remaining | sort -r | awk '!seen[$1]++' ;; *) printf " for more information about a specific allocation, run: 'allocations -a <allocation name>' " ;; esac
问题原因
bash在非交互式环境(如被Ruby调用时)会对stdout启用块缓冲,而非终端环境的行缓冲。输出会暂存到缓冲区,直到缓冲区满或脚本执行完毕才会输出,导致Ruby的stdout.read无法实时获取中间内容,甚至只能拿到部分输出。
解决方法
方法1:调用脚本时禁用输出缓冲
在Ruby中构造命令时,用stdbuf强制脚本的stdout无缓冲:
command = "stdbuf -o0 #{script} #{args.join(' ')}"
stdbuf -o0会让脚本的每一行输出立即发送到stdout,无需等待缓冲区填满。
方法2:修改Ruby代码,逐行读取输出
将一次性读取stdout.read改为逐行读取,即使有缓冲也能实时捕获内容:
stdout_thread = Thread.new do while line = stdout.gets output << line end end stderr_thread = Thread.new do while line = stderr.gets error << line end end
方法3:修改bash脚本,强制行缓冲
在bash脚本开头添加以下内容,强制stdout使用行缓冲模式:
exec 1>&0
或者在每个printf后手动刷新输出(适合局部调整):
printf "$line" "Account" "Balance" "Reserved" "Available" && flush
修正后的完整Ruby代码
get '/' do script_path = "/path/to/bash/script" @output, @error = execute_script(script_path) erb :index end def execute_script(script, *args) # 启用stdbuf禁用输出缓冲 command = "stdbuf -o0 #{script} #{args.join(' ')}" output = "" error = "" Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| # 逐行读取输出 stdout_thread = Thread.new do while line = stdout.gets output << line end end stderr_thread = Thread.new do while line = stderr.gets error << line end end stdout_thread.join stderr_thread.join exit_status = wait_thr.value unless exit_status.success? error << "\nCommand failed with exit code #{exit_status.exitstatus}" end end return output, error end
额外排查项
- 检查bash脚本是否有输出到stderr的内容,这些会被捕获到
@error变量中,确保erb模板同时渲染@output和@error。 - 如果脚本包含
sudo命令,需确保Ruby进程拥有免密sudo权限,否则脚本会卡在密码输入环节,导致无输出或超时。可通过修改sudoers文件配置免密规则,或移除不必要的sudo调用。
内容的提问来源于stack exchange,提问作者Khanrad
相关产品推荐
相关产品推荐

