0
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-07-08

はじめに

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

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

インストール方法

pip install Socket_Singleton

基本的な使い方

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

app.py
from Socket_Singleton import Socket_Singleton

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

if __name__ == "__main__":
    main()

こちらを実行してみる。

image.png

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

しくみ

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

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

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

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

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

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

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

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

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

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

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

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

Socket_Singleton をカスタムする

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

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

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

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

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

最後に

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1