cvs import
[celtk.git] / fileevent.lisp
blobf27a06ab2921c0fc42af1f8e82d1af8fa96d9c93
1 ;; -*- mode: Lisp; Syntax: Common-Lisp; Package: celtk; -*-
2 ;;;
3 ;;; Copyright (c) 2006 by Frank Goenninger, Germany.
4 ;;;
5 ;;; Permission is hereby granted, free of charge, to any person obtaining a
6 ;;; copy of this software and associated documentation files (the "Software"),
7 ;;; to deal in the Software without restriction, including without limitation
8 ;;; the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 ;;; and/or sell copies of the Software, and to permit persons to whom the
10 ;;; Software is furnished to do so, subject to the following conditions:
11 ;;;
12 ;;; The above copyright notice and this permission notice shall be included in
13 ;;; all copies or substantial portions of the Software.
14 ;;;
15 ;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 ;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 ;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 ;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 ;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 ;;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 ;;; DEALINGS IN THE SOFTWARE.
22 ;;;
23 ;;; ---------------------------------------------------------------------------
24 ;;; $Header: /project/cells/cvsroot/Celtk/fileevent.lisp,v 1.9 2006/11/04 20:53:08 ktilton Exp $
25 ;;; ---------------------------------------------------------------------------
27 ;;; ===========================================================================
28 ;;; PACKAGE / EXPORTS
29 ;;; ===========================================================================
31 (in-package :celtk)
33 (eval-when (:load-toplevel :compile-toplevel)
34 (export '(tk-fileevent
35 iostream
36 read-fn
37 write-fn
38 eof-fn
39 mk-fileevent
40 stream-2-in-fd
41 stream-2-out-fd)))
43 ;;; ===========================================================================
44 ;;; TK-FILEEVENT MODEL
45 ;;; ===========================================================================
47 (defmodel tk-fileevent (widget)
49 ((.md-name
50 :accessor id :initarg :id
51 :initform (c-in nil)
52 :documentation "ID of the fileevent instance.")
54 (input-fd
55 :accessor input-fd :initarg :input-fd
56 :initform (c? (if (^iostream)
57 (stream-2-in-fd (^iostream))))
58 :documentation "The input/read file descriptor - internal use only.")
60 (output-fd
61 :accessor output-fd
62 :initarg :output-fd
63 :initform (c? (if (^iostream)
64 (stream-2-out-fd (^iostream))))
65 :documentation "The output/write file descriptor - internal use only.")
67 (in-tcl-channel
68 :accessor in-tcl-channel :initarg :in-tcl-channel
69 :initform (c? (fd-to-tcl-channel (^tki) (^input-fd)))
70 :documentation "The TCL channel generated from the input file descriptor. - Internal use only.")
72 (out-tcl-channel
73 :accessor out-tcl-channel :initarg :in-tcl-channel
74 :initform (c? (fd-to-tcl-channel (^tki) (^output-fd)))
75 :documentation "The TCL channel generated from the output file descriptor. - Internal use only.")
77 (in-tcl-ch-name
78 :accessor in-tcl-ch-name :initarg :in-tcl-ch-name
79 :initform (c? (if (^in-tcl-channel)
80 (Tcl_GetChannelName (^in-tcl-channel))
81 nil))
82 :documentation "The input TCL channel's name as passed to the fileevent command. - Internal use only.")
84 (out-tcl-ch-name
85 :accessor out-tcl-ch-name :initarg :in-tcl-ch-name
86 :initform (c? (if (^out-tcl-channel)
87 (Tcl_GetChannelName (^out-tcl-channel))
88 nil))
89 :documentation "The output TCL channel's name as passed to the fileevent command. - Internal use only.")
91 (iostream
92 :accessor iostream :initarg :iostream
93 :initform (c-in nil)
94 :documentation "The Lisp stream to be monitored - API: initarg,setf.")
96 (readable-cb
97 :accessor readable-cb :initarg :readable-cb
98 :initform (c-in nil)
99 :documentation "The readable callback. A dispatcher function used to call the function supplied via the read-fn slot. - Internal use only.")
101 (writeable-cb
102 :accessor writeable-cb :initarg :writeable-cb
103 :initform (c-in nil)
104 :documentation "The writeable callback. A dispatcher function used to call the function supplied via the read-fn slot. - Internal use only.")
106 (eof-cb
107 :accessor eof-cb :initarg :eof-cb
108 :initform (c-in nil)
109 :documentation "The eof callback. A dispatcher function used to call the function supplied via the eof-fn slot. - Internal use only.")
111 (error-cb
112 :accessor error-cb :initarg :error-cb
113 :initform (c-in nil)
114 :documentation "The error callback. A dispatcher function used to call the function supplied via the error-fn slot. - Internal use only.")
116 (tki
117 :accessor tki :initarg :tki
118 :initform (c-in nil)
119 :documentation "The Tcl/Tk Interpreter used. - API: initarg.")
121 (opcode
122 :accessor opcode :initarg :opcode
123 :initform (file-event-opcode-cell-rule)
124 :documentation "The opcode slot is used to control the operaion of the fileevent instance. - Internal use only.")
126 (read-fn
127 :accessor read-fn :initarg :read-fn
128 :initform (c-in nil)
129 :documentation "User supplied function, gets called when iostream is ready for reading. Gets iostream as parameter. - API: initarg, setf")
131 (write-fn
132 :accessor write-fn :initarg :write-fn
133 :initform (c-in nil)
134 :documentation "User supplied function, gets called when iostream is ready for writing. Gets iostream as parameter. - API: initarg, setf")
136 (eof-fn
137 :accessor eof-fn :initarg :eof-fn
138 :initform (c-in nil)
139 :documentation "User supplied function, gets called when iostream is EOF. Gets iostream as parameter. - API: initarg, setf (Via default-initarg set to fn default-eof-fn which simply closes the stream).")
141 (error-fn
142 :accessor error-fn :initarg :error-fn
143 :initform (c-in nil)
144 :documentation "User supplied function, gets called when iostream has encountntered an error. Gets iostream and error sting as parameters. - API: initarg, setf (Via default-initarg set to fn default-error-fn which simply closes the stream and signals an error of class tcl-error)."))
146 (:default-initargs
147 :id (gensym "tk-fileevent-")
148 :eof-fn 'default-eof-fn))
151 ;;; ===========================================================================
152 ;;; CELL RULE: FILE-EVENT/OPCODE
153 ;;; ===========================================================================
155 ;;; Depending on opcode call the appropriate function to handle the various
156 ;;; cases/combinations of input-fd, output-fd, and the previously executed
157 ;;; update operation.
159 (defun file-event-opcode-cell-rule ()
160 "Set the opcode depending on values of input-fd, output-fd, iostream, readable-cb, writeable-cb"
161 (c? (cond
162 ((not (or (^input-fd) (^output-fd) .cache))
163 :nop)
165 ((and (^input-fd) (^iostream) (^readable-cb))
166 :update-input-tk-fileevent)
168 ((and (^output-fd) (^iostream) (^writeable-cb))
169 :update-output-tk-fileevent)
171 ((not (or (^iostream) (^input-fd)))
172 :reset-input-tk-fileevent)
174 ((not (or (^iostream) (^output-fd)))
175 :reset-output-tk-fileevent)
177 (t :nop))))
179 ;;; ===========================================================================
180 ;;; INIT-TK-FILEEVENT - CALLED UPON INITIALIZATION
181 ;;; ===========================================================================
183 (defun init-tk-fileevent (tki)
184 (assert tki)
185 ;; Nop - all init done in observers now.
188 ;;; ===========================================================================
189 ;;; FILEEVENT HELPER METHODS AND FUCTIONS
190 ;;; ===========================================================================
192 (defmethod set-tk-readable ((self tk-fileevent) ch-name path type)
194 ;; frgo, 2006-05-26:
195 ;; The code here was aimed at EOF checking after reading...
196 ;; So the API needs rework...
197 ;; STATUS: IN WORK
199 ;; (tk-format-now " proc readable {channel path} {
200 ;; # check for async errors (sockets only, I think)
201 ;; if {[string length [set err [fconfigure $channel -error]]]} {
202 ;; error-cb $path $err
203 ;; close $channel
204 ;; return
205 ;; }
206 ;; # Read a line from the channel
207 ;; if {[catch {set line [gets $channel]} err]} {
208 ;; error-cb $path $err
209 ;; close $channel
210 ;; return
211 ;; }
212 ;; if {[string length $line]} {
213 ;; received-cb $path $line
214 ;; }
215 ;; # check for eof
216 ;; if {[eof $channel]} {
217 ;; eof-cb $path
218 ;; close $channel
219 ;; }
220 ;; }")
222 ;; frgo: Old code snippet:
223 ;; (tk-format-now "proc readable {channel path} { if [ eof $channel ] then { eof-cb $path } else { readable-cb $path } }")
224 ;; (tk-format-now "fileevent ~A readable [list readable ~A ~A]"
225 ;; ch-name
226 ;; ch-name
227 ;; path)
229 (trc "tk-set-readable sees ch-name path type" ch-name path type)
230 (tk-format-now
231 "proc readable {channel path type} {
233 if {! [string compare $type \"socket\"]} {
234 if {[string length [set err [fconfigure $channel -error]]]} {
235 error-cb $path $err
236 close $channel
237 return
241 readable-cb $path
243 catch { if {[eof $channel]} {
244 eof-cb $path
245 close $channel
250 (tk-format-now "fileevent ~A readable [list readable ~A ~A ~a]"
251 ch-name
252 ch-name
253 path
254 type)
257 (defmethod set-tk-writeable ((self tk-fileevent) ch-name path type)
258 (tk-format-now "proc writeable {channel path type} { if [ eof $channel ] then { eof-cb $path } else { writeable-cb $path } }")
259 (tk-format-now "fileevent ~A writeable [list writeable ~A ~A ~a]"
260 ch-name
261 ch-name
262 path
263 type))
265 ;;; ===========================================================================
266 ;;; FILEEVENT CONDITIONS
267 ;;; ===========================================================================
269 (define-condition tcl-fileevent-error (error)
272 ;;; ===========================================================================
273 ;;; OBSERVERS - USED TO SEND UPDATES TO TK LAND
274 ;;; ===========================================================================
276 (defobserver opcode ((self tk-fileevent))
277 (let ((*tki* (tki self)))
278 (ecase new-value
280 ((:init-tk-fileevent)
281 (init-tk-fileevent (tki self)))
283 ((:update-input-tk-fileevent)
284 (let* ((channel (in-tcl-channel self))
285 (path (path self))
286 (ch-name (Tcl_GetChannelName channel))
287 (ch-type (Tcl_GetChannelType channel)))
288 (set-tk-readable self
289 ch-name
290 path
291 (foreign-slot-value ch-type
292 'Tcl_ChannelType
293 'typeName ))))
295 ((:update-output-tk-fileevent)
296 (let* ((channel (out-tcl-channel self))
297 (path (path self))
298 (ch-name (Tcl_GetChannelName channel))
299 (ch-type (Tcl_GetChannelType channel)))
300 (set-tk-writeable self
301 ch-name
302 path
303 (foreign-slot-value ch-type
304 'Tcl_ChannelType
305 'typeName))))
307 ((:reset-input-tk-fileevent)
308 ;; Do nothing
309 nil)
311 ((:reset-output-tk-fileevent)
312 ;; Do nothing
313 nil)
315 ((:nop)
316 ;; Do nothing
317 nil))))
319 (defobserver in-tcl-channel ((self tk-fileevent))
320 (let ((*tki* (tki self)))
321 (if (and new-value
322 (not old-value))
323 (Tcl_RegisterChannel *tki* new-value))
324 (if (and old-value (not new-value))
325 (progn
326 (tk-format-now "fileevent ~A readable {}"
327 (Tcl_GetChannelName old-value))
328 (Tcl_UnregisterChannel *tki* old-value)))))
330 (defobserver out-tcl-channel ((self tk-fileevent))
331 (let ((*tki* (tki self)))
332 (if (and new-value (not old-value))
333 (Tcl_RegisterChannel *tki* new-value))
334 (if (and old-value (not new-value))
335 (progn
336 (tk-format-now "fileevent ~A writeable {}"
337 (Tcl_GetChannelName old-value))
338 (Tcl_UnregisterChannel *tki* old-value)))))
340 (defobserver readable-cb ((self tk-fileevent))
341 (if new-value
342 (tcl-create-command *tki*
343 "readable-cb"
344 new-value
345 (null-pointer)
346 (null-pointer))))
348 (defobserver writeable-cb ((self tk-fileevent))
349 (if new-value
350 (tcl-create-command *tki*
351 "writeable-cb"
352 new-value
353 (null-pointer)
354 (null-pointer))))
356 (defobserver eof-cb ((self tk-fileevent))
357 (if new-value
358 (tcl-create-command *tki*
359 "eof-cb"
360 new-value
361 (null-pointer)
362 (null-pointer))))
364 (defobserver error-cb ((self tk-fileevent))
365 (if new-value
366 (tcl-create-command *tki*
367 "error-cb"
368 new-value
369 (null-pointer)
370 (null-pointer))))
372 ;;; ===========================================================================
373 ;;; HELPER FUNCTIONS - FILE DESCRIPTOR TO STREAM AND CHANNEL
374 ;;; ===========================================================================
376 (defun fd-to-tcl-channel (interp fd)
377 (assert interp)
378 (if fd
379 (let ((channel (Tcl_MakeFileChannel fd 6))) ;; 6 = READ/WRITE
380 (if channel
381 channel
382 (error "*** Tcl error: ~a" (tcl-get-string-result interp))))))
385 (defun stream-2-out-fd (stream) ;; FRGO: PORTING...
387 #+allegro
388 (excl:stream-output-fn stream)
390 #-allegro
391 (error "STREAM-2-OUT-FD: Not implemented for ~A Version ~A. Sorry."
392 (lisp-implementation-type)
393 (lisp-implementation-version))
396 (defun stream-2-in-fd (stream) ;; FRGO: PORTING...
398 #+allegro
399 (excl:stream-input-fn stream)
401 #-allegro
402 (error "STREAM-2-IN-FD: Not implemented for ~A Version ~A. Sorry."
403 (lisp-implementation-type)
404 (lisp-implementation-version))
407 ;;; ===========================================================================
408 ;;; CALLBACKS
409 ;;; ===========================================================================
411 (defcallback readable-cb :int
412 ((clientData :pointer)
413 (interp :pointer)
414 (argc :int)
415 (argv :pointer))
416 (declare (ignore clientData argc interp))
417 (let* ((path (foreign-string-to-lisp (mem-aref argv :pointer 1)))
418 (self (gethash path (dictionary *tkw*))))
419 (bwhen (fn (^read-fn))
420 (funcall fn self :read)))
421 (values (foreign-enum-value 'tcl-retcode-values :tcl-ok)))
423 (defcallback writeable-cb :int
424 ((clientData :pointer)
425 (interp :pointer)
426 (argc :int)
427 (argv :pointer))
428 (declare (ignore clientData argc interp))
429 (let* ((path (foreign-string-to-lisp (mem-aref argv :pointer 1)))
430 (self (gethash path (dictionary *tkw*))))
431 (bwhen (fn (^write-fn))
432 (funcall fn self :write)))
433 (values (foreign-enum-value 'tcl-retcode-values :tcl-ok)))
435 (defcallback eof-cb :int
436 ((clientData :pointer)
437 (interp :pointer)
438 (argc :int)
439 (argv :pointer))
440 (declare (ignore clientData interp argc))
441 (trc "EOF-CB !!!")
442 (let* ((path (foreign-string-to-lisp (mem-aref argv :pointer 1)))
443 (self (gethash path (dictionary *tkw*))))
444 (bwhen (fn (^eof-fn))
445 (funcall fn self)))
446 (values (foreign-enum-value 'tcl-retcode-values :tcl-ok)))
448 (defcallback error-cb :int
449 ((clientData :pointer)
450 (interp :pointer)
451 (argc :int)
452 (argv :pointer))
453 (declare (ignore clientData interp argc))
454 (trc "ERROR-CB !!!")
455 (let* ((path (foreign-string-to-lisp (mem-aref argv :pointer 1)))
456 (err$ (foreign-string-to-lisp (mem-aref argv :pointer 2)))
457 (self (gethash path (dictionary *tkw*))))
458 (bwhen (fn (^error-fn))
459 (funcall fn self err$)))
460 (values (foreign-enum-value 'tcl-retcode-values :tcl-error)))
462 ;;; ===========================================================================
463 ;;; MK-FILEEVENT: CONVENIENCE MACRO
464 ;;; ===========================================================================
466 (defmacro mk-fileevent (&rest inits)
467 `(make-instance 'tk-fileevent
468 :tki *tki*
469 :readable-cb (get-callback 'readable-cb)
470 :writeable-cb (get-callback 'writeable-cb)
471 :eof-cb (get-callback 'eof-cb)
472 :error-cb (get-callback 'error-cb)
473 :fm-parent *parent*
474 ,@inits))
476 ;;; ===========================================================================
477 ;;; A DEFAULT EOF FUNCTION, USER MAY SUPPLY ANOTHER FUNCTION WHEN MAKING THE
478 ;;; INSTANCE OF TK-FILEEVENT
479 ;;; ===========================================================================
481 (defmethod default-eof-fn ((self tk-fileevent))
482 ;; Default action: close stream
483 (bwhen (iostream (^iostream))
484 (with-integrity (:client `(:variable ,self))
485 (setf (^iostream) nil)
486 (close iostream))))
488 ;;; ===========================================================================
489 ;;; A DEFAULT ERROR FUNCTION, USER MAY SUPPLY ANOTHER FUNCTION WHEN MAKING THE
490 ;;; INSTANCE OF TK-FILEEVENT
491 ;;; ===========================================================================
493 (defmethod default-error-fn ((self tk-fileevent) err$)
494 (declare (ignorable err$))
495 (trc "Heya! Error ~a ... :-(" err$)
496 ;; Default action 1: close stream
497 (bwhen (iostream (^iostream))
498 (close iostream)
499 (setf (^iostream) nil))
500 ;; Default action 2: signal error
501 (signal 'tcl-fileevent-error))
503 ;;; ===========================================================================
504 ;;; TESTING
505 ;;; ===========================================================================
507 ;;; With these few lines below we get a simple application with a text widget
508 ;;; that shows data sent to a pipe in that text widget.
510 ;;; The app does this by opening the named pipe for reading. It then waits
511 ;;; for data on the pipe via the Tcl fileevent command. When establishing
512 ;;; the fileevent a set of callbacks is established. The callbacks call
513 ;;; two Lisp functions, depending on the type of channel (read or write.
515 ;;; The callback functions look for the file channel's registered read or
516 ;;; write functions. Those functions are set via the write-fn and read-fn
517 ;;; methods of the tk-fileevent object.
519 ;;; In the test example below we use the read case: the function read-from-pipe
520 ;;; actually reads from the pipe and sends the data to the text widget by
521 ;;; setting the text widgets model value.
522 ;;;
523 ;;; In order to use this example please adapt the code below with a
524 ;;; pipe name suitable for you (see the ^^^^^^^^ marks below).
525 ;;; On Unixes you have to create the pipe with the mkfifo command.
527 ;;; Have fun!
529 ;;; Questions welcome...
531 ;;; Frank Goenninger
532 ;;; frgo@mac.com
534 ;;; May 2006
537 ;;; This is the User Supplied Read Function USRF. USRF has to take care of
538 ;;; closing the channel if it is a file that is read from !!!
539 ;;; The sample supplied here may serve as a template ...
540 (defmethod USRF ((self tk-fileevent) &optional (operation :read))
541 (declare (ignorable operation))
542 (let ((stream (^iostream)))
543 (let ((data (read-line stream nil nil nil)))
544 (trc "*** USRF: data = " data)
545 (if data
546 (setf (value (fm-other :receive-window)) data)
547 (funcall (^eof-fn) self)))))
549 (defmodel fileevent-test-window (window)
551 (:default-initargs
552 :kids (c? (the-kids
553 (mk-stack (:packing (c?pack-self))
554 (mk-label :text "Receive window"
555 :pady 10)
556 (mk-text-widget :id :receive-window
557 ;:state 'disabled
558 :value (c-in "")
559 :height 10
560 :width 80
561 :borderwidth 2
562 :relief 'sunken
563 :pady 5))
564 (mk-fileevent :id :fileevent-test
565 :read-fn 'USRF
566 :iostream (c-in
567 (open "/Users/frgo/dribble.lisp"
568 ;;; Adapt here !!! ^^^^^^^^^^^^^^^^^^^^^^^^^^^
569 :direction :input)))))))
571 ;;; Call this function for testing !!
572 (defun test-fileevent ()
573 (trc "-----------------------------------------------------------------------------")
574 (test-window 'fileevent-test-window)
575 (trc "-----------------------------------------------------------------------------"))
577 #+test
578 (test-window 'fileevent-test-window)