はじめに
Python でツールを作ることが多いが、Window を表示する機能を伴うツールを作った時、特に Pyinstaller などで exe 化したりして配布していたりすると、どうしても複数個立ち上げたくないという事が起こったりする。
こうした Python プロセスが分かれてしまった場合、通常の Singleton 実装は使えない。
lock ファイルを使うなどの方法があるが、調べていると Socket_Singleton というライブラリがあることに気が付いた。
これを使った実装方法をまとめる。
インストール方法
pip install Socket_Singleton
基本的な使い方
基本的には、Socket_Singleton のインスタンスを作るだけで良い。
from Socket_Singleton import Socket_Singleton
def main():
ss = Socket_Singleton()
print("Running!")
input()
if __name__ == "__main__":
main()
こちらを実行してみる。
片方では、Running!
と表示されるが、同時に別プロセスで実行すると即座に終了され、最初に実行しているプロセスのみが残る Singleton 実装が出来ている。
しくみ
最初の Socket_Singleton の呼び出しで、ソケットを用意 (デフォルトでは、Address: 127.0.0.1 (localhost)、 Port: 1337)し、このソケットに対してアクセスしてすでに存在するとプロセスを終了させたり、トレースさせたりする。
Singleton なプロセスを作ってトレースする
他のプロセスの呼び出しトレースをするためには、Socket_Singleton.trace()
メソッドを使用する。
from Socket_Singleton import Socket_Singleton
def callback(argument):
print(argument)
def main():
ss = Socket_Singleton()
ss.trace(callback)
input()
if __name__ == "__main__":
main()
これを実行し、別のプロセスで引数付きで実行してみる。
すると、次の用に引数の値を受け取り print していることが分かる。
Singleton な Window を出す exe を作ってみる
上記工程を使って、次のようなアイデアができる。
- 最初に Window を立ち上げるプロセスを起動する。
- 次に Window を立ち上げるプロセスの通信を受け取って Callback 関数を実行し、その際に Window が最小化していたり、隠れていたりすると、通常表示させたり、隠す状態から戻して表示させたり、アクティブ状態に持ってき行けるのでは。
しかしながら、ここで、Socket_Singleton の仕様にぶつかる。
- この Socket_Singleton は、二つ目以降のプロセスでは、最低でも一つの引数がないと Callback が呼び出されない。
これが何が問題かというと、次のことができなくなる。
- 最初の .exe ファイルを実行し、Window を立ち上げ、最小化する。
- 次に、また同じ .exe ファイルを実行し Window を立ち上げ、通常表示にさせようとする。
この場合何も起きないということになる。
なので、こちらの振る舞いをカスタムしてしまう。
(本来この仕様には変更を入れた方が良いと思うので、いずれ PR 出そうと思う…。)
Socket_Singleton をカスタムする
とりあえず最初にサンプルコードの Gist をペタリ。
要点だけ以下に書いていく。
まず、カスタムコールバックの _create_client
の挙動を上書きして、全引数を受けとるようにする。
実は基本はこれだけ。
class Custom_Socket_Singleton(Socket_Singleton):
def __init__(self, address: str = "127.0.0.1", port: int = 1337, timeout: int = 0, client: bool = True, strict: bool = True, max_clients: int = 0):
super().__init__(address, port, timeout, client, strict, max_clients)
self.address = address
def _create_client(self):
with self._sock as s:
s.connect((self.address, self.port))
for arg in sys.argv:
s.send(arg.encode())
s.send("\n".encode())
あとは、Window 側に Callback を呼び出す仕組みを作る。
ちなみに、GUI ライブラリはいつも使っている Qt for Python (PySide)。
ここでの注意点は、Callback 関数へは必ず引数を渡すようになるので、何もしなくても引数を設定しておく。
class MainWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
ss = Custom_Socket_Singleton(address="localhost")
ss.trace(self.callback)
# 省略
def callback(self, arg):
self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
self.show()
self.activateWindow()
これで次の GIF の挙動になる。
最後に
Python だと、プロセスをまたいだ Singleton の設定はめんどくさいが、とりあえず Socket_Singleton というライブラリとしてアイデアが得られたのは良かった。
先述の通り、微妙に癖があるので、カスタムして使うか、PR 出してしまう方がよさそうなのが今ってカンジ。
以上!!