Capture et forwarding des sorties standards (STDOUT et SDTIN)
Principe
le but de cet exemple est de capturer les sorties standards d'une méthode ou d'un Proc Object
et de les restituer dans une autre partie du logiciel
Etape #1 : Le cas simpliste (STDOUT)
On trouve beaucoup d'exemple sur Stackoverflow d'exemple de capture de STDOUT
seulement :
require 'stringio'
def workload
puts "STDOUT output #1"
$stderr.puts "STDERR output #1"
$stdout.puts "STDOUT output #2"
end
class Executor
def self.with_captured
original_stdout = $stdout
$stdout = StringIO.new
ret = yield
return {returned: ret , output: $stdout.string.split("\n") }
ensure
$stdout = original_stdout
end
def self.run
return self.with_captured{yield } if block_given?
end
end
p Executor::run { workload }
Note : l'Executor.run
renvoie un hash avec les clefs optionnelles ( :output
=> le contenu de STDOUT
et :returned
les données renvoyées par la méthode.
La sortie donne :
STDERR output #1
{:returned=>nil, :output=>["STDOUT output #1", "STDOUT output #2"]}
Explication
On utilise un String IO Adapter
pour capturer la sortie standard, on sauvegarde le descripteur en cours et l'IO
sur STDOUT
, on le substitue par l'adaptateur
On utilise une Class pour structurer les Execution via une méthode de classe run
qui wrappe un workload
⚠️ Problème : STDERR n'est pas capturée.
Affichage de la sortie
on ajoute la classe :
class Output
def self.display(result: )
if result[:output] then
result[:output].each do |item|
puts item
end
end
return result[:returned]
end
end
La sortie donne :
STDERR output #1
STDOUT output #1
STDOUT output #2
Et là vous vous dites tout ça pour ça !
Remarque : en plus l'ordre chronologique n'est plus correct.
Etape #2 : Capture de STDOUT et STDERR
On capture les deux STDOUT
et STDERR
et on les envoies sur deux clefs spécifiques du hash de retour
require 'stringio'
def workload
puts "STDOUT output #1"
$stderr.puts "STDERR output #1"
$stdout.puts "STDOUT output #2"
end
class Executor
def self.with_captured
original_stdout = $stdout
original_stderr = $stderr
$stdout = StringIO.new
$stderr = StringIO.new
ret = yield
return {returned: ret , stdout: $stdout.string.split("\n"),
stderr: $stderr.string.split("\n") }
ensure
$stdout = original_stdout
$stderr = original_stderr
end
def self.run
return self.with_captured{yield } if block_given?
end
end
class Output
def self.display(result: )
if result[:stdout] then
result[:stdout].each do |item|
puts item
end
end
if result[:stderr] then
result[:stderr].each do |item|
puts item
end
end
return result[:returned]
end
end
Output::display result: Executor::run { workload }
Sortie :
STDOUT output #1
STDOUT output #2
STDERR output #1
Remarque : un autre ordre, mais forcément pas le bon : choix arbitraireSTDOUT
puisSTDIN
, mais aucun moyen de trier
Une alternative serais de pousser sur une seule entrée output: les sorties de STDOUT
et STDERR
, mais du coup pas moyen de les discriminer et logiquement tout ne serait forwardé QUE sur STDOUT
à la cible.
Etape #3 : Version finale : Split entre STDOUT et STDERR.
Code définitif :
require 'stringio'
def workload
puts "STDOUT output #1"
$stderr.puts "STDERR output #1"
$stdout.puts "STDOUT output #2"
end
class STDProxy < IO
def initialize(source: "STDOUT", target:)
@source = source
@target = target
end
def puts(*args)
tagged_args = args.map { |arg| "#{@source}:#{arg}" }
@target.puts(*tagged_args)
end
def method_missing(method, *args, &block)
@target.send(method, *args, &block)
end
end
class Executor
def self.with_captured
original_stdout = $stdout
original_stderr = $stderr
output = StringIO.new
$stdout = STDProxy.new(source: "STDOUT", target: output)
$stderr = STDProxy.new(source: "STDERR", target: output)
ret = yield
return {returned: ret , output: output.string.split("\n") }
ensure
$stdout = original_stdout
$stderr = original_stderr
end
def self.run
return self.with_captured{yield } if block_given?
end
end
class Output
def self.display(result: )
if result[:output] then
result[:output].each do |item|
target,*rest = item.split(":")
Object.const_get(target).puts rest.join(':')
end
end
return result[:returned]
end
end
result = Executor::run { workload }
Output::display result: result
Principe : On vient ajouter un Proxy de dispatching : STDProxy
qui implémente de façon abstraite et forward toutes les méthodes sauf #puts , qu'il modifie pour tagguer les sorties
la classe Output
n'a plus qu'à itérer les lignes en re-routant via instrospection de constante sur la bonne sortie !
🎯 Usecase : L'objectif est de pouvoir sérialiser (JSON, YAML) le retour d’exécution du workload sur une machine serveur et de la récupérer sur une machine client via une API un RPC, etc ...