1

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:

  1. top shows that the server-eval-at emacs (client) is consuming 100% CPU.
  2. strace shows that the server-eval-at emacs (client) is calling read on stdin (fd 0) repeatedly, and receiving an empty string each time
  3. strace shows that the eval emacs calls read on stdin twice: once to read the command, and once receiving an empty string before exiting.
  4. gdb on the server-eval-at emacs (client) has a Lisp Backtrace with read-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.

Chris Hunt
  • 113
  • 4
  • Not sure what "but the process does not exit!" means. It seems work as expected here with Emacs 27.1 on macOS. Your code won't quit properly as read-string always returns a string (non-nil), and I don't see why you wrap it with ignore-errors, the function is not expected to fail. Emacs quits while it encounters an error, such as "End of file during parsing:" from read when you does not enter a valid sexp. – xuchunyang Nov 11 '20 at 02:30
  • read-string signals an error at the end of input, this is easy to verify. ignore-errors ignores that error and returns nil 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 using server-eval-at. If what you are saying is true, then not using server-eval-at actually causes emacs to behave incorrectly, but I don't think that's the case. – Chris Hunt Nov 11 '20 at 02:43
  • 1
    You're correct about read-string. I didn't realize I can use C-d, normally user can quit the minibuffer via C-g which not work in batch mode. I didn't test your exact code, I simply test server-eval-at. Your program does not kill the first Emacs process, it remains running after the program ends. – xuchunyang Nov 11 '20 at 02:59

0 Answers0