あなた:
PythonでKeyboardInterruptの例外処理しようとしても、どの例外もつかめずにエラーになります。コンソール上には、KeyboardInterruptの表示が3つほど出ており、二つはマルチプロセッシングの分だと思うのですが、もう一つのこのコード自体がのKeyboardInterruptがつかめないのがよくわかりません。
py
# main server scirpt
# serve dataserver function with websocket. 

import asyncio
import multiprocessing
# from modules.reqestParser import parser
from websockets.asyncio.server import serve
from helper import loadSettings
from helper.netwrok import receiveLoop 
import tasks
import controller
import sys

websocketRef = None

async def mainLoop(websocket):
    websocketRef = websocket
    await receiveLoop(websocket,controller.controller)    

async def dataserver():
    async with serve(mainLoop, loadSettings.settings["ip"], loadSettings.settings["port"]) as server:
        await server.serve_forever()

def dataserverThread():
    asyncio.run(dataserver())


def serviceThread():
    asyncio.run(tasks.taskController(loadSettings.settings["taskInterval"]))

try:
    if __name__ == "__main__":
            print("This dataserver is started.")
            multiprocessing.Process(target=dataserverThread).start()
            multiprocessing.Process(target=serviceThread).start()
except KeyboardInterrupt:
    print("This DataServer will be stopped.")
    # if(websocketRef != None):
    #     websocketRef.close()
    # else:
    #     print("The websocket connection could be not closed correctly.")
    sys.exit()
ChatGPT:

原因はコードの構造と KeyboardInterrupt の伝播の仕組みにあります。今の書き方だと、KeyboardInterrupttry ブロックで捕まえられないのは当然です。順を追って説明します。


問題点 1:multiprocessing.Process は親プロセスとは別プロセス

python
multiprocessing.Process(target=dataserverThread).start()
  • この行で新しいプロセスを作っています。

  • 子プロセス内で asyncio.run() が走ります。

  • Ctrl+C を押すと、親プロセスと複数の子プロセスに KeyboardInterrupt が送られる場合がありますが、try/except があるのは親プロセスだけ

  • 子プロセス内で例外が発生しても親プロセスの except KeyboardInterrupt: には届きません。

  • そのため「KeyboardInterruptがつかめない」ように見えます。


問題点 2:try ブロックが if __name__ == "__main__": の外にある

python
try: if __name__ == "__main__": multiprocessing.Process(...).start()
  • try ブロックは親プロセス内でしか効かない。

  • KeyboardInterrupt は、プロセス内で同期的に待機しているコード (time.sleepinputasyncio.run) に届きます。

  • 親プロセスはすぐに Process.start() を実行したら終了してしまうので、待機していません。

  • つまり try ブロックの中では何も待っていないので、KeyboardInterrupt が入っても捕まえられない


問題点 3:子プロセスの asyncio.run では KeyboardInterrupt は asyncio 内で処理される

  • asyncio.run()KeyboardInterrupt をキャッチすると CancelledError に変換する場合があります。

  • そのため、asyncio.run 内で KeyboardInterrupt を捕まえるには、子プロセス内で try/except を使う必要があります。


正しい捕まえ方の例

子プロセス内でも KeyboardInterrupt を捕まえる構造にするのが安全です。

python
def dataserverThread(): try: asyncio.run(dataserver()) except KeyboardInterrupt: print("Dataserver thread stopped by KeyboardInterrupt") def serviceThread(): try: asyncio.run(tasks.taskController(loadSettings.settings["taskInterval"])) except KeyboardInterrupt: print("Service thread stopped by KeyboardInterrupt") if __name__ == "__main__": print("This dataserver is started.") dataserver_process = multiprocessing.Process(target=dataserverThread) service_process = multiprocessing.Process(target=serviceThread) dataserver_process.start() service_process.start() try: dataserver_process.join() service_process.join() except KeyboardInterrupt: print("Stopping all processes...") dataserver_process.terminate() service_process.terminate() dataserver_process.join() service_process.join() print("All processes stopped.") sys.exit()

ポイント:

  1. 子プロセス内でも try/except を使う

  2. 親プロセスは join() で待機し、KeyboardInterrupt が来たら terminate()

  3. asyncio.run() は子プロセス内で正しくキャンセルされる


まとめると

  • あなたのコードで「KeyboardInterrupt がつかめない」のは、try ブロックが親プロセスでしか効かないことと、子プロセス内で async 関数が実行されているため。

  • 3つ出ている KeyboardInterrupt は:

    1. 親プロセス

    2. dataserverThread プロセス

    3. serviceThread プロセス
      の3つです。

  • 親だけで try/except しても、子プロセスは捕まえられません。


必要なら、子プロセスを安全に終了させる方法もあります。

あなたのコードに近い形で、KeyboardInterrupt を親子両方で正しく処理できる例を作ることもできます。

あなた:

def dataserverThread(): try: asyncio.run(dataserver()) except KeyboardInterrupt: print("Dataserver thread stopped by KeyboardInterrupt") def serviceThread(): try: asyncio.run(tasks.taskController(loadSettings.settings\["taskInterval"\])) except KeyboardInterrupt: print("Service thread stopped by KeyboardInterrupt") if \_\_name\_\_ == "\_\_main\_\_": print("This dataserver is started.") dataserver\_process = multiprocessing.Process(target=dataserverThread) service\_process = multiprocessing.Process(target=serviceThread) dataserver\_process.start() service\_process.start() try: dataserver\_process.join() service\_process.join() except KeyboardInterrupt: print("Stopping all processes...") dataserver\_process.terminate() service\_process.terminate() dataserver\_process.join() service\_process.join() print("All processes stopped.") sys.exit()

私は子プロセスについては、現状ではなにも例外処理していないことは承知の上で、親プロセスについて、例外がつかめないことについて考えています。あなたの出力で行けば、マルチプロセッシングをスタートはしているが、joinしていないために、親プロセスが、KeyboardInterruptをそもそもつかめる場所が私の今ここで出したコードではないということに見えますがどうですか?
ChatGPT:

おっしゃる通りです。親プロセスの try ブロックで KeyboardInterrupt を捕まえられないのは、親プロセス自身がすぐに Process.start() を実行して終了してしまう構造になっていることが原因です。

具体的にいうと、元のコードでは:

python
try: if __name__ == "__main__": multiprocessing.Process(target=dataserverThread).start() multiprocessing.Process(target=serviceThread).start()
  • Process.start() はプロセスを起動してすぐに制御を返すだけです。

  • 親プロセスは start() を呼んだ後、すぐに try ブロックを抜けて終了してしまいます。

  • そのため、親プロセスは KeyboardInterrupt を受け取る待機状態になっていないので、例外が捕まらないように見えます。

一方で、私の例でやったように:

python
dataserver_process.join() service_process.join()
  • join() によって親プロセスは子プロセスの終了を待機する状態になります。

  • この待機中に Ctrl+C が押されると、親プロセスは KeyboardInterrupt を受け取り、except ブロックで捕まえられるようになります。

ですので、親プロセスで KeyboardInterrupt を捕まえたい場合は、少なくともどこかでブロック待機(join() など)をして、プロセスが終了するまで親が生きている必要がある、という理解で正しいです。

あなた:

### しい捕まえ方の例 子プロセス内でも KeyboardInterrupt を捕まえる構造にするのが安全です。 python コードをコピーする `def dataserverThread(): try: asyncio.run(dataserver()) except KeyboardInterrupt: print("Dataserver thread stopped by KeyboardInterrupt") def serviceThread(): try: asyncio.run(tasks.taskController(loadSettings.settings["taskInterval"])) except KeyboardInterrupt: print("Service thread stopped by KeyboardInterrupt") if __name__ == "__main__": print("This dataserver is started.") dataserver_process = multiprocessing.Process(target=dataserverThread) service_process = multiprocessing.Process(target=serviceThread) dataserver_process.start() service_process.start() try: dataserver_process.join() service_process.join() except KeyboardInterrupt: print("Stopping all processes...") dataserver_process.terminate() service_process.terminate() dataserver_process.join() service_process.join() print("All processes stopped.") sys.exit()`

dataserverThread内で、asyncio.run(dataserver())されているdataserver 関数内でも例外処理をしたいとき
ChatGPT:

dataserverThread 内で asyncio.run(dataserver()) を呼んでいる場合、dataserver 関数の中でも例外処理したい場合は、asyncio.run の外ではなく中で try/except を書くことになります。例えば:

python
async def dataserver(): try: async with serve(mainLoop, loadSettings.settings["ip"], loadSettings.settings["port"]) as server: await server.serve_forever() except KeyboardInterrupt: print("Dataserver async function caught KeyboardInterrupt") # 必要に応じてサーバを閉じる処理

こうすると、asyncio.run(dataserver()) を呼ぶときに、dataserver 内部で KeyboardInterrupt を捕まえて処理できます。

ポイント:

  • asyncio.run は基本的に KeyboardInterrupt をキャッチして asyncio タスクをキャンセルします。

  • 上のように async with serve(...) の中で try/except を置くことで、サーバの実行中に Ctrl+C が押されたときに、asyncio 内で捕まえてクリーンアップ処理が可能になります。

  • 外側の dataserverThread の try/except は、万一内部で捕まえられなかった例外や他のエラーのために残しておくのが良いです。

あなた:
このような構造の時、さらにdataserver関数内で動いているmainLoop関数内でも例外を処理したいときはどうなりますか。
async def mainLoop(websocket):
    try:
        await receiveLoop(websocket,controller.controller)    
    except KeyboardInterrupt:
        print("The dataserver is stopped.")
        websocket.close()

async def dataserver():
    async with serve(mainLoop, loadSettings.settings["ip"], loadSettings.settings["port"]) as server:
        await server.serve_forever()

def dataserverThread():
    asyncio.run(dataserver())
ChatGPT:

この構造だと、mainLoop 内での KeyboardInterruptほとんど捕まえられません。理由は以下の通りです:


理由 1:asyncio.run と KeyboardInterrupt の挙動

  • asyncio.run(coro) は、実行中に Ctrl+C が押されると、Python 標準の KeyboardInterrupt を受け取り、内部で全てのタスクを CancelledError に変換してキャンセルします。

  • そのため、mainLoop 内で except KeyboardInterrupt: を書いても、通常はそこに例外は届かず、asyncio.CancelledError が発生します。


理由 2:serve_forever の中でのキャンセル

  • async with serve(...) as server の中で await server.serve_forever() をしている場合、serve_forever() は内部でタスクを管理しているので、KeyboardInterrupt が入ると サーバ全体のタスクがキャンセルされる形で propagate されます

  • そのため、mainLoop 内で try/except しても、receiveLoop に直接 KeyboardInterrupt が届くことはありません。


実務的に捕まえるには

  • mainLoop 内で何かクリーンアップをしたい場合は、CancelledError を捕まえる必要があります:

python
async def mainLoop(websocket): try: await receiveLoop(websocket, controller.controller) except asyncio.CancelledError: print("The dataserver mainLoop is cancelled.") await websocket.close()
  • これで、dataserverThread 内で Ctrl+C が押され、asyncio.run(dataserver()) がキャンセルされた場合でも、mainLoop 内でクリーンアップが可能になります。


ポイント

  1. KeyboardInterrupt は asyncio の中では直接届かず、タスクキャンセル (CancelledError) に変換される。

  2. mainLoop 内で例外処理したい場合は CancelledError を捕まえる。

  3. dataserver 全体で KeyboardInterrupt を処理したい場合は dataserverThread 側で try/except を使う。

この構造を踏まえると、asyncio 内では Ctrl+C に対して CancelledError を使って例外処理するのが正しい方法です。

あなた:
dataserver 関数内では asyncio.CancelledErrorがつかまりますが、mainLoopでは機能しませんでした。
ChatGPT:

それは正しい挙動です。あなたのコード構造(websockets.serve(mainLoop, ...)await server.serve_forever())では、
mainLoop が直接キャンセル例外を受け取る立場にない ためです。

順に整理します。


1. serve(mainLoop, ...) の仕組み

websockets.serve() は、内部で新しいタスクを生成し、
新しいクライアント接続ごとに mainLoop(websocket) を呼び出します。

しかし:

python
await server.serve_forever()

という呼び出しをしているとき、

  • これは asyncio の「無限待機ループ」として動作します。

  • Ctrl+C(KeyboardInterrupt)が来ると、serve_forever() がキャンセルされ、
    サーバが新しい接続受付を停止します。

  • ただし すでに動作中の各接続(=mainLoop タスク)には CancelledError は即座には伝わらない のです。


2. なぜ mainLoop 側では捕まらないのか

serve_forever() は「サーバループ」をキャンセルしますが、
既に生成されているクライアントごとのタスク (mainLoop) は、

  • それぞれの await receiveLoop() がまだ動作中であり、

  • その内部が asyncio のキャンセルを伝播していない(またはブロッキング中)場合、
    CancelledError が伝わらないまま強制終了します。

また KeyboardInterrupt 自体は mainLoop には直接伝播しません。
asyncio.run() が最上位で捕まえてキャンセルに変換しているため)


3. 対処法

mainLoop で確実にクリーンアップ処理を行いたい場合は、
try/finally で明示的にリソースを閉じるようにします。

python
async def mainLoop(websocket): try: await receiveLoop(websocket, controller.controller) except asyncio.CancelledError: print("mainLoop cancelled") raise # 上位に再伝播してOK except Exception as e: print(f"mainLoop error: {e}") finally: await websocket.close() print("websocket closed.")

これなら:

  • CancelledError が伝わる場合も、

  • それ以外の異常終了の場合も、

  • 最後に finally で確実に websocket.close() が呼ばれます。


4. 重要な補足

asyncio.CancelledErrormainLoop に届かないケースは、

  • receiveLoop 内部がブロッキング I/O を行っている

  • もしくは asyncio のキャンセルポイントを含まないループを回している
    場合です。

その場合は、receiveLoop 内で await asyncio.sleep(0)
await websocket.recv() のようなキャンセル可能な await を適度に挟む必要があります。


つまり:

  • dataserver 内ではサーバ全体のキャンセルが捕まる(serve_forever() が中断される)

  • mainLoop 内では、キャンセルが内部処理 (receiveLoop) に伝わらなければ例外処理が機能しない

  • これを防ぐには、receiveLoop 側でキャンセル可能な設計を維持する

この動きは websockets と asyncio の典型的な仕様通りです。