📘

pythonからUNIX domain socketでopenMSXに接続する

2021/04/14に公開

openMSXは外部からsocket接続してコマンドを送ることができるので、pythonを使って接続してみた。

環境

  • MacBook Air (13inch, Mid 2012)
  • macOS 10.15.7
  • python 3.9.4

事の顛末

pythonからUNIX domain socketでopenMSXに接続してみようとした時の試行錯誤のメモ。

紆余曲折の部分が必要ない場合は「最終的なpythonのコード」を参照。

openMSXのマニュアル

openMSXのマニュアルにあるControlling openMSX from Externam Applicationにはこうある。

There are multiple ways to connect to openMSX. The first (and oldest) way is using a pipe. On Windows you can use a named pipe, on other systems you use stdio. To enable this, start openMSX like this:

openmsx -control stdio
or for Windows:

openmsx -control pipe
The second method is using a socket. Connecting on non-Windows systems goes with a UNIX domain socket. openMSX puts the socket in /tmp/openmsx-<username>/socket.<pid>. The /tmp/ dir can be overridden by environment variables TMPDIR, TMP or TEMP (in that order).

接続方法は2つあって1つはパイプ、もう一つはUNIX domain socket。

とりあえずパイプでの接続はターミナルから試してみると簡単に繋がった。

paraches@uriel-2 MacOS % ./openmsx -control stdio
<openmsx-output>
<command>set renderer SDL</command>
<reply result="ok">SDL</reply>
<command>exit</command>
<reply result="ok"></reply>
</openmsx-output>
paraches@uriel-2 MacOS % 

もう一つのUNIX domain socketでの接続、socketファイルは下記のようになるらしい。

/tmp/openmsx-<username>/socket.<pid>

これを使ってpythonからsocket接続を行うことになる。

socketファイルの確認

openMSXを起動してsocketファイルを確認してみる。

paraches@uriel-2 /tmp % ls -l
total 0
drwx------  4 root      wheel  128  2 11 12:55 PKInstallSandbox.dLsXw9
drwx------  3 paraches  wheel   96  4 13 22:29 com.apple.launchd.TQ8qbaX0ky
drwx------  3 paraches  wheel   96  4 13 22:29 com.apple.launchd.c930VoZ6Cr
drwxr-xr-x  5 paraches  wheel  160  4 13 23:47 openmsx.921
drwxr-xr-x  2 root      wheel   64  4 13 22:29 powerlog
paraches@uriel-2 /tmp % ls

が、/tmpフォルダにopenmsx-<username>というフォルダはないし、それらしいopenmsx.<pid>フォルダの中には訳のわからない3つのファイルがあるだけ。socket.<pid>というファイルはどこにもない。

一応わけのわからない3つのファイルをsocketファイルだとして接続を試みてみることにする。

pythonのコード

python unix domain socketで検索するとたくさん出てくるが、今回はクライアントのコードが必要。色々と確認してこんなコードを用意した。

import socket

def connect(path):
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(path)
    data = s.recv(1024)
    print('recieve: %s' % data.decode())
    s.close()

def main():
    connect('/tmp/openmsx.921/Bvq4ew')

if __name__ == '__main__':
    main()

socketファイルのパスは前出の3つのファイルそれぞれで試してみたが、結果はsocketファイルではないというエラーになるだけ。

paraches@uriel-2 openMSX_socket % python psocket.py
<中略>
socket.error: [Errno 38] Socket operation on non-socket
paraches@uriel-2 openMSX_socket % 

さて、困った。

openMSX Debugger

ここでopenMSXとsocket接続しているであろうアプリケーションopenMSX Debuggerの存在を思い出す。これのコードを読めばsocketファイルの場所がわかるのでは?

というわけで、前回の記事「macOSでopenMSX Debuggerをビルドする」という紆余曲折を経てopenMSX DebuggerがopenMSXと接続できることを確認。コードを読むことに。

ConnectDialog.cpp

openMSX DebuggerのソースのConnectDialog.cppcheckSocket関数を発見。引数のQFileInfoからファイル名が"socket."で始まっているかどうかを確認している。

ということは、やはりsocketファイルはマニュアルにあったsocket.<pid>で正しい。
では、どこにこのファイルはあるのだろう?

checkSocket関数に手を加えてabsoluteFilePath()をデバッグ出力するようにして起動。

ConnectDialog.cpp
qDebug() << info.absoluteFilePath();

openMSX Debuggerのconnectダイアログからrescanをかけるとsocketファイルの場所が表示された。

paraches@uriel-2 MacOS % ./openmsx-debugger
"/var/folders/6v/cq3c0g0j4214dbg338snk5pr0000gn/T/openmsx-paraches/socket.2271"

マニュルにある通りにopenmsx-<username>フォルダの中にsocket.<pid>ファイルがある。

ところで/var/foldersって何?

/var/folders

/varは一時的なファイルの置き場だと認識しているけど、macOSはfoldersを何に使っているのか?
調べてみるとアプリケーションのキャッシュ的に使っているらしい。
https://ascii.jp/elem/000/001/123/1123733/2/

早速自分のマシンでも環境変数を確認してみる。

paraches@uriel-2 MacOS % echo $TMPDIR
/var/folders/6v/cq3c0g0j4214dbg338snk5pr0000gn/T/
paraches@uriel-2 MacOS % 

ちゃんとopenmsx-<username>フォルダがあるパスが表示される。

と、ここまで来てようやく気づいた。
最初のマニュアルに書いてあった…。

The /tmp/ dir can be overridden by environment variables TMPDIR, TMP or TEMP (in that order).

最終的なpythonのコード

socketファイルは$TMPDIRの下にあることがわかったのでpythonのコードを変更。os.getenv('TMPDIR')してsocketファイルを探すようにした。

最終的なコードはこれ。

psocket.py
import os
import sys
import socket
import getpass
from pathlib import Path


class SocketClient:
    def __init__(self, path):
        self.socket_path = path
        self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)

    def connect(self):
        self.socket.connect(self.socket_path)
        self.receive_data()

    def disconnect(self):
        self.socket.close()

    def send_message(self, message):
        self.socket.send(message.encode())
        self.receive_data()

    def receive_data(self):
        data = self.socket.recv(1024)
        print('receive: %s' % data.decode())


def find_socket_files():
    tmp_dir = Path(os.getenv('TMPDIR'))
    socket_dir = tmp_dir / ('openmsx-%s' % getpass.getuser())
    socket_files = socket_dir.glob('socket.*')
    return [str(s_file) for s_file in socket_files]

def main():
    socket_files = find_socket_files()
    if len(socket_files) > 0:
        client = SocketClient(socket_files[0])
        client.connect()
        client.send_message("<command>exit</command>")
        client.disconnect()

if __name__ == '__main__':
    main()

これでコマンドを送ってみると…接続成功!

paraches@uriel-2 ~ % python psocket.py
receive from server: <openmsx-output>

receive from server: <reply result="ok"></reply>

paraches@uriel-2 ~ % 

ちゃんとopenMSXは終了した。

教訓

今回の教訓はコレ。

よし、/tmpにsocketファイルがあるのね」と早合点して行動しておかしなことになるパターン。その先にそうでない場合もあることが書いてあるのに…。

前回と今回でpythonからopenMSXをコントロールできるようにはなったのだが、前回のopenMSX Debuggerを使えばやりたい事は全てできそうなので、わざわざ自分でpythonからコントロールする必要はなくなった。

Discussion