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.