Python で、別プロセス間での Singleton 実装 - Socket_Singleton を使う

# はじめに

Python でツールを作ることが多いが、Window を表示する機能を伴うツールを作った時、特に Pyinstaller などで exe 化したりして配布していたりすると、どうしても複数個立ち上げたくないという事が起こったりする。
こうした Python プロセスが分かれてしまった場合、通常の Singleton 実装は使えない。

lock ファイルを使うなどの方法があるが、調べていると Socket_Singleton というライブラリがあることに気が付いた。
これを使った実装方法をまとめる。

# インストール方法

```bash
pip install Socket_Singleton
```

# 基本的な使い方

基本的には、Socket_Singleton のインスタンスを作るだけで良い。

```python: app.py
from Socket_Singleton import Socket_Singleton

def main():
    ss = Socket_Singleton()
    print("Running!")
    input()

if __name__ == "__main__":
    main()
```

こちらを実行してみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/1fb4cda4-5ba0-f827-d7d8-4743937f20e2.png)


片方では、`Running!` と表示されるが、同時に別プロセスで実行すると即座に終了され、最初に実行しているプロセスのみが残る Singleton 実装が出来ている。

## しくみ

最初の Socket_Singleton の呼び出しで、ソケットを用意 (デフォルトでは、Address: 127.0.0.1 (localhost)、 Port: 1337)し、このソケットに対してアクセスしてすでに存在すると**プロセスを終了**させたり、**トレースさせたり**する。

# Singleton なプロセスを作ってトレースする

他のプロセスの呼び出しトレースをするためには、`Socket_Singleton.trace()` メソッドを使用する。

```python:single_process.py
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 していることが分かる。

![PsStTOqpGO.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/e48a3207-478c-b96d-4835-8a819b04baf3.png)



# Singleton な Window を出す exe を作ってみる

上記工程を使って、次のようなアイデアができる。

* 最初に Window を立ち上げるプロセスを起動する。
* 次に Window を立ち上げるプロセスの通信を受け取って Callback 関数を実行し、その際に Window が最小化していたり、隠れていたりすると、通常表示させたり、隠す状態から戻して表示させたり、アクティブ状態に持ってき行けるのでは。

しかしながら、ここで、Socket_Singleton の仕様にぶつかる。

* この Socket_Singleton は、二つ目以降のプロセスでは、最低でも一つの引数がないと Callback が呼び出されない。

これが何が問題かというと、次のことができなくなる。

* 最初の .exe ファイルを実行し、Window を立ち上げ、最小化する。
* 次に、また同じ .exe ファイルを実行し Window を立ち上げ、通常表示にさせようとする。

この場合何も起きないということになる。
なので、こちらの振る舞いをカスタムしてしまう。
（本来この仕様には変更を入れた方が良いと思うので、いずれ PR 出そうと思う…。）

## Socket_Singleton をカスタムする

とりあえず最初にサンプルコードの Gist をペタリ。

https://gist.github.com/takavfx/ff0284e6b21483b4b5982aa12ce29c5d

要点だけ以下に書いていく。

まず、カスタムコールバックの `_create_client` の挙動を上書きして、全引数を受けとるようにする。
実は基本はこれだけ。 

```python:simple_window.py
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 関数へは必ず引数を渡すようになるので、何もしなくても引数を設定しておく。

```python:simple_window.py
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 の挙動になる。

![WindowsTerminal_aaQ7I3BdLB.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/d3d2a102-ac82-3214-e360-ced2c0c4fa11.gif)

# 最後に

Python だと、プロセスをまたいだ Singleton の設定はめんどくさいが、とりあえず Socket_Singleton というライブラリとしてアイデアが得られたのは良かった。
先述の通り、微妙に癖があるので、カスタムして使うか、PR 出してしまう方がよさそうなのが今ってカンジ。
以上！！