A Clojure library to handle POSIX signals in JVM applications. It does the low-level work and gives you a simple interface to the signal handlers.
Use beckon by default. It runs on JDK 8+ with no extra JVM flags. Use
beckon-ffm only if you want the
experimental Foreign Function & Memory backend on JDK 22+.
Leiningen (project.clj):
[net.clojars.savya/beckon "0.4.3"]
Clojure CLI (deps.edn):
net.clojars.savya/beckon {:mvn/version "0.4.3"}
beckon runs on JDK 8 or later with no extra JVM flags. It wraps
sun.misc.Signal, which stays available through the jdk.unsupported module on
current JDKs. Thus you do not need --add-exports or --add-opens.
To catch SIGINT and print a message when someone interrupts the process, start
your Emacs nREPL with nrepl-jack-in or a similar command. Then write the
following:
(require 'beckon)
(let [print-function (fn [] (println "Hahah, nothing can stop me!"))]
(reset! (beckon/signal-atom "INT") #{print-function}))
That is all. To check that this works, use the raise! function, which raises a
POSIX signal to the VM:
(beckon/raise! "INT")
; prints nothing
Why did raise! print nothing? When the JVM receives a signal, it starts a new
thread with maximum priority and handles the signal asynchronously. Thus the
output does not show in the nREPL window. Look in the *nrepl-server* buffer to
see the message.
By default, signals such as SIGTERM and SIGINT terminate the running VM. Be careful. You can experiment with them in a REPL:
(beckon/raise! "TERM")
; NB: This will terminate nREPL.
If the signal handling is in a bad state, you can go back to the default:
(beckon/reinit! "INT")
; Reinitializes the SIGINT signal handler.
(beckon/raise! "INT")
; NB: This will terminate your JVM process.
That is all you need to know to work with beckon.
beckon has 4 core functions: signal-atom, raise!, reinit! and
reinit-all!. Usually you need only signal-atom in a production system. The
other functions help you to debug, and to reset the signal handling to the
initial setup of signal handlers when the JVM starts.
signal-atomsignal-atom is the core of this library. It uses atoms to set up signal
handlers. It returns an atom. The atom has a validator function, so the only
legal values are Seqable collections in which every element is Runnable. All
Clojure functions implement Runnable, but only a function that takes zero
arguments works as a Runnable.
beckon requires a Seqable of Runnable in the atom, because this lets you add more than one independent signal handler to a single signal. beckon runs the signal handlers in sequence. If a function throws an exception, beckon stops the signal handling and throws no exception. If a function throws an error, the whole signal handling crashes. You can use this behavior to get conditional dispatch of functions. For example:
(reset! (beckon/signal-atom "INT")
[(fn [] (println "foo"))
(fn [] (println "bar") (throw (Exception.)))
(fn [] (println "We'll never see this"))])
Will only print foo and bar.
This is not a good way to do dispatch. Put this logic in the functions when possible.
beckon updates the signal handler when the atom changes, but a change to the signal handler does not update the atom. If you use beckon, do not also set signal handling through another library or through the native Java interface.
raise!raise! sends a signal of the type given as input. For example, (beckon/raise! "INT") has the same effect as a SIGINT signal sent to the JVM process. Use it
to check that your signal handlers work as intended.
reinit! and reinit-all!reinit! and reinit-all! reset the signal handlers to their state when the
JVM started. reinit! takes one argument, the signal to reset. reinit-all!
takes no argument and resets every signal.
When the JVM receives a signal, it starts a new thread at
Thread.MAX_PRIORITY and runs it asynchronously. This is why nREPL shows no
output, although output works in a command-line program. It is better to send a
message from the signal handler to a logger or a printer than to print in the
signal handler.
This list gives the common problems with this library. If it does not help you, add a new issue.
Q: My infinite sequence does not work with this library. Why?
A: For speed, beckon puts the collection of functions into a Java array.
An infinite sequence does not fit in a Java array.
Q: Why does the collection of functions accept keywords, symbols, and other
values that are clearly not functions?
A: Keywords, symbols and some persistent collections implement the IFn
interface in Clojure, and thus they also implement Runnable. They implement
Runnable, but they cannot return a value of use. A later version will correct
this.
By default beckon uses sun.misc.Signal, which works on JDK 8+ with no extra
JVM flags. The JDK marks that API "internal proprietary", so beckon keeps all
use of it behind a small internal SignalBackend seam. An alternative backend
can replace it if necessary.
An experimental alternative uses the Foreign Function & Memory API (JDK 22+). It
ships as a separate package,
beckon-ffm (Linux signalfd,
macOS/BSD kqueue). Add it with beckon:
[net.clojars.savya/beckon "0.4.3"]
[net.clojars.savya/beckon-ffm "0.1.7"]
Then start the JVM with -Dbeckon.signal.backend=ffm --enable-native-access=ALL-UNNAMED to select it. It is a separate package
because it needs JDK 22+, but this core jar targets JDK 8. sun.misc stays the
default.
Copyright © 2013 Jean Niklas L'orange.
Maintenance fork (2026) by Savyasachi, original: https://github.com/hyPiRion/beckon. Distributed under the Eclipse Public License 1.0, preserving the original license.
Distributed under the Eclipse Public License, the same as Clojure.
Can you improve this documentation? These fine people already did:
Savyasachi & Jean Niklas L'orangeEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |