On errors and close–on–exec ports (2015 February 14)

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:

  1. It is possible to hand an error handler to flush-ports-in-close-on-exec-mode and close-ports-in-close-on-exec-mode.
  2. Before applying flush-output-port or close-port to a close–on–exec port: the error handler is installed.
  3. If applying flush-output-port or close-port to a close–on–exec port raises an exception: the error handler is applied to the raised object.
  4. If the error handler itself raises an exception: such second exception is blocked and discarded.

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.

Some examples

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.