pythonからUNIX domain socketでopenMSXに接続する
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.cpp
にcheckSocket
関数を発見。引数のQFileInfo
からファイル名が"socket."で始まっているかどうかを確認している。
ということは、やはりsocketファイルはマニュアルにあったsocket.<pid>で正しい。
では、どこにこのファイルはあるのだろう?
checkSocket
関数に手を加えてabsoluteFilePath()
をデバッグ出力するようにして起動。
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を何に使っているのか?
調べてみるとアプリケーションのキャッシュ的に使っているらしい。
早速自分のマシンでも環境変数を確認してみる。
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ファイルを探すようにした。
最終的なコードはこれ。
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