| git.druid.rocks | index | druid520 | radium | radium-ipc.el |
radium-ipc.el
;;; radium-ipc.el, a named-pipe command channel at ~/.radium.ipc -*- lexical-binding: t; -*-
;;
;; item 12: "essentially an elisp/keybind/etc evaluator" - a fifo, not
;; a second emacsclient. emacsclient --eval already IS a synchronous,
;; return-value-having ipc channel into the daemon (rd eval, below,
;; is nothing but a thin wrapper around exactly that) - what it can't
;; do is sit as the far end of a script/window-manager keybind/cron
;; job that just wants to fire a command at a running daemon without
;; caring about the reply. that's what this is: write one elisp form
;; (a plain eval, or a keybind trigger like `(call-interactively
;; 'save-buffer)' or `(execute-kbd-macro (kbd "C-x C-s"))' - both are
;; just eval too, no separate mini-language needed) to the fifo,
;; radium-ipc reads and evals it, the result goes to *Messages* (where
;; it's genuinely useful if you're attached to the daemon) rather than
;; back down the pipe - fire-and-forget, by design, not a limitation
;; to fix later.
;;
;; a fifo's own real gotcha: a reader sees EOF the moment the writer
;; count drops to zero, which is EVERY single write from a plain shell
;; redirect (`echo ... > fifo` opens, writes, closes) - so the
;; process reading it exits after each command, not just once at
;; shutdown. radium-ipc--sentinel respawns the reader immediately
;; every time, so the channel stays open for the next command without
;; needing a second, always-open writer fd to work around it.
;;
;; the fifo is created mode 600 (owner-only) and lives at
;; radium-ipc-path, ~/.radium.ipc by default: the same trust boundary
;; as .emacs.d itself already has - anyone who can write to it can
;; already run as this unix user, so evaling whatever it sends isn't
;; a new privilege boundary to defend, and a per-call approval prompt
;; (gptel's own tools all have one, see radium-gptel.el) would defeat
;; the entire point of a fire-and-forget scripting channel.
(defvar radium-ipc-path
(or (getenv "RADIUM_IPC") (expand-file-name ".radium.ipc" "~"))
"the fifo radium-ipc reads elisp forms from, one per line. reads
$RADIUM_IPC if set - the same env var bin/rd.sh itself already falls
back to $HOME/.radium.ipc from, so the two halves of one feature
share a single config point instead of two independently-hardcoded
defaults that could drift. override from ~/.radium.el (loaded dead
last) instead, then call `radium-ipc-start' to make it take.")
(defvar radium-ipc--proc nil
"the current `cat' reader process, or nil/dead between writes.")
(defvar radium-ipc--buf ""
"bytes read since the last complete line.")
(defun radium-ipc--eval-line (line)
"read LINE as one elisp form and eval it, logging the result (or
the error) to *Messages* - never signals out to the reader process."
(setq line (string-trim line))
(unless (string-empty-p line)
(condition-case err
(message "radium-ipc: %s => %S" line (eval (car (read-from-string line)) t))
(error (message "radium-ipc: %s -- err: %s" line (error-message-string err))))))
(defun radium-ipc--filter (_proc chunk)
(setq radium-ipc--buf (concat radium-ipc--buf chunk))
(while (string-match "\n" radium-ipc--buf)
(radium-ipc--eval-line (substring radium-ipc--buf 0 (match-beginning 0)))
(setq radium-ipc--buf (substring radium-ipc--buf (match-end 0)))))
(defun radium-ipc--sentinel (_proc _event)
"respawn the reader - unless radium-ipc-path itself is gone, in
which case that respawn would immediately fail and re-trigger this
same sentinel again, forever: confirmed by testing exactly that
\(an isolated cat-on-a-missing-fifo repro hit ~4400 respawns in 3s,
pegging a cpu core) before this guard existed. `radium-ipc-start' is
the documented, explicit way back once the fifo is actually there
again."
(if (file-exists-p radium-ipc-path)
(radium-ipc--spawn)
(message "radium-ipc: %s is gone, not respawning - M-x radium-ipc-start to recreate it."
radium-ipc-path)))
(defun radium-ipc--spawn ()
(setq radium-ipc--proc
(make-process :name "radium-ipc"
:command (list "cat" radium-ipc-path)
:filter #'radium-ipc--filter
:sentinel #'radium-ipc--sentinel
:noquery t)))
(defun radium-ipc-start ()
"create radium-ipc-path (mode 600) if it doesn't exist yet and
start (or restart) the reader listening on it. called once at startup
below; also a real interactive command for after a manual `rm' of
the fifo, or after changing radium-ipc-path (from ~/.radium.el, say)
to move to a different one - always stops whatever's currently
listening first, rather than a stale `process-live-p' check silently
no-op'ing and leaving the reader on the old path."
(interactive)
(when (process-live-p radium-ipc--proc)
(set-process-sentinel radium-ipc--proc #'ignore)
(delete-process radium-ipc--proc))
(unless (file-exists-p radium-ipc-path)
(call-process "mkfifo" nil nil nil radium-ipc-path)
(set-file-modes radium-ipc-path #o600))
(radium-ipc--spawn))
(radium-ipc-start)
(provide 'radium-ipc)