PageRenderTime 70ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/fileevent.lisp

http://github.com/kennytilton/celtk
Lisp | 578 lines | 347 code | 86 blank | 145 comment | 9 complexity | ac533231adddb20ac62d97d185462e81 MD5 | raw file
  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. ;;; ---------------------------------------------------------------------------
  26. ;;; ===========================================================================
  27. ;;; PACKAGE / EXPORTS
  28. ;;; ===========================================================================
  29. (in-package :celtk)
  30. (eval-when (:load-toplevel :compile-toplevel)
  31. (export '(tk-fileevent
  32. iostream
  33. read-fn
  34. write-fn
  35. eof-fn
  36. mk-fileevent
  37. stream-2-in-fd
  38. stream-2-out-fd)))
  39. ;;; ===========================================================================
  40. ;;; TK-FILEEVENT MODEL
  41. ;;; ===========================================================================
  42. (defmodel tk-fileevent (widget)
  43. ((.md-name
  44. :accessor id :initarg :id
  45. :initform (c-in nil)
  46. :documentation "ID of the fileevent instance.")
  47. (input-fd
  48. :accessor input-fd :initarg :input-fd
  49. :initform (c? (if (^iostream)
  50. (stream-2-in-fd (^iostream))))
  51. :documentation "The input/read file descriptor - internal use only.")
  52. (output-fd
  53. :accessor output-fd
  54. :initarg :output-fd
  55. :initform (c? (if (^iostream)
  56. (stream-2-out-fd (^iostream))))
  57. :documentation "The output/write file descriptor - internal use only.")
  58. (in-tcl-channel
  59. :accessor in-tcl-channel :initarg :in-tcl-channel
  60. :initform (c? (fd-to-tcl-channel (^tki) (^input-fd)))
  61. :documentation "The TCL channel generated from the input file descriptor. - Internal use only.")
  62. (out-tcl-channel
  63. :accessor out-tcl-channel :initarg :in-tcl-channel
  64. :initform (c? (fd-to-tcl-channel (^tki) (^output-fd)))
  65. :documentation "The TCL channel generated from the output file descriptor. - Internal use only.")
  66. (in-tcl-ch-name
  67. :accessor in-tcl-ch-name :initarg :in-tcl-ch-name
  68. :initform (c? (if (^in-tcl-channel)
  69. (Tcl_GetChannelName (^in-tcl-channel))
  70. nil))
  71. :documentation "The input TCL channel's name as passed to the fileevent command. - Internal use only.")
  72. (out-tcl-ch-name
  73. :accessor out-tcl-ch-name :initarg :in-tcl-ch-name
  74. :initform (c? (if (^out-tcl-channel)
  75. (Tcl_GetChannelName (^out-tcl-channel))
  76. nil))
  77. :documentation "The output TCL channel's name as passed to the fileevent command. - Internal use only.")
  78. (iostream
  79. :accessor iostream :initarg :iostream
  80. :initform (c-in nil)
  81. :documentation "The Lisp stream to be monitored - API: initarg,setf.")
  82. (readable-cb
  83. :accessor readable-cb :initarg :readable-cb
  84. :initform (c-in nil)
  85. :documentation "The readable callback. A dispatcher function used to call the function supplied via the read-fn slot. - Internal use only.")
  86. (writeable-cb
  87. :accessor writeable-cb :initarg :writeable-cb
  88. :initform (c-in nil)
  89. :documentation "The writeable callback. A dispatcher function used to call the function supplied via the read-fn slot. - Internal use only.")
  90. (eof-cb
  91. :accessor eof-cb :initarg :eof-cb
  92. :initform (c-in nil)
  93. :documentation "The eof callback. A dispatcher function used to call the function supplied via the eof-fn slot. - Internal use only.")
  94. (error-cb
  95. :accessor error-cb :initarg :error-cb
  96. :initform (c-in nil)
  97. :documentation "The error callback. A dispatcher function used to call the function supplied via the error-fn slot. - Internal use only.")
  98. (tki
  99. :accessor tki :initarg :tki
  100. :initform (c-in nil)
  101. :documentation "The Tcl/Tk Interpreter used. - API: initarg.")
  102. (opcode
  103. :accessor opcode :initarg :opcode
  104. :initform (file-event-opcode-cell-rule)
  105. :documentation "The opcode slot is used to control the operaion of the fileevent instance. - Internal use only.")
  106. (read-fn
  107. :accessor read-fn :initarg :read-fn
  108. :initform (c-in nil)
  109. :documentation "User supplied function, gets called when iostream is ready for reading. Gets iostream as parameter. - API: initarg, setf")
  110. (write-fn
  111. :accessor write-fn :initarg :write-fn
  112. :initform (c-in nil)
  113. :documentation "User supplied function, gets called when iostream is ready for writing. Gets iostream as parameter. - API: initarg, setf")
  114. (eof-fn
  115. :accessor eof-fn :initarg :eof-fn
  116. :initform (c-in nil)
  117. :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).")
  118. (error-fn
  119. :accessor error-fn :initarg :error-fn
  120. :initform (c-in nil)
  121. :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)."))
  122. (:default-initargs
  123. :id (gensym "tk-fileevent-")
  124. :eof-fn 'default-eof-fn))
  125. ;;; ===========================================================================
  126. ;;; CELL RULE: FILE-EVENT/OPCODE
  127. ;;; ===========================================================================
  128. ;;;
  129. ;;; Depending on opcode call the appropriate function to handle the various
  130. ;;; cases/combinations of input-fd, output-fd, and the previously executed
  131. ;;; update operation.
  132. (defun file-event-opcode-cell-rule ()
  133. "Set the opcode depending on values of input-fd, output-fd, iostream, readable-cb, writeable-cb"
  134. (c? (cond
  135. ((not (or (^input-fd) (^output-fd) .cache))
  136. :nop)
  137. ((and (^input-fd) (^iostream) (^readable-cb))
  138. :update-input-tk-fileevent)
  139. ((and (^output-fd) (^iostream) (^writeable-cb))
  140. :update-output-tk-fileevent)
  141. ((not (or (^iostream) (^input-fd)))
  142. :reset-input-tk-fileevent)
  143. ((not (or (^iostream) (^output-fd)))
  144. :reset-output-tk-fileevent)
  145. (t :nop))))
  146. ;;; ===========================================================================
  147. ;;; INIT-TK-FILEEVENT - CALLED UPON INITIALIZATION
  148. ;;; ===========================================================================
  149. (defun init-tk-fileevent (tki)
  150. (assert tki)
  151. ;; Nop - all init done in observers now.
  152. )
  153. ;;; ===========================================================================
  154. ;;; FILEEVENT HELPER METHODS AND FUCTIONS
  155. ;;; ===========================================================================
  156. (defmethod set-tk-readable ((self tk-fileevent) ch-name path type)
  157. ;; frgo, 2006-05-26:
  158. ;; The code here was aimed at EOF checking after reading...
  159. ;; So the API needs rework...
  160. ;; STATUS: IN WORK
  161. ;;
  162. ;; (tk-format-now " proc readable {channel path} {
  163. ;; # check for async errors (sockets only, I think)
  164. ;; if {[string length [set err [fconfigure $channel -error]]]} {
  165. ;; error-cb $path $err
  166. ;; close $channel
  167. ;; return
  168. ;; }
  169. ;; # Read a line from the channel
  170. ;; if {[catch {set line [gets $channel]} err]} {
  171. ;; error-cb $path $err
  172. ;; close $channel
  173. ;; return
  174. ;; }
  175. ;; if {[string length $line]} {
  176. ;; received-cb $path $line
  177. ;; }
  178. ;; # check for eof
  179. ;; if {[eof $channel]} {
  180. ;; eof-cb $path
  181. ;; close $channel
  182. ;; }
  183. ;; }")
  184. ;; frgo: Old code snippet:
  185. ;; (tk-format-now "proc readable {channel path} { if [ eof $channel ] then { eof-cb $path } else { readable-cb $path } }")
  186. ;; (tk-format-now "fileevent ~A readable [list readable ~A ~A]"
  187. ;; ch-name
  188. ;; ch-name
  189. ;; path)
  190. (trc "tk-set-readable sees ch-name path type" ch-name path type)
  191. (tk-format-now
  192. "proc readable {channel path type} {
  193. if {! [string compare $type \"socket\"]} {
  194. if {[string length [set err [fconfigure $channel -error]]]} {
  195. error-cb $path $err
  196. close $channel
  197. return
  198. }
  199. }
  200. readable-cb $path
  201. catch { if {[eof $channel]} {
  202. eof-cb $path
  203. close $channel
  204. }
  205. }
  206. }")
  207. (tk-format-now "fileevent ~A readable [list readable ~A ~A ~a]"
  208. ch-name
  209. ch-name
  210. path
  211. type)
  212. )
  213. (defmethod set-tk-writeable ((self tk-fileevent) ch-name path type)
  214. (tk-format-now "proc writeable {channel path type} { if [ eof $channel ] then { eof-cb $path } else { writeable-cb $path } }")
  215. (tk-format-now "fileevent ~A writeable [list writeable ~A ~A ~a]"
  216. ch-name
  217. ch-name
  218. path
  219. type))
  220. ;;; ===========================================================================
  221. ;;; FILEEVENT CONDITIONS
  222. ;;; ===========================================================================
  223. (define-condition tcl-fileevent-error (error)
  224. ())
  225. ;;; ===========================================================================
  226. ;;; OBSERVERS - USED TO SEND UPDATES TO TK LAND
  227. ;;; ===========================================================================
  228. (defobserver opcode ((self tk-fileevent))
  229. (let ((*tki* (tki self)))
  230. (ecase new-value
  231. ((:init-tk-fileevent)
  232. (init-tk-fileevent (tki self)))
  233. ((:update-input-tk-fileevent)
  234. (let* ((channel (in-tcl-channel self))
  235. (path (path self))
  236. (ch-name (Tcl_GetChannelName channel))
  237. (ch-type (Tcl_GetChannelType channel)))
  238. (set-tk-readable self
  239. ch-name
  240. path
  241. (foreign-slot-value ch-type
  242. 'Tcl_ChannelType
  243. 'typeName ))))
  244. ((:update-output-tk-fileevent)
  245. (let* ((channel (out-tcl-channel self))
  246. (path (path self))
  247. (ch-name (Tcl_GetChannelName channel))
  248. (ch-type (Tcl_GetChannelType channel)))
  249. (set-tk-writeable self
  250. ch-name
  251. path
  252. (foreign-slot-value ch-type
  253. 'Tcl_ChannelType
  254. 'typeName))))
  255. ((:reset-input-tk-fileevent)
  256. ;; Do nothing
  257. nil)
  258. ((:reset-output-tk-fileevent)
  259. ;; Do nothing
  260. nil)
  261. ((:nop)
  262. ;; Do nothing
  263. nil))))
  264. (defobserver in-tcl-channel ((self tk-fileevent))
  265. (let ((*tki* (tki self)))
  266. (if (and new-value
  267. (not old-value))
  268. (Tcl_RegisterChannel *tki* new-value))
  269. (if (and old-value (not new-value))
  270. (progn
  271. (tk-format-now "fileevent ~A readable {}"
  272. (Tcl_GetChannelName old-value))
  273. (Tcl_UnregisterChannel *tki* old-value)))))
  274. (defobserver out-tcl-channel ((self tk-fileevent))
  275. (let ((*tki* (tki self)))
  276. (if (and new-value (not old-value))
  277. (Tcl_RegisterChannel *tki* new-value))
  278. (if (and old-value (not new-value))
  279. (progn
  280. (tk-format-now "fileevent ~A writeable {}"
  281. (Tcl_GetChannelName old-value))
  282. (Tcl_UnregisterChannel *tki* old-value)))))
  283. (defobserver readable-cb ((self tk-fileevent))
  284. (if new-value
  285. (tcl-create-command *tki*
  286. "readable-cb"
  287. new-value
  288. (null-pointer)
  289. (null-pointer))))
  290. (defobserver writeable-cb ((self tk-fileevent))
  291. (if new-value
  292. (tcl-create-command *tki*
  293. "writeable-cb"
  294. new-value
  295. (null-pointer)
  296. (null-pointer))))
  297. (defobserver eof-cb ((self tk-fileevent))
  298. (if new-value
  299. (tcl-create-command *tki*
  300. "eof-cb"
  301. new-value
  302. (null-pointer)
  303. (null-pointer))))
  304. (defobserver error-cb ((self tk-fileevent))
  305. (if new-value
  306. (tcl-create-command *tki*
  307. "error-cb"
  308. new-value
  309. (null-pointer)
  310. (null-pointer))))
  311. ;;; ===========================================================================
  312. ;;; HELPER FUNCTIONS - FILE DESCRIPTOR TO STREAM AND CHANNEL
  313. ;;; ===========================================================================
  314. (defun fd-to-tcl-channel (interp fd)
  315. (assert interp)
  316. (if fd
  317. (let ((channel (Tcl_MakeFileChannel fd 6))) ;; 6 = READ/WRITE
  318. (if channel
  319. channel
  320. (error "*** Tcl error: ~a" (tcl-get-string-result interp))))))
  321. (defun stream-2-out-fd (stream) ;; FRGO: PORTING...
  322. #+allegro
  323. (excl:stream-output-fn stream)
  324. #-allegro
  325. (error "STREAM-2-OUT-FD: Not implemented for ~A Version ~A. Sorry."
  326. (lisp-implementation-type)
  327. (lisp-implementation-version))
  328. )
  329. (defun stream-2-in-fd (stream) ;; FRGO: PORTING...
  330. #+allegro
  331. (excl:stream-input-fn stream)
  332. #-allegro
  333. (error "STREAM-2-IN-FD: Not implemented for ~A Version ~A. Sorry."
  334. (lisp-implementation-type)
  335. (lisp-implementation-version))
  336. )
  337. ;;; ===========================================================================
  338. ;;; CALLBACKS
  339. ;;; ===========================================================================
  340. (defcallback readable-cb :int
  341. ((clientData :pointer)
  342. (interp :pointer)
  343. (argc :int)
  344. (argv :pointer))
  345. (declare (ignore clientData argc interp))
  346. (let* ((path (foreign-string-to-lisp (mem-aref argv :pointer 1)))
  347. (self (gethash path (dictionary *tkw*))))
  348. (bwhen (fn (^read-fn))
  349. (funcall fn self :read)))
  350. (values (foreign-enum-value 'tcl-retcode-values :tcl-ok)))
  351. (defcallback writeable-cb :int
  352. ((clientData :pointer)
  353. (interp :pointer)
  354. (argc :int)
  355. (argv :pointer))
  356. (declare (ignore clientData argc interp))
  357. (let* ((path (foreign-string-to-lisp (mem-aref argv :pointer 1)))
  358. (self (gethash path (dictionary *tkw*))))
  359. (bwhen (fn (^write-fn))
  360. (funcall fn self :write)))
  361. (values (foreign-enum-value 'tcl-retcode-values :tcl-ok)))
  362. (defcallback eof-cb :int
  363. ((clientData :pointer)
  364. (interp :pointer)
  365. (argc :int)
  366. (argv :pointer))
  367. (declare (ignore clientData interp argc))
  368. (trc "EOF-CB !!!")
  369. (let* ((path (foreign-string-to-lisp (mem-aref argv :pointer 1)))
  370. (self (gethash path (dictionary *tkw*))))
  371. (bwhen (fn (^eof-fn))
  372. (funcall fn self)))
  373. (values (foreign-enum-value 'tcl-retcode-values :tcl-ok)))
  374. (defcallback error-cb :int
  375. ((clientData :pointer)
  376. (interp :pointer)
  377. (argc :int)
  378. (argv :pointer))
  379. (declare (ignore clientData interp argc))
  380. (trc "ERROR-CB !!!")
  381. (let* ((path (foreign-string-to-lisp (mem-aref argv :pointer 1)))
  382. (err$ (foreign-string-to-lisp (mem-aref argv :pointer 2)))
  383. (self (gethash path (dictionary *tkw*))))
  384. (bwhen (fn (^error-fn))
  385. (funcall fn self err$)))
  386. (values (foreign-enum-value 'tcl-retcode-values :tcl-error)))
  387. ;;; ===========================================================================
  388. ;;; MK-FILEEVENT: CONVENIENCE MACRO
  389. ;;; ===========================================================================
  390. (defmacro mk-fileevent (&rest inits)
  391. `(make-instance 'tk-fileevent
  392. :tki *tki*
  393. :readable-cb (get-callback 'readable-cb)
  394. :writeable-cb (get-callback 'writeable-cb)
  395. :eof-cb (get-callback 'eof-cb)
  396. :error-cb (get-callback 'error-cb)
  397. :fm-parent *parent*
  398. ,@inits))
  399. ;;; ===========================================================================
  400. ;;; A DEFAULT EOF FUNCTION, USER MAY SUPPLY ANOTHER FUNCTION WHEN MAKING THE
  401. ;;; INSTANCE OF TK-FILEEVENT
  402. ;;; ===========================================================================
  403. (defmethod default-eof-fn ((self tk-fileevent))
  404. ;; Default action: close stream
  405. (bwhen (iostream (^iostream))
  406. (with-integrity (:client `(:variable ,self))
  407. (setf (^iostream) nil)
  408. (close iostream))))
  409. ;;; ===========================================================================
  410. ;;; A DEFAULT ERROR FUNCTION, USER MAY SUPPLY ANOTHER FUNCTION WHEN MAKING THE
  411. ;;; INSTANCE OF TK-FILEEVENT
  412. ;;; ===========================================================================
  413. (defmethod default-error-fn ((self tk-fileevent) err$)
  414. (declare (ignorable err$))
  415. (trc "Heya! Error ~a ... :-(" err$)
  416. ;; Default action 1: close stream
  417. (bwhen (iostream (^iostream))
  418. (close iostream)
  419. (setf (^iostream) nil))
  420. ;; Default action 2: signal error
  421. (signal 'tcl-fileevent-error))
  422. ;;; ===========================================================================
  423. ;;; TESTING
  424. ;;; ===========================================================================
  425. ;;;
  426. ;;; With these few lines below we get a simple application with a text widget
  427. ;;; that shows data sent to a pipe in that text widget.
  428. ;;;
  429. ;;; The app does this by opening the named pipe for reading. It then waits
  430. ;;; for data on the pipe via the Tcl fileevent command. When establishing
  431. ;;; the fileevent a set of callbacks is established. The callbacks call
  432. ;;; two Lisp functions, depending on the type of channel (read or write.
  433. ;;;
  434. ;;; The callback functions look for the file channel's registered read or
  435. ;;; write functions. Those functions are set via the write-fn and read-fn
  436. ;;; methods of the tk-fileevent object.
  437. ;;;
  438. ;;; In the test example below we use the read case: the function read-from-pipe
  439. ;;; actually reads from the pipe and sends the data to the text widget by
  440. ;;; setting the text widgets model value.
  441. ;;;
  442. ;;; In order to use this example please adapt the code below with a
  443. ;;; pipe name suitable for you (see the ^^^^^^^^ marks below).
  444. ;;; On Unixes you have to create the pipe with the mkfifo command.
  445. ;;;
  446. ;;; Have fun!
  447. ;;;
  448. ;;; Questions welcome...
  449. ;;;
  450. ;;; Frank Goenninger
  451. ;;; frgo@mac.com
  452. ;;;
  453. ;;; May 2006
  454. ;;; This is the User Supplied Read Function USRF. USRF has to take care of
  455. ;;; closing the channel if it is a file that is read from !!!
  456. ;;; The sample supplied here may serve as a template ...
  457. (defmethod USRF ((self tk-fileevent) &optional (operation :read))
  458. (declare (ignorable operation))
  459. (let ((stream (^iostream)))
  460. (let ((data (read-line stream nil nil nil)))
  461. (trc "*** USRF: data = " data)
  462. (if data
  463. (setf (value (fm-other :receive-window)) data)
  464. (funcall (^eof-fn) self)))))
  465. (defmodel fileevent-test-window (window)
  466. ()
  467. (:default-initargs
  468. :kids (c? (the-kids
  469. (mk-stack (:packing (c?pack-self))
  470. (mk-label :text "Receive window"
  471. :pady 10)
  472. (mk-text-widget :id :receive-window
  473. ;:state 'disabled
  474. :value (c-in "")
  475. :height 10
  476. :width 80
  477. :borderwidth 2
  478. :relief 'sunken
  479. :pady 5))
  480. (mk-fileevent :id :fileevent-test
  481. :read-fn 'USRF
  482. :iostream (c-in
  483. (open "/Users/frgo/dribble.lisp"
  484. ;;; Adapt here !!! ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  485. :direction :input)))))))
  486. ;;; Call this function for testing !!
  487. (defun test-fileevent ()
  488. (trc "-----------------------------------------------------------------------------")
  489. (test-window 'fileevent-test-window)
  490. (trc "-----------------------------------------------------------------------------"))
  491. #+test
  492. (test-window 'fileevent-test-window)