I am trying to create a script that, when run with emacs, will read commands from stdin a line at a time, execute them in another emacs process (with server-eval-at
) and print the result. The process should exit when no more input is available. I have come up with a basic protocol and everything seems to work when using eval
locally, but after introducing server-eval-at
the emacs process hangs instead of exiting at the end of input.
For initial development I was doing the evaluation inline, in the same process that was reading the input. That seems to work well. That script looks like
(while (setq command (ignore-errors (read-string "")))
(setq command (read command))
(message "[DEBUG] Received command %s" command)
(let ((result (eval command)))
(message "[DEBUG] Response received %s" result)))
In a shell script, for easier testing:
#!/bin/sh
cd "$(mktemp -d)"
cat <<EOF > script.el
(while (setq command (ignore-errors (read-string "")))
(setq command (read command))
(message "[DEBUG] Received command %s" command)
(let ((result (eval command)))
(message "[DEBUG] Response received %s" result)))
EOF
echo '(message "hello world")' | \
emacs --quick --batch --script "$PWD/script.el"
An example of running this, with output:
$ ./t2
[DEBUG] Received command (message hello world)
hello world
[DEBUG] Response received hello world
$
As you can see, the debug messages are printed. The process exits at the end of input. All of this is expected and OK.
Now I want to run the incoming commands on the server, so I use server-eval-at
. Here is that script
(require 'server)
(while (setq command (ignore-errors (read-string "")))
(setq command (read command))
(message "[DEBUG] Received command %s" command)
(let ((result (server-eval-at (getenv "RPC_SERVER_NAME") command)))
(message "[DEBUG] Response received %s" result)))
Another shell script, for easier testing
#!/bin/sh
cd "$(mktemp -d)"
export RPC_SERVER_NAME="$PWD/emacs"
emacs --quick "--fg-daemon=$RPC_SERVER_NAME" >stdout.log 2>stderr.log &
sleep 1
cat <<EOF > script.el
(require 'server)
(while (setq command (ignore-errors (read-string "")))
(setq command (read command))
(message "[DEBUG] Received command %s" command)
(let ((result (server-eval-at (getenv "RPC_SERVER_NAME") command)))
(message "[DEBUG] Response received %s" result)))
EOF
echo '(message "hello world")' | \
emacs --quick --batch --script "$PWD/script.el"
An example of running this, with output:
$ ./t1
[DEBUG] Received command (message hello world)
[DEBUG] Response received hello world
The output looks fine, but the process does not exit!
I have introspected these processes a few ways:
- top shows that the
server-eval-at
emacs (client) is consuming 100% CPU. - strace shows that the
server-eval-at
emacs (client) is callingread
on stdin (fd 0) repeatedly, and receiving an empty string each time - strace shows that the
eval
emacs callsread
on stdin twice: once to read the command, and once receiving an empty string before exiting. - gdb on the
server-eval-at
emacs (client) has a Lisp Backtrace withread-string
at the top.
I inlined server-eval-at
and was able to identify that calling accept-process-output
is necessary for the subsequent call to read-string
(in the next loop iteration) to behave differently, but it is hard for me to
dig much deeper than that.
I tried these same steps against emacs 26.3 and observed the same result.
I am using emacs 27.1 on Ubuntu Linux 18.04, as installed with evm.
read-string
always returns a string (non-nil), and I don't see why you wrap it withignore-errors
, the function is not expected to fail. Emacs quits while it encounters an error, such as "End of file during parsing:" fromread
when you does not enter a valid sexp.read-string
signals an error at the end of input, this is easy to verify.ignore-errors
ignores that error and returnsnil
which will cleanly break out of the loop. After the loop emacs will exit because it is the behavior when running with--script
/--batch
. As I mentioned above, all of this behaves as I expect when not usingserver-eval-at
. If what you are saying is true, then not usingserver-eval-at
actually causes emacs to behave incorrectly, but I don't think that's the case.read-string
. I didn't realize I can use C-d, normally user can quit the minibuffer viaC-g
which not work in batch mode. I didn't test your exact code, I simply testserver-eval-at
. Your program does not kill the first Emacs process, it remains running after the program ends.