Prevent an args-out-of-range error during login/out
[riece] / lisp / server.rb
1 # server.rb --- A simple IPC server executing Ruby programs.
2 # Copyright (C) 1998-2005 Daiki Ueno
3
4 # Author: Daiki Ueno <ueno@unixuser.org>
5 # Created: 1998-09-28
6 # Keywords: IRC, riece, Ruby
7
8 # This file is part of Riece.
9
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2, or (at your option)
13 # any later version.
14
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.   See the
18 # GNU General Public License for more details.
19
20 # You should have received a copy of the GNU General Public License
21 # along with GNU Emacs; see the file COPYING.  If not, write to the
22 # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
23 # Boston, MA 02110-1301, USA.
24
25 require 'thread'
26 require 'stringio'
27
28 class Server
29   def initialize(outfile, errfile, logfile)
30     @out = $stdout
31     @err = $stderr
32     $stdout = outfile ? File.new(outfile, 'a') : StringIO.new
33     $stderr = errfile ? File.new(errfile, 'a') : StringIO.new
34     @log = File.new(logfile, 'a') if logfile
35
36     @buf = ''
37     @que = Queue.new
38     @thr = Hash.new
39     @cnt = 0
40   end
41
42   def dispatch(line)
43     @log.puts(line) if @log
44     case line.chomp
45     when /\AD /
46       @buf << $'
47     when /\A(\S+)\s*/
48       c = $1
49       r = $'
50       d = "dispatch_#{c.downcase}"
51       if respond_to?(d, true)
52         Thread.start do
53           self.send(d, c, r)
54         end
55       else
56         send_line("ERR 103 Unknown command\r\n")
57       end
58     end
59   end
60
61   def dispatch_cancel(c, r)
62     send_line("ERR 100 Not implemented\r\n")
63   end
64
65   def dispatch_bye(c, r)
66     send_line("ERR 100 Not implemented\r\n")
67   end
68
69   def dispatch_auth(c, r)
70     send_line("ERR 100 Not implemented\r\n")
71   end
72
73   def dispatch_reset(c, r)
74     send_line("ERR 100 Not implemented\r\n")
75   end
76
77   def dispatch_end(c, r)
78     enq_data
79   end
80
81   def dispatch_help(c, r)
82     send_line("ERR 100 Not implemented\r\n")
83   end
84
85   def dispatch_quit(c, r)
86     send_line("ERR 100 Not implemented\r\n")
87   end
88
89   def dispatch_eval(c, r)
90     r = deq_data if r.empty?
91     name = nil
92     Thread.exclusive do
93       while @thr.include?(name = @cnt.to_s)
94         @cnt += 1
95       end
96       @thr[name] = Thread.current
97     end
98     send_line("S name #{name}\r\n")
99     send_line("OK\r\n")
100     Thread.current[:rubyserv_name] = name
101     begin
102       Thread.current[:rubyserv_error] = false
103       Thread.current[:rubyserv_response] = eval(r, exec_env.empty_binding)
104     rescue Exception => e
105       Thread.current[:rubyserv_error] = true
106       Thread.current[:rubyserv_response] =
107         e.to_s.sub(/\A.*?\n#{Regexp.quote(__FILE__)}:\d+: /o, '')
108     end
109     send_line("# exit #{name}\r\n")
110   end
111
112   def dispatch_poll(c, r)
113     thr = @thr[r]
114     if !thr
115       send_line("ERR 105 Parameter error: no such name \"#{r}\"\r\n")
116     elsif thr.alive?
117       send_line("S running #{r}\r\n")
118       send_line("OK\r\n")
119     else
120       if thr[:rubyserv_error]
121         send_line("S exited #{r}\r\n")
122       else
123         send_line("S finished #{r}\r\n")
124       end
125       if d = thr[:rubyserv_response]
126         send_data(d.to_s)
127       end
128       send_line("OK\r\n")
129     end
130   end
131
132   def dispatch_exit(c, r)
133     thr = @thr[r]
134     if !thr
135       send_line("ERR 105 Parameter error: no such name \"#{r}\"\r\n")
136       return
137     end
138     thr.kill if thr.alive?
139     @thr.delete(r)
140     send_line("OK\r\n")
141   end
142
143   def escape(s)
144     s.gsub(/[%\r\n]/) {|m| '%%%02X' % m[0]}
145   end
146
147   def unescape(s)
148     s.gsub(/%([0-9A-Z][0-9A-Z])/) {[$1].pack('H*')}
149   end
150
151   def send_data(d)
152     d = escape(d)
153     begin
154       len = [d.length, 998].min   # 998 = 1000 - "D "
155       send_line("D #{d[0 ... len]}\r\n")
156       d = d[len .. -1]
157     end until d.empty?
158   end
159
160   def enq_data
161     d = unescape(@buf)
162     @buf = ''
163     @que.enq(d)
164   end
165
166   def deq_data
167     @que.deq
168   end
169
170   def send_line(line)
171     @out.puts(line)
172     @log.puts(line) if @log
173   end
174
175   def exec_env
176     env = Object.new
177     def env.empty_binding
178       binding
179     end
180     out, log = @out, @log
181     env.instance_eval {@out, @log = out, log}
182     def env.send_line(line)
183       @out.puts(line)
184       @log.puts(line) if @log
185     end
186     def env.output(s)
187       send_line("# output #{Thread.current[:rubyserv_name]} #{s}\r\n")
188     end
189     env
190   end
191 end
192
193 if $0 == __FILE__
194   require 'optparse'
195
196   opt_outfile, opt_errfile, opt_logfile = nil, nil, nil
197   opts = OptionParser.new do |opts|
198     opts.banner = <<"End"
199 Usage: #{$0} [OPTIONS]
200 End
201     opts.on('-o', '--out OUTFILE', 'Send stdout to OUTFILE.') do |outfile|
202       opt_outfile = outfile
203     end
204     opts.on('-e', '--err ERRFILE', 'Send stderr to ERRFILE.') do |errfile|
205       opt_errfile = errfile
206     end
207     opts.on('-l', '--log LOGFILE', 'Send log to LOGFILE.') do |logfile|
208       opt_logfile = logfile
209     end
210     opts.on_tail('--help', '-h', 'Show this message.') do
211       $stdout.print(opts.to_s)
212       exit(0)
213     end
214   end
215   begin
216     opts.parse!(ARGV)
217   rescue OptionParser::ParseError
218     $stderr.print(opts.to_s)
219     exit(1)
220   end
221
222   server = Server.new(opt_outfile, opt_errfile, opt_logfile)
223   while gets
224     server.dispatch($_)
225   end
226 end