diff --git a/README.md b/README.md index a8ed6a3..89cc9ae 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ for each. Here are some additional projects that provide Trapperkeeper services, and other related functionality: -* [trapperkeeper-webserver-jetty9](https://github.com/openvoxproject/trapperkeeper-webserver-jetty9): a Jetty9-based webserver for use with TK applications +* [trapperkeeper-webserver-jetty10](https://github.com/openvoxproject/trapperkeeper-webserver-jetty10): a jetty10-based webserver for use with TK applications * [trapperkeeper-rpc](https://github.com/puppetlabs/trapperkeeper-rpc): a TK service that allows you to easily build a way to call remote TK services over RPC * [trapperkeeper-metrics](https://github.com/openvoxproject/trapperkeeper-metrics): a TK service that manages the life cycle of a [MetricRegistry](https://github.com/dropwizard/metrics), so that all of your TK services can register metrics with a common configuration syntax. * [trapperkeeper-comidi-metrics](https://github.com/openvoxproject/trapperkeeper-comidi-metrics): a TK utility library that provides middleware to automatically generate metrics for all requests to each of your bidi/comidi HTTP routes. diff --git a/project.clj b/project.clj index d81c156..5635f06 100644 --- a/project.clj +++ b/project.clj @@ -44,7 +44,9 @@ [org.openvoxproject/kitchensink "3.4.2" :exclusions [cheshire]] [org.openvoxproject/i18n] [nrepl/nrepl] - [io.github.clj-kondo/config-slingshot-slingshot "1.0.0"]] + [io.github.clj-kondo/config-slingshot-slingshot "1.0.0"] + + [com.kohlschutter.junixsocket/junixsocket-core "2.10.1" :extension "pom"]] :deploy-repositories [["releases" {:url "https://clojars.org/repo" :username :env/CLOJARS_USERNAME @@ -74,7 +76,7 @@ [org.openvoxproject/i18n "0.9.3"]] :eastwood {:ignored-faults {:reflection {puppetlabs.trapperkeeper.logging [{:line 92}] - puppetlabs.trapperkeeper.internal [{:line 128}] + puppetlabs.trapperkeeper.internal [{:line 174}] puppetlabs.trapperkeeper.testutils.logging true puppetlabs.trapperkeeper.testutils.logging-test true puppetlabs.trapperkeeper.services.nrepl.nrepl-service-test true diff --git a/src/puppetlabs/trapperkeeper/internal.clj b/src/puppetlabs/trapperkeeper/internal.clj index d26ef21..38dfb64 100644 --- a/src/puppetlabs/trapperkeeper/internal.clj +++ b/src/puppetlabs/trapperkeeper/internal.clj @@ -1,6 +1,13 @@ (ns puppetlabs.trapperkeeper.internal - (:import (clojure.lang ExceptionInfo IFn IDeref) - (java.lang ArithmeticException NumberFormatException)) + (:import + (clojure.lang ExceptionInfo IFn IDeref) + (java.lang ArithmeticException NumberFormatException) + (java.io File) + (java.net DatagramPacket SocketException) + ;; Looks like JDK 16+ support doesn't do SOCK_DGRAM yet + (org.newsclub.net.unix AFUNIXSocketAddress AFUNIXDatagramSocket) + (java.nio.charset StandardCharsets)) + (:require [clojure.tools.logging :as log] [beckon] [plumbing.graph :as graph] @@ -16,6 +23,24 @@ [clojure.core.async.impl.protocols :as async-prot] [me.raynes.fs :as fs])) +;; helper functions to resolve unknown type references +;; this feels cleaner compared to type hints, but I've no idea what I'm doing +;; - bastelfreak 2025-08-26 +(defn utf8-bytes + "Convert a String to a UTF-8 byte array." + [^String s] + (.getBytes s StandardCharsets/UTF_8)) + +(defn socket-addr + "Convert a String path to a reflection-free AFUNIXSocketAddress." + [^String path] + (AFUNIXSocketAddress/of (File. path))) + +(defn send-packet! + "Send a UTF-8 message via a connected AFUNIXDatagramSocket." + [^AFUNIXDatagramSocket s ^String msg] + (let [^bytes buf (utf8-bytes msg)] + (.send s (DatagramPacket. buf (alength buf))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Schemas ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -28,6 +53,27 @@ ;; Private ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Optional systemd support + +(def systemd-notify-socket (System/getenv "NOTIFY_SOCKET")) + +(defn maybe-notify-systemd + "Send a message to systemd NOTIFY_SOCKET if available." + [^String message] + (when-let [^String socket-path systemd-notify-socket] + (let [^AFUNIXSocketAddress addr (socket-addr socket-path)] + (with-open [^AFUNIXDatagramSocket s (AFUNIXDatagramSocket/newInstance)] + (try + (.connect s addr) + (send-packet! s message) + (catch SocketException _ + (log/warn (i18n/trs "Unable to connect to NOTIFY_SOCKET {0}" + (pr-str socket-path))))))))) + +(defn notice-service-ready [] (maybe-notify-systemd "READY=1\n")) +(defn notice-service-reloading [] (maybe-notify-systemd "RELOADING=1\n")) +(defn notice-service-stopping [] (maybe-notify-systemd "STOPPING=1\n")) + ;; This is (eww) a global variable that holds a reference to all of the running ;; Trapperkeeper apps in the process. It can be used when connecting via nrepl ;; to allow you to do useful things, and also may be used for other things @@ -615,11 +661,13 @@ this) (a/start [this] (run-lifecycle-fns app-context s/start "start" ordered-services) + (notice-service-ready) (inc-restart-counter! this) this) (a/stop [this] (a/stop this false)) (a/stop [this throw?] + (notice-service-stopping) (let [errors (shutdown! app-context)] (if (and throw? (seq errors)) (let [msg (i18n/trs "Error during app shutdown!")] @@ -628,10 +676,12 @@ this))) (a/restart [this] (try + (notice-service-reloading) (run-lifecycle-fns app-context s/stop "stop" (reverse ordered-services)) (doseq [svc-id (keys services-by-id)] (swap! app-context assoc-in [:service-contexts svc-id] {})) (run-lifecycle-fns app-context s/init "init" ordered-services) (run-lifecycle-fns app-context s/start "start" ordered-services) + (notice-service-ready) (inc-restart-counter! this) this (catch Throwable t