Tetris destination

Tetris destination

Overview

In this blog post, I would like to show you a fun way of using Python destinations in syslog-ng. We will write Tetris destination. :)

Speaking of fun: we will write the Python destination in Hy (a Lisp dialect that translates into Python). The syntax is very simple: one can trivially rewrite the samples below in pure Python. Still, why would you want to write anything in pure Python when you can use Lisp? :) Python destinations in syslog-ng can interpret hy, if the loaders(hy) option is used. You can install hy via pip.

We will use the builtin Tetris implementation of Emacs. The syslog-ng Python destination will connect to an Emacs server. The log messages will be turned into Tetris commands inside Emacs. Using an stdin source, users can interactively feed syslog-ng with messages, that will control the Tetris in the end.

For further information on Python destination in syslog-ng, consult the documentation.

The Emacs destination

The first step is to write an Emacs destination (that is, the messages logged into an Emacs buffer).

Inserting messages into an Emacs buffer

How can we programmatically insert messages into an Emacs buffer without syslog-ng involved?

There is an insert function (which can insert a string at the current cursor position), we can use for this purpose. You can have multiple buffers open in Emacs, so we want to specify which buffer we want to use for the insertion. We can use with-current-buffer to achieve this. In the example below, we will insert hello world into a buffer called *scratch*.

(with-current-buffer "*scratch*" (insert "hello world\n"))

Inserting messages outside Emacs

In this case, Emacs should be started as a daemon: emacs --daemon. The Emacs daemon opens a Unix stream socket: /tmp/emacs1000/server on My Computer. We can use that to communicate with the daemon.

The question is: what is our communication protocol? Let's intercept communication between emacsclient and emacs daemon using socat.

Let's use emacsclient to send commands to the Emacs daemon.

emacsclient -s /tmp/testsocket --eval '(with-current-buffer "*scratch*" (insert "hello world"))'

The result is:

$ socat -v UNIX-LISTEN:/tmp/testsocket,reuseaddr,fork UNIX-CONNECT:/tmp/emacs1000/server
-dir /home/furiel/ -current-frame -eval (with-current-buffer&_"*scratch*"&_(insert&_"hello&_world"))
< 2019/03/01 10:29:36.341252  length=16 from=0 to=15
-emacs-pid 3985
< 2019/03/01 10:29:36.341707  length=11 from=16 to=26
-print nil

What we see here is the same command that is sent through the socket (the spaces are replaced with &_) and some extra parameters. There is a backward communication: the daemon sends back the pid of the daemon, and a return value.

Simulating this protocol, we can create our own Emacs destination in Python.

Put this snippet next to the syslog-ng binary (or set PYTHONPATH accordingly) into a mymodule.hy file.

(import socket)

(defclass EmacsDestination [object]
  (defn init [self options]
    (setv self.sock None)
    True)

  (defn send [self -msg]
    (setv self.sock (socket.socket socket.AF_UNIX socket.SOCK_STREAM))
    (.connect self.sock "/tmp/emacs1000/server")

    (setv msg (.replace (.decode (get -msg "MSG") "utf8") " " "&_"))
    ;; qoutes need to be escaped too

    (setv payload
          (.format
            "-dir /home/furiel/ -current-frame -eval (with-current-buffer&_\"*scratch*\"&_(insert&_\"{}\\n\"))\n"
            msg))
    (.send self.sock (.encode payload "utf8"))

    (print (.recv self.sock 1000))
    (print (.recv self.sock 1000))
    (.close self.sock)

    True))

Technically, a proper implementation should escape spaces inside the msg as well. However, this snippet is good enough for the Tetris destination. The correction is left as an optional homework to the reader. :)

The corresponding syslog-ng config example:

@version: 3.20

destination d_python {
  python(
    loaders(hy)
    class(mymodule.EmacsDestination)
  );
};

log {
  source { stdin(flags(no-parse)); };
  destination (d_python);
};

The Tetris destination

Now we know how to send commands to the Emacs daemon using Python destination. To have a Tetris destination, all we need to do now is to parse the incoming messages, and translate them into Tetris commands:

(import socket)

(defclass TetrisDestination [object]
  (defn init [self options]
    (setv self.sock None)
    True)

  (defn send [self -msg]
    (setv self.sock (socket.socket socket.AF_UNIX socket.SOCK_STREAM))
    (.connect self.sock "/tmp/emacs1000/server")

    (setv msg (.rstrip (.decode (get -msg "MSG")) "\n"))
    (print msg)
    (setv command
          (cond
            [ (= msg "o") "(tetris-start-game)" ]
            [ (= msg "a") "(tetris-move-left)" ]
            [ (= msg "d") "(tetris-move-right)" ]
            [ (= msg "w") "(tetris-rotate-prev)" ]
            [ (= msg "p") "(tetris-pause-game)"]
            [ (= msg "s") "(tetris-move-bottom)"]
            [ True (return True) ]))

    (print command)

    (setv payload (.format "-dir /home/furiel/ -current-frame -eval (with-current-buffer&_\"*Tetris*\"&_{})\n"
                           command))
    (.send self.sock (.encode payload "utf8"))

    (print (.recv self.sock 1000))
    (print (.recv self.sock 1000))
    (.close self.sock)

    True))

Config example:

@version: 3.20

destination d_python {
  python(
    loaders(hy)
    class(mymodule.TetrisDestination)
  );
};

log {
  source { stdin(flags(no-parse)); };
  destination (d_python);
};

Just start Tetris in Emacs using M-x tetris, then start feeding syslog-ng with data and keep playing.

Have fun! :)

Finally some screenshots:

$ ./syslog-ng -Fe -f ../etc/python-destination.conf --no-caps
[2019-03-01T10:37:32.129069] syslog-ng starting up; version='3.20.1'
a
a
(tetris-move-left)
b'-emacs-pid 3985\n'
b'-print nil\n'
a
a
(tetris-move-left)
b'-emacs-pid 3985\n'
b'-print nil\n'
s
s
(tetris-move-bottom)
b'-emacs-pid 3985\n'
b'-print [nil&_23672&_64868&_910140&_0.4&_tetris&-update&-game&n&_&_&_&_&_(#<buffer&_*Tetris*>)&n&_&_&_&_&_nil&_0]&n\n'
p
p
(tetris-pause-game)
b'-emacs-pid 3985\n'
b'-print "Game&_paused&_(press&_p&_to&_resume)"\n'

tetris.jpg

Figure 1: screenshot

Related Content