Fixed typo.
[riece] / lisp / riece-ruby.el
1 ;;; riece-ruby.el --- interact with Ruby interpreter
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., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
24
25 ;;; Commentary:
26
27 ;; riece-ruby.el is a library to interact with Ruby interpreter.
28 ;; It supports concurrent execution of Ruby programs in a single
29 ;; session.  For example:
30 ;; 
31 ;; (riece-ruby-execute "sleep 30"); returns immediately
32 ;; => "rubyserv0"
33 ;;
34 ;; (riece-ruby-execute "1 + 1")
35 ;; => "rubyserv1"
36 ;;
37 ;; (riece-ruby-execute "\"")
38 ;; => "rubyserv2"
39 ;;
40 ;; (riece-ruby-inspect "rubyserv0")
41 ;; => ((OK nil) nil (("running")))
42 ;;
43 ;; (riece-ruby-inspect "rubyserv1")
44 ;; => ((OK nil) "2" (("finished")))
45 ;;
46 ;; (riece-ruby-inspect "rubyserv2")
47 ;; => ((OK nil) "(eval):1: unterminated string meets end of file" (("exited")))
48
49 ;;; Code:
50
51 (require 'riece-debug)
52
53 (defgroup riece-ruby nil
54   "Interact with Ruby interpreter."
55   :group 'riece)
56
57 (defcustom riece-ruby-command "ruby"
58   "Command name for Ruby interpreter."
59   :type 'string
60   :group 'riece-ruby)
61
62 (defcustom riece-ruby-out-file (expand-file-name "riece-ruby.out"
63                                                  riece-directory)
64   "A file which records stdout of Ruby programs."
65   :type 'string
66   :group 'riece-ruby)
67
68 (defcustom riece-ruby-err-file (expand-file-name "riece-ruby.err"
69                                                  riece-directory)
70   "A file which records stderr of Ruby programs."
71   :type 'string
72   :group 'riece-ruby)
73
74 (defcustom riece-ruby-log-file (expand-file-name "riece-ruby.log"
75                                                  riece-directory)
76   "A file used to logging."
77   :type 'string
78   :group 'riece-ruby)
79
80 (defvar riece-ruby-server-program "server.rb"
81   "The server program file.  If the filename is not absolute, it is
82 assumed that the file is in the same directory of this file.")
83
84 (defvar riece-ruby-server-program-arguments (list "-o" riece-ruby-out-file
85                                                   "-e" riece-ruby-err-file
86                                                   "-l" riece-ruby-log-file)
87   "Command line arguments passed to `riece-ruby-server-program'.")
88
89 (defvar riece-ruby-process nil
90   "Process object of Ruby interpreter.")
91
92 (defvar riece-ruby-lock nil
93   "Lock for waiting server response.
94 Local to the process buffer.")
95 (defvar riece-ruby-response nil
96   "The server response.
97 Local to the process buffer.")
98 (defvar riece-ruby-data nil
99   "Data from server.
100 Local to the process buffer.")
101 (defvar riece-ruby-escaped-data nil
102   "Escaped data from server.  This variable is cleared every time
103 server response arrives.
104 Local to the process buffer.")
105 (defvar riece-ruby-status-alist nil
106   "Status from server.
107 Local to the process buffer.")
108
109 (defvar riece-ruby-output-handler-alist nil
110   "An alist mapping from program name to output handler.
111 Output handlers are called every time \"# output\" line arrives.
112 Use `riece-ruby-set-output-handler' to set this variable.")
113 (defvar riece-ruby-exit-handler-alist nil
114   "An alist mapping from program name to exit handler.
115 Exit handlers are called once when \"# exit\" line arrives.
116 Use `riece-ruby-set-exit-handler' to set this variable.")
117 (defvar riece-ruby-property-alist nil
118   "An alist mapping from program name to the property list.
119 Use `riece-ruby-set-property' to set this variable.")
120
121 (defun riece-ruby-escape-data (data)
122   (let ((index 0))
123     (while (string-match "[%\r\n]+" data index)
124       (setq data (replace-match
125                   (mapconcat (lambda (c) (format "%%%02X" c))
126                              (match-string 0 data) "")
127                   nil nil data)
128             index (+ (match-end 0)
129                      (* (- (match-end 0) (match-beginning 0)) 2))))
130     data))
131
132 (defun riece-ruby-unescape-data (data)
133   (let ((index 0))
134     (while (string-match "%\\([0-9A-F][0-9A-F]\\)" data index)
135       (setq data (replace-match
136                   (read (concat "\"\\x" (match-string 1 data) "\""))
137                   nil nil data)
138             index (- (match-end 0) 2)))
139     data))
140
141 (defun riece-ruby-reset-process-buffer ()
142   (save-excursion
143     (set-buffer (process-buffer riece-ruby-process))
144     (buffer-disable-undo)
145     (make-local-variable 'riece-ruby-response)
146     (setq riece-ruby-response nil)
147     (make-local-variable 'riece-ruby-data)
148     (setq riece-ruby-data nil)
149     (make-local-variable 'riece-ruby-escaped-data)
150     (setq riece-ruby-escaped-data nil)
151     (make-local-variable 'riece-ruby-status-alist)
152     (setq riece-ruby-status-alist nil)))
153
154 (defun riece-ruby-send-eval (program)
155   (let* ((string (riece-ruby-escape-data program))
156          (length (- (length string) 998))
157          (index 0)
158          data)
159     (while (< index length)
160       (setq data (cons (substring string index (setq index (+ index 998)))
161                        data)))
162     (setq data (cons (substring string index) data)
163           data (nreverse data))
164     (process-send-string riece-ruby-process "EVAL\r\n")
165     (while data
166       (process-send-string riece-ruby-process
167                            (concat "D " (car data) "\r\n"))
168       (setq data (cdr data)))
169     (process-send-string riece-ruby-process "END\r\n")))
170
171 (defun riece-ruby-send-poll (name)
172   (process-send-string riece-ruby-process
173                        (concat "POLL " name "\r\n")))
174
175 (defun riece-ruby-send-exit (name)
176   (process-send-string riece-ruby-process
177                        (concat "EXIT " name "\r\n")))
178
179 (defun riece-ruby-filter (process input)
180   (save-excursion
181     (set-buffer (process-buffer process))
182     (goto-char (point-max))
183     (insert input)
184     (goto-char (point-min))
185     (beginning-of-line)
186     (while (looking-at ".*\r\n")
187       (if (looking-at "OK\\( \\(.*\\)\\)?\r")
188           (progn
189             (if riece-ruby-escaped-data
190                 (setq riece-ruby-data (mapconcat #'riece-ruby-unescape-data
191                                                  riece-ruby-escaped-data "")))
192             (setq riece-ruby-escaped-data nil
193                   riece-ruby-response (list 'OK (match-string 2))
194                   riece-ruby-lock nil))
195         (if (looking-at "ERR \\([0-9]+\\)\\( \\(.*\\)\\)?\r")
196             (progn
197               (setq riece-ruby-escaped-data nil
198                     riece-ruby-response
199                     (list 'ERR (string-to-number (match-string 2))
200                           (match-string 3))
201                     riece-ruby-lock nil))
202           (if (looking-at "D \\(.*\\)\r")
203               (setq riece-ruby-escaped-data (cons (match-string 1)
204                                                   riece-ruby-escaped-data))
205             (if (looking-at "S \\([^ ]*\\) \\(.*\\)\r")
206                 (progn
207                   (setq riece-ruby-status-alist (cons (cons (match-string 1)
208                                                             (match-string 2))
209                                                       riece-ruby-status-alist))
210                   (if (member (car (car riece-ruby-status-alist))
211                               '("finished" "exited"))
212                       (riece-ruby-run-exit-handler
213                        (cdr (car riece-ruby-status-alist)))))
214               (if (looking-at "# output \\([^ ]*\\) \\(.*\\)\r")
215                   (let ((entry (assoc (match-string 1)
216                                       riece-ruby-output-handler-alist)))
217                     (if entry
218                         (riece-debug-with-backtrace
219                           (funcall (cdr entry) (car entry) (match-string 2)))))
220                 (if (looking-at "# exit \\(.*\\)\r")
221                     (riece-ruby-run-exit-handler (match-string 1))))))))
222       (forward-line))
223     (delete-region (point-min) (point))))
224
225 (defun riece-ruby-run-exit-handler (name)
226   (let ((entry (assoc name riece-ruby-exit-handler-alist)))
227     (if entry
228         (progn
229           (setq riece-ruby-exit-handler-alist
230                 (delq entry riece-ruby-exit-handler-alist))
231           (riece-debug-with-backtrace
232             (funcall (cdr entry) (car entry)))
233           (riece-ruby-clear name)))))
234
235 (defun riece-ruby-sentinel (process status)
236   (kill-buffer (process-buffer process)))
237
238 (defun riece-ruby-execute (program)
239   "Schedule an execution of a Ruby PROGRAM.
240 Return a string name assigned by the server."
241   (unless (and riece-ruby-process
242                (eq (process-status riece-ruby-process) 'run))
243     (let (selective-display
244           (coding-system-for-write 'binary)
245           (coding-system-for-read 'binary))
246       (setq riece-ruby-process
247             (apply #'start-process "riece-ruby" (generate-new-buffer " *Ruby*")
248                    riece-ruby-command
249                    (if (file-name-absolute-p riece-ruby-server-program)
250                        riece-ruby-server-program
251                      (expand-file-name
252                       riece-ruby-server-program
253                       (file-name-directory
254                        (locate-library
255                         (symbol-file 'riece-ruby-execute)))))
256                    riece-ruby-server-program-arguments))
257       (process-kill-without-query riece-ruby-process)
258       (set-process-filter riece-ruby-process #'riece-ruby-filter)
259       (set-process-sentinel riece-ruby-process #'riece-ruby-sentinel)))
260   (save-excursion
261     (set-buffer (process-buffer riece-ruby-process))
262     (riece-ruby-reset-process-buffer)
263     (make-local-variable 'riece-ruby-lock)
264     (setq riece-ruby-lock t)
265     (riece-ruby-send-eval program)
266     (while riece-ruby-lock
267       (accept-process-output riece-ruby-process))
268     (if (eq (car riece-ruby-response) 'ERR)
269         (error "Couldn't execute: %S" (cdr riece-ruby-response)))
270     (cdr (assoc "name" riece-ruby-status-alist))))
271
272 (defun riece-ruby-inspect (name)
273   "Inspect a result of program execution distinguished by NAME.
274 Return a three element list.
275 The car is protocol response line which looks like:
276   \(ERR 103 \"Not implemented\").
277 The cadr is data from the server, that is, the result of the program.
278 The caddr is status from the server."
279   (save-excursion
280     (set-buffer (process-buffer riece-ruby-process))
281     (riece-ruby-reset-process-buffer)
282     (make-local-variable 'riece-ruby-lock)
283     (setq riece-ruby-lock t)
284     (riece-ruby-send-poll name)
285     (while riece-ruby-lock
286       (accept-process-output riece-ruby-process))
287     (list riece-ruby-response
288           riece-ruby-data
289           riece-ruby-status-alist)))
290
291 (defun riece-ruby-clear (name)
292   "Clear a result of program execution distinguished by NAME.
293 Note that riece-ruby-clear is automatically called iff an exit-handler
294 is specified.  Otherwise, it should be called explicitly."
295   (save-excursion
296     (set-buffer (process-buffer riece-ruby-process))
297     (riece-ruby-reset-process-buffer)
298     (make-local-variable 'riece-ruby-lock)
299     (setq riece-ruby-lock t)
300     (riece-ruby-send-exit name)
301     (while riece-ruby-lock
302       (accept-process-output riece-ruby-process)))
303   (let ((entry (assoc name riece-ruby-property-alist)))
304     (if entry
305         (delq entry riece-ruby-property-alist))))
306
307 (defun riece-ruby-set-exit-handler (name handler)
308   "Set an exit-handler HANDLER for the program distinguished by NAME.
309 An exit-handler is called when the program is finished or exited abnormally.
310 An exit-handler is called with an argument same as NAME.
311 Note that riece-ruby-clear is automatically called iff an exit-handler
312 is specified.  Otherwise, it should be called explicitly."
313   (let ((entry (assoc name riece-ruby-exit-handler-alist)))
314     (if handler
315         (progn
316           (if entry
317               (setcdr entry handler)
318             (setq riece-ruby-exit-handler-alist
319                   (cons (cons name handler)
320                         riece-ruby-exit-handler-alist)))
321           ;;check if the program already exited
322           (riece-ruby-inspect name))
323       (if entry
324           (setq riece-ruby-exit-handler-alist
325                 (delq entry riece-ruby-exit-handler-alist))))))
326
327 (defun riece-ruby-set-output-handler (name handler)
328   "Set an output-handler HANDLER for the program distinguished by NAME.
329 An output-handler is called when the program sends any output by using
330 `output' method in the Ruby program.
331 An output-handler is called with two argument.  The first argument is
332 the same as NAME.  The second argument is output string."
333   (let ((entry (assoc name riece-ruby-output-handler-alist)))
334     (if handler
335         (progn
336           (if entry
337               (setcdr entry handler)
338             (setq riece-ruby-output-handler-alist
339                   (cons (cons name handler)
340                         riece-ruby-output-handler-alist))))
341       (if entry
342           (setq riece-ruby-output-handler-alist
343                 (delq entry riece-ruby-output-handler-alist))))))
344
345 (defun riece-ruby-set-property (name property value)
346   "Set given PROPERTY/VALUE pair to the program distinguished by NAME."
347   (let ((entry (assoc name riece-ruby-property-alist))
348         property-entry)
349     (unless entry
350       (setq entry (list name)
351             riece-ruby-property-alist (cons entry riece-ruby-property-alist)))
352     (if (setq property-entry (assoc property (cdr entry)))
353         (setcdr property-entry value)
354       (setcdr entry (cons (cons property value) (cdr entry))))))
355
356 (defun riece-ruby-property (name property)
357   "Return the value of PROPERTY set to the program distinguished by NAME."
358   (cdr (assoc property (cdr (assoc name riece-ruby-property-alist)))))
359
360 (defun riece-ruby-substitute-variables (program alist)
361   "Substitute symbols in PROGRAM by looking up ALIST.
362 Return a string concatenating elements in PROGRAM."
363   (setq program (copy-sequence program))
364   (while alist
365     (let ((pointer program))
366       (while pointer
367         (setq pointer (memq (car (car alist)) program))
368         (if pointer
369             (setcar pointer (cdr (car alist))))))
370     (setq alist (cdr alist)))
371   (apply #'concat program))
372
373 (provide 'riece-ruby)
374
375 ;;; riece-ruby.el ends here