* server.rb: Added logging feature.
[riece] / lisp / server.rb
1 # A simple IPC server executing Ruby programs.
2
3 require 'thread'
4 require 'stringio'
5
6 class Server
7   def initialize(outfile, errfile, logfile)
8     @out = $stdout
9     @err = $stderr
10     $stdout = outfile ? File.new(outfile, 'a') : StringIO.new
11     $stderr = errfile ? File.new(errfile, 'a') : StringIO.new
12     @log = File.new(logfile, 'a') if logfile
13
14     @buf = ''
15     @que = Queue.new
16     @thr = Hash.new
17     @cnt = 0
18   end
19
20   def dispatch(line)
21     @log.puts(line) if @log
22     case line.chomp
23     when /\AD /
24       @buf << $'
25     when /\A(\S+)\s*/
26       c = $1
27       r = $'
28       d = "dispatch_#{c.downcase}"
29       if respond_to?(d, true)
30         Thread.start do
31           self.send(d, c, r)
32         end
33       else
34         send_line("ERR 103 Unknown command\r\n")
35       end
36     end
37   end
38
39   def dispatch_cancel(c, r)
40     send_line("ERR 100 Not implemented\r\n")
41   end
42
43   def dispatch_bye(c, r)
44     send_line("ERR 100 Not implemented\r\n")
45   end
46
47   def dispatch_auth(c, r)
48     send_line("ERR 100 Not implemented\r\n")
49   end
50
51   def dispatch_reset(c, r)
52     send_line("ERR 100 Not implemented\r\n")
53   end
54
55   def dispatch_end(c, r)
56     enq_data
57   end
58
59   def dispatch_help(c, r)
60     send_line("ERR 100 Not implemented\r\n")
61   end
62
63   def dispatch_quit(c, r)
64     send_line("ERR 100 Not implemented\r\n")
65   end
66
67   def dispatch_eval(c, r)
68     r = deq_data if r.empty?
69     name = nil
70     Thread.exclusive do
71       while @thr.include?(name = @cnt.to_s)
72         @cnt += 1
73       end
74       @thr[name] = Thread.current
75     end
76     send_line("S name #{name}\r\n")
77     send_line("OK\r\n")
78     Thread.current[:rubyserv_name] = name
79     out, log = @out, @log
80     env = Module.new
81     env.module_eval do
82       @out, @log = out, log
83
84       def send_line(line)
85         @out.puts(line)
86         @log.puts(line) if @log
87       end
88       module_function :send_line
89
90       def output(s)
91         send_line("# output #{Thread.current[:rubyserv_name]} #{s}\r\n")
92       end
93       module_function :output
94     end
95     begin
96       Thread.current[:rubyserv_error] = false
97       Thread.current[:rubyserv_response] = eval(r, env.module_eval {binding()})
98     rescue Exception => e
99       Thread.current[:rubyserv_error] = true
100       Thread.current[:rubyserv_response] = e.to_s.sub(/\A.*?\n/, '')
101     end
102     send_line("# exit #{name}\r\n")
103   end
104
105   def dispatch_poll(c, r)
106     thr = @thr[r]
107     if !thr
108       send_line("ERR 105 Parameter error: no such name \"#{r}\"\r\n")
109     elsif thr.alive?
110       send_line("S running #{r}\r\n")
111       send_line("OK\r\n")
112     else
113       if thr[:rubyserv_error]
114         send_line("S exited #{r}\r\n")
115       else
116         send_line("S finished #{r}\r\n")
117       end
118       if d = thr[:rubyserv_response]
119         send_data(d.to_s)
120       end
121       send_line("OK\r\n")
122     end
123   end
124
125   def dispatch_exit(c, r)
126     thr = @thr[r]
127     if !thr
128       send_line("ERR 105 Parameter error: no such name \"#{r}\"\r\n")
129       return
130     end
131     thr.kill if thr.alive?
132     @thr.delete(r)
133     send_line("OK\r\n")
134   end
135
136   def escape(s)
137     s.gsub(/[%\r\n]/) {|m| '%%%02X' % m[0]}
138   end
139
140   def unescape(s)
141     s.gsub(/%([0-9A-Z][0-9A-Z])/) {[$1].pack('H*')}
142   end
143
144   def send_data(d)
145     d = escape(d)
146     begin
147       len = [d.length, 998].min   # 998 = 1000 - "D "
148       send_line("D #{d[0 ... len]}\r\n")
149       d = d[len .. -1]
150     end until d.empty?
151   end
152
153   def enq_data
154     d = unescape(@buf)
155     @buf = ''
156     @que.enq(d)
157   end
158
159   def deq_data
160     @que.deq
161   end
162
163   def send_line(line)
164     @out.puts(line)
165     @log.puts(line) if @log
166   end
167 end
168
169 if $0 == __FILE__
170   require 'optparse'
171
172   opt_outfile, opt_errfile, opt_logfile = nil, nil, nil
173   opts = OptionParser.new do |opts|
174     opts.banner = <<"End"
175 Usage: #{$0} [OPTIONS]
176 End
177     opts.on('-o', '--out OUTFILE', 'Send stdout to OUTFILE.') do |outfile|
178       opt_outfile = outfile
179     end
180     opts.on('-e', '--err ERRFILE', 'Send stderr to ERRFILE.') do |errfile|
181       opt_errfile = errfile
182     end
183     opts.on('-e', '--log LOGFILE', 'Send stdlog to LOGFILE.') do |logfile|
184       opt_logfile = logfile
185     end
186     opts.on_tail('--help', '-h', 'Show this message.') do
187       $stdout.print(opts.to_s)
188       exit(0)
189     end
190   end
191   begin
192     opts.parse!(ARGV)
193   rescue OptionParser::ParseError
194     $stderr.print(opts.to_s)
195     exit(1)
196   end
197
198   server = Server.new(opt_outfile, opt_errfile, opt_logfile)
199   while gets
200     server.dispatch($_)
201   end
202 end