lib/tork/server.rb in tork-18.2.4 vs lib/tork/server.rb in tork-19.0.0

- old
+ new

@@ -1,32 +1,133 @@ -require 'tork/client' +require 'socket' +require 'thread' +require 'json' +require 'shellwords' module Tork class Server - def initialize - trap(:SIGTERM){ quit } + def self.address program=$0 + ".#{program}.sock" end - def quit - Thread.exit # kill Client::Receiver in loop() + def initialize + # only JSON messages are supposed to be emitted on STDOUT + # so make puts() in the user code write to STDERR instead + @stdout = STDOUT.dup + STDOUT.reopen STDERR + + @clients = [STDIN] + @servers = [] end def loop - @client = Client::Transmitter.new(STDOUT.dup) - STDOUT.reopen(STDERR).sync = true + server = UNIXServer.open(Server.address) + @servers << server + catch :quit do + while @clients.include? STDIN + IO.select(@servers + @clients).first.each do |stream| + @client = stream - Client::Receiver.new(STDIN) do |command| - if command.first != __method__ # prevent loops - @command = command - begin - __send__(*command) - rescue => error - warn "#{$0}: #{error}" - warn error.backtrace.join("\n") + if stream == server + @clients << stream.accept + + elsif (stream.eof? rescue true) + @clients.delete stream + + elsif @command = hear(stream, stream.gets) + recv stream, @command + end end end - end.join + end + ensure + # UNIX domain socket files are not deleted automatically upon closing + File.delete server.path if server + end + + def quit + throw :quit + end + +protected + + JSON_REGEXP = /\A\s*[\[\{]/.freeze + + # On failure to decode the message, warns the sender and returns nil. + def hear sender, message + if message =~ JSON_REGEXP + JSON.load message + + # accept non-JSON "command lines" from clients + elsif @clients.include? sender + Shellwords.split message + + # forward tell() output from children to clients + elsif @servers.include? sender + tell @clients, message, false + nil + end + rescue JSON::ParserError => error + tell sender, error + nil + end + + def recv client, command + __send__(*command) + rescue => error + tell client, error + nil + end + + def send one_or_more_clients, message + tell one_or_more_clients, JSON.dump(message), false + end + + def tell one_or_more_clients, message, prefix=true + if message.kind_of? Exception + message = [message.inspect, message.backtrace] + end + + if prefix + message = Array(message).join("\n").gsub(/^/, "#{$0}: ") + end + + targets = + if one_or_more_clients.kind_of? IO + [one_or_more_clients] + else + Array(one_or_more_clients) + end + + targets.each do |target| + target = @stdout if target == STDIN + target.puts message + target.flush + end + end + + def popen command + child = IO.popen(command, 'r+') + @servers << child + child + end + + def pclose child + return unless @servers.delete child + + # this should be enough to stop programs that use Tork::Server#loop + # because their IO.select() loop terminates on the closing of STDIN + child.close_write + + # but some programs like tork-herald(1) need to be killed explicitly + # because they do not follow this convention of exiting on STDIN close + Process.kill :SIGTERM, child.pid + Process.waitpid child.pid + + # this will block until the child process has exited so we must kill it + # explicitly (as above) to ensure that this program does not hang here + child.close_read end end end