11

I'm developing an app in python which sends commands to a running emacs instance. Currently, I start an emacs server and send the commands through the command line, like this:

subprocess.call(['emacsclient', '-e', '(with-current-buffer ' +
                 '(window-buffer (selected-window)) (save-buffer))'])

Is there another way to control to emacs externally, like an API? Because this solution I came up with doesn't seem very elegant to me.

8
  • 2
    Asking for the "best" way to do something is generally either too broad or primarily opinion-based. Consider asking for another way to do it from the way you're using, and state what you think is wrong, inefficient, or inadequate with the way you're currently doing it.
    – Drew
    Commented Feb 23, 2018 at 16:44
  • 2
    If you are concerned with the overhead of having to create a new process every time you call emacsclient, maybe creating a simple TCP socket in on Emacs side which will simply evaluate the code you send it will be more efficient.
    – wvxvw
    Commented Feb 24, 2018 at 8:41
  • Please explain if your question is about Emacs per se, or about Python. If there's a running Emacs instance, it's not clear that there's much to say about how to access it that isn't about the calling program, which is Python-based in this case.
    – Dan
    Commented Feb 24, 2018 at 16:46
  • Thanks for the warning. I edited the question and removed these words.
    – Jesse
    Commented Feb 26, 2018 at 2:39
  • 1
    @nealmcb I posted an answer with an example. Check if it works for you
    – Jesse
    Commented Feb 7, 2019 at 11:55

1 Answer 1

6

I used the TCP approach suggested by @wvxvw in the comments. I'm starting a TCP server inside emacs, which, when receiving a package, will eval it as elisp code. I found a piece of code for the TCP server somewhere in the internet (I can't seem to find it again, if anybody knows please leave a comment and I'll add to the answer), and made a few changes for the code evaluation:

;;; echo-server.el --- -*- lexical-binding: t -*-
;;
;; Copyright (C) 2016-2017 York Zhao <[email protected]>

;; Author: York Zhao <[email protected]>
;; Created: June 1, 2016
;; Version: 0.1
;; Keywords: TCP, Server, Network, Socket
;;
;; This file is NOT part of GNU Emacs.
;;
;; This program is free software; you can redistribute it and/or modify it under
;; the terms of the GNU General Public License as published by the Free Software
;; Foundation; either version 3 of the License, or (at your option) any later
;; version.
;;
;; This program is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
;; FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
;; details.
;;
;; You should have received a copy of the GNU General Public License along with
;; this program. If not, see <http://www.gnu.org/licenses/>.
;;
;;; Commentary:
;;
;; Running "M-x tcp-server-start" will prompt user to enter a port number to
;; listen to.
;;
;;; Code:

(require 'cl-lib)

(defvar tcp-server-clients '()
  "Alist where KEY is a client process and VALUE is the string")

(defvar tcp-server-servers '()
  "Alist where KEY is the port number the server is listening at")

(defvar tcp-server-display-buffer-on-update nil
  "If non-nil, force the process buffer to be visible whenever
new text arrives")
(make-variable-buffer-local 'tcp-server-display-buffer-on-update)

(defun tcp-server-make-process-name (port)
  "Return server name of the process listening on PORT"
  (format "tcp-server:%d" port))

(defun tcp-server-get-process (port)
  "Return the server process that is listening on PORT"
  (get-process (tcp-server-make-process-name port)))

(defun tcp-server-process-buffer (port)
  "Return buffer of the server process that is listening on PORT"
  (process-contact (tcp-server-get-process port) :buffer))

(defun tcp-server-delete-clients (server-proc)
  (let ((server-proc-name (process-contact server-proc :name)))
    (cl-loop for client in tcp-server-clients
             if (string= server-proc-name (process-contact client :name))
             do
             (delete-process client)
             (message "Deleted client process %s" client))
    (setq tcp-server-clients
          (cl-delete-if (lambda (client)
                          (string= (process-contact server-proc :name)
                                   (process-contact client :name)))
                        tcp-server-clients))))

(cl-defun tcp-server-start (port &optional (display-buffer-on-update nil)
                                 (buffer-major-mode 'text-mode))
  "Start a TCP server listening at PORT"
  (interactive
   (list (read-number "Enter the port number to listen to: " 9999)))
  (let* ((proc-name (tcp-server-make-process-name port))
         (buffer-name (format "*%s*" proc-name)))
    (unless (process-status proc-name)
      (make-network-process :name proc-name :buffer buffer-name
                            :family 'ipv4 :service port
                            :sentinel 'tcp-server-sentinel
                            :filter 'tcp-server-filter :server 't)
      (with-current-buffer buffer-name
        (funcall buffer-major-mode)
        (setq tcp-server-display-buffer-on-update display-buffer-on-update))
      (setq tcp-server-clients '()))
    ;; (display-buffer buffer-name)
    ))

(defun tcp-server-stop (port)
  "Stop an emacs TCP server at PORT"
  (interactive
   (list (read-number "Enter the port number the server is listening to: "
                      9999)))
  (let ((server-proc (tcp-server-get-process port)))
    (tcp-server-delete-clients server-proc)
    (delete-process server-proc)))

(defun tcp-server-append-to-proc-buffer (proc string)
  (let ((buffer (process-contact proc :buffer))
        (inhibit-read-only t))
    (and buffer (get-buffer buffer)
         (with-current-buffer buffer
           (when tcp-server-display-buffer-on-update
             (display-buffer buffer))
           (let ((moving (= (point) (point-max))))
             (save-excursion
               (goto-char (point-max))
               (insert string)
               )
             (if moving (goto-char (point-max))))))))

(defun tcp-server-filter (proc string)
  (tcp-eval string))

(defun tcp-eval (string)
  (eval (car (read-from-string (format "(progn %s)" string)))))

(defun tcp-server-sentinel (proc msg)
  (cond
   ((string-match "open from .*\n" msg)
    (push proc tcp-server-clients)
    (tcp-server-log proc "client connected\n")
    )
   ((string= msg "connection broken by remote peer\n")
    (setq tcp-server-clients (cl-delete proc tcp-server-clients))
    (tcp-server-log proc "client has quit\n")
    )
   ((eq (process-status proc) 'closed)
    (tcp-server-delete-clients proc))))

(defun tcp-server-log (client string)
  "If a server buffer exists, write STRING to it for logging purposes."
  (tcp-server-append-to-proc-buffer client
                                    (format "%s %s: %s"
                                            (current-time-string)
                                            client string)))


(provide 'tcp-server)

;;; tcp-server.el ends here

Then you can call the tcp-server-start command, informing the port you want it to run, and your server should be ready. Here's a python client example:

import socket

def connect():
    conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    conn.connect(('127.0.0.1', 9999)) # ip and port running emacs
    return conn

if __name__ == '__main__':
    conn = connect()

    # now you send your desired elisps commands like this:
    conn.send(b'(next-line)')
    conn.send(b'(insert "foo bar")')

Keep in mind that this code doesn't have any type of firewall, so you should be vulnerable to external attacks.

2
  • Thank you. Looks useful to me, though lack of authentication is indeed something to be very wary of! You can add that the command tcp-server-stop should be used when you're done with the server.
    – nealmcb
    Commented Feb 17, 2019 at 0:41
  • hi @Jesse perhaps via unix socket, it could be easier for security concerns ?
    – taharqa
    Commented Jun 9, 2020 at 8:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.