Error handling is hard. Error handling is ugly. Somebody has to do error handling. I try to have Vicare do something about it.
posix specifies an interesting feature: automatically close file descriptors when
a process calls execv() or similar function. This feature is disabled by
default and it is enabled by setting the flag FD_CLOEXEC for the file
descriptor:
int fd = ...; int flags = fcntl(fd, F_GETFD, 0); assert(0 <= flags); flags |= FD_CLOEXEC; fcntl(fd, F_SETFD, flags);
The library (vicare posix) interfaces this facility with the function
fd-set-close-on-exec-mode!. Standard Scheme port objects also have this
feature through a mechanism implemented by (vicare posix): a weak hashtable
is used to register ports for close–on–exec with
port-set-close-on-exec-mode!; then we can use
flush-ports-in-close-on-exec-mode and
close-ports-in-close-on-exec-mode.
Problem: what should happen when flushing or closing such ports fails? This is a difficult problem; whatever the language and technology we use, what should happen when flushing or closing an input/output port fails? Was the data written? Was the file corrupted? Should the software ignore the error and tell the user to go on the beach to take a sunbath?
I have no answer. Vicare has no answer. The best I can think is this:
flush-ports-in-close-on-exec-mode
and close-ports-in-close-on-exec-mode.
flush-output-port or close-port to a close–on–exec
port: the error handler is installed.
flush-output-port or close-port to a close–on–exec port
raises an exception: the error handler is applied to the raised object.
this “solution” is now
implemented in the master branch. It is a pitiful solution. Maybe custom
“flush handlers” and “close handlers” would be better; maybe in the future I will
add them.
Let’s imagine the following program prelude, in which make-test-port is just a
wrapper for the standard open-string-output-port allowing us to raise an
exception when data is written:
#!vicare
(import (vicare)
(prefix (vicare posix) px.)
(only (vicare checks)
with-result
add-result))
;;Select the output buffer size for the port created
;;by MAKE-CUSTOM-TEXTUAL-OUTPUT-PORT.
(string-port-buffer-size 16)
(define (trace template . args)
(when #t
(apply fprintf (current-error-port) template args)))
(define error-on-write
(make-parameter #f))
(define (make-test-port)
;;Create a port that wraps the one created by OPEN-STRING-OUTPUT-PORT.
;;
(receive (subport extract)
(open-string-output-port)
(define (write! src.str src.start count)
(trace "writing ~a chars\n" count)
(when (error-on-write)
(error __who__ "error writing characters" count))
(do ((i 0 (+ 1 i)))
((= i count)
count)
(put-char subport (string-ref src.str (+ i src.start)))))
(define (get-position)
(port-position subport))
(define (set-position! new-position)
(set-port-position! subport new-position))
(define (close)
#f)
(values (make-custom-textual-output-port
"*test-port*" write! get-position set-position! close)
extract)))
(define-constant the-string-len
(* 4 (string-port-buffer-size)))
(define-constant the-string
(make-string the-string-len #\A))
we can simulate an error when flushing with:
(parametrise ((error-on-write #t)) (flush-output-port port))
and an error when closing with:
(parametrise ((error-on-write #t)) (close-port port))
We can test the port with the following code, no error, no close–on–exec:
(let-values (((port extract) (make-test-port)))
(trace "writing ~a chars\n" the-string-len)
(display the-string port)
(trace "flushing output\n")
(flush-output-port port)
(trace "closing\n")
(close-port port)
(receive-and-return (rv)
(extract)
(trace "full contents: ~s\n" rv)))
-| writing 64 chars
-| writing 16 chars
-| writing 16 chars
-| writing 16 chars
-| flushing output
-| writing 16 chars
-| closing
-| full contents: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
We can simulate error–on–flushing with the following code:
(with-result
(let-values (((port extract) (make-test-port)))
(px.port-set-close-on-exec-mode! port)
(trace "writing ~a chars\n" the-string-len)
(add-result 'writing)
(display the-string port)
(trace "flushing output\n")
(add-result 'flushing)
(parametrise ((error-on-write #t))
(px.flush-ports-in-close-on-exec-mode (lambda (E)
(trace "flush exception: ~s\n" E))))
(trace "closing\n")
(add-result 'closing)
(px.close-ports-in-close-on-exec-mode port)
(receive-and-return (rv)
(string-length (extract))
(trace "full contents: ~s\n" rv))))
⇒ (48 (writing flushing closing))
-| writing 64 chars
-| writing 16 chars
-| writing 16 chars
-| writing 16 chars
-| flushing output
-| writing 16 chars
-| flush exception: #[r6rs-record: compound-condition
components=(#[r6rs-record: &error]
#[r6rs-record: &who who=write!]
#[r6rs-record: &message message="error writing characters"]
#[r6rs-record: &irritants irritants=(16)])]
-| closing
-| full contents: 48
when flush-ports-in-close-on-exec-mode applies flush-output-port on the
port: the given error handler is called.
We can simulate error–on–closing with the following code:
(with-result
(let-values (((port extract) (make-test-port)))
(px.port-set-close-on-exec-mode! port)
(trace "writing ~a chars\n" the-string-len)
(add-result 'writing)
(display the-string port)
(trace "closing\n")
(add-result 'closing)
(parametrise ((error-on-write #t))
(px.close-ports-in-close-on-exec-mode (lambda (E)
(trace "close exception: ~s\n" E))))
(receive-and-return (rv)
(string-length (extract))
(trace "full contents: ~s\n" rv))))
⇒ (48 (writing closing))
-| writing 64 chars
-| writing 16 chars
-| writing 16 chars
-| writing 16 chars
-| closing
-| writing 16 chars
-| close exception: #[r6rs-record: compound-condition
components=(#[r6rs-record: &error]
#[r6rs-record: &who who=write!]
#[r6rs-record: &message message="error writing characters"]
#[r6rs-record: &irritants irritants=(16)])]
-| full contents: 48
when close-ports-in-close-on-exec-mode applies close-port on the port:
the given error handler is called.