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