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.

Jesse
  • 1,984
  • 11
  • 19
  • 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 Feb 23 '18 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 Feb 24 '18 at 08: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 Feb 24 '18 at 16:46
  • Thanks for the warning. I edited the question and removed these words. – Jesse Feb 26 '18 at 02:39
  • I asked this because I was wondering if there was another way to talk to emacs externally. I'm not familiar with TCP sockets, but that might do it. I'll try it. Thanks! – Jesse Feb 26 '18 at 02:43
  • @wvxvw I followed your suggestion and now I'm using the TCP approach along with `read-from-string` function on emacs side. If you write an answer, I'll accept it. Thanks again. – Jesse Mar 05 '18 at 02:43
  • Thanks, @wvxvw. Congrats, Jesse. TCP and `read-from-string` sound great! But can one of you give some emacs and Python code (best in an answer here) to explain this via an example? See also https://stackoverflow.com/questions/6162967/simple-tcp-client-examples-in-emacs-elisp – nealmcb Feb 03 '19 at 18:46
  • 1
    @nealmcb I posted an answer with an example. Check if it works for you – Jesse Feb 07 '19 at 11:55

1 Answers1

5

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 <gtdplatform@gmail.com>

;; Author: York Zhao <gtdplatform@gmail.com>
;; 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.

Jesse
  • 1,984
  • 11
  • 19
  • 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 Feb 17 '19 at 00:41
  • hi @Jesse perhaps via unix socket, it could be easier for security concerns ? – taharqa Jun 09 '20 at 08:07