I'm trying to revive Hemlock, which now appears abandoned (original author has been absent for some time now). Mostly this has been a straightforward process of clearing out the bit rot, but I'm now running up against a rather annoying problem with signal handling and threads. The system works fine(-ish) now, and I'm trying to solve the problem of TTY screen redisplay after a screen resize (you now have to use C-l (redisplay-all) to redraw the screen. Annoying, but not a deal-breaker.
My initial solution works some of the time. But if you're unlucky enough to receive SIGWINCH
when not in the main thread the system fails. Signals and threads are a well-known problem, but here a solution eludes me, partly because the system uses IOLIB which has zero documentation and doesn't appear to allow me to mask signals in any way that I can see.
cl-async has signal handling, but refactoring to use it will take more time than I have right now.
I could cobble together something that checks row/column count each time through the main event loop and then call redisplay-all if required, but that seems clumsy and inefficient.
Does anyone have any ideas? I don't care if it's SBCL specific for the moment; I'd just like it to work first.
Typically the signal handler will just inform the main loop that something happened, and not do anything unsafe according to signal-safety(7) (you need to write async-signal-safe functions).
So you would have to check in the main loop if the redisplay need to be done, but that may be only one boolean flag. And your handler would set the flag.
There is one problem however that may exists, namely that the event loop is currently waiting for new events, and blocked. You want the signal handler to somehow interact with the event loop so that it wakes up.
I'm reproducing here an idea I saw when working on C base code that used such signal handlers. The principle here is to have a dedicated file descriptor to which you can attach actions to perform during idle time (whenever you want to interrupt waiting for new events and perform some actions).
edit: after reviewing said code in C, it appears that it uses eventfd(2)
, which is exactly the right way to do it. Here I allocate a pipe, because currently iolib does not expose this syscall (nb: there is a pull request).
Here is some preliminary code in one block, with comments, I'll explain a bit more below:
(defpackage #:my-iolib (:use :cl))
(in-package #:my-iolib)
;;;; DEBUGGING
(defun dbg (&rest stuff)
(fresh-line)
(write stuff :stream *debug-io*)
(finish-output *debug-io*))
;;;; PIPE structures
(defstruct pipe write-fd read-fd)
(defun open-pipe ()
(multiple-value-bind (read write) (iolib/syscalls:pipe)
(make-pipe :read-fd read :write-fd write)))
(defun close-pipe (pipe)
(ignore-errors (isys:close (pipe-read-fd pipe)))
(ignore-errors (isys:close (pipe-write-fd pipe))))
(defmacro with-pipe ((var) &body body)
`(let ((,var (open-pipe)))
(unwind-protect (progn ,@body)
(close-pipe ,var))))
;;;; STREAMS FOR PIPE WITH SMALL BUFFERS
(defclass small-stream
;; maybe a more precise set of super mixin classes may
;; work too, but at the moment this one works
(iolib:dual-channel-gray-stream)
()
;; small values for buffers because we only use the fds to
;; wakeup the event loop; I'm not sure if size 1 is a good
;; idea
(:default-initargs :input-buffer-size 1 :output-buffer-size 1))
(defun pipe-read-stream (pipe)
(make-instance 'small-stream :fd (pipe-read-fd pipe)))
(defun pipe-write-stream (pipe)
(make-instance 'small-stream :fd (pipe-write-fd pipe)))
(defmacro with-pipe-streams ((read write) pipe &body body)
(check-type pipe (and symbol (not null)))
`(with-open-stream (,read (pipe-read-stream ,pipe))
(with-open-stream (,write (pipe-write-stream ,pipe))
,@body)))
;;;; SIGNALS
;; copied from question
(defmacro set-signal-handler (signo &body body)
(let ((handler (gensym "HANDLER")))
`(progn
(cffi:defcallback ,handler :void ((signo :int))
(declare (ignore signo))
,@body)
(cffi:foreign-funcall "signal"
:int
,signo
:pointer
(cffi:callback ,handler)))))
Now we can define and call the following function:
(defun test ()
(with-pipe (idle-pipe)
(dbg :open-idle-streams)
(with-pipe-streams (idle-read idle-write) idle-pipe
(let ((flag (bt2:make-atomic-integer)))
(flet ((idle-interrupt ()
(when (bt2:atomic-integer-compare-and-swap flag 0 1)
(write-byte 0 idle-write)
(finish-output idle-write)))
(idle-cleanup ()
(when (bt2:atomic-integer-compare-and-swap flag 1 0)
(read-byte idle-read))))
(dbg :start-event-base)
(iomux:with-event-base (base)
(dbg :register-idle-handler)
;; monitor the "idle" pipe, then inside the handler,
;; perform any task that you want to do when you
;; wake-up from the event loop. For example, here
;; you can check if the screen needs to be
;; redisplayed.
(iomux:set-io-handler base
(pipe-read-fd idle-pipe)
:read
(lambda (&rest args)
(apply #'dbg :on-idle args)
(idle-cleanup)
;; here we just request the
;; event loop to stop
(iomux:exit-event-loop base)))
;; This handler simulates any kind of situation
;; where we want to wake up the event loop, by
;; writing at least one byte in the "idle" pipe.
(dbg :setup-signal-handler)
(set-signal-handler iolib/syscalls:sigusr2
(idle-interrupt))
;; wait for events until exit
(dbg :wait-for-event)
(iomux:event-dispatch base)))))
(dbg :exit-event-base)))
We open a pipe, attach streams to both its file descriptors, and observe the pipe for read events. The handler for this file descriptor is the code that you want to execute when you interrupt your main event loop, ie. refreshing the screen, etc. In theory we could also have a queue of idle events to process, but let's not go there for now.
The code then setups a signal handler for SIGUSR2 and wait forever for events. The signal handler only writes to the pipe and exits, something that should not be a problem for a signal handler. We also protect the code with an atomic variable to avoid having concurrent writes (and reads).
The output so far is:
(:OPEN-IDLE-STREAMS)
(:START-EVENT-BASE)
(:REGISTER-IDLE-HANDLER)
(:SETUP-SIGNAL-HANDLER)
(:WAIT-FOR-EVENT)
In a terminal I send a signal as usual:
kill -n SIGUSR2 PID
And the handler is executed:
(:ON-IDLE 3 :READ NIL)
(:EXIT-EVENT-BASE)
NIL
I want to point out also that it is preferable to use sigaction
when possible (What is the difference between sigaction and signal?).
If you get a lot of resize signals when interacting with your window it might be a good idea to debounce them by having a small timer that you reset on each resize event. When the timer actually fires the redisplay can happen.