この記事を作った動機

 2026/9/16 に至るまでの間で OneNote 代替に関して、実装をするなど研究を進めているうちにわかったことなどを簡易的に記録する。

次に軽くやりたいことを記述

  • 割り込みの呼び出しが、コマンドモジュールから、コマンドコントローラのプロセスを介して、割り込みのプロセスへ IPC するようにする。
  • フロントエンドの割り込みの実装を今の試作物の DataServer に適合させる。
  • フロントエンドが DataServer の拡張機能読込み時に HTML CSS JS としてビルドされて配置され、それがホスティングされるようにデータサーバを実装する。
  • pexpect のログをファイルシステム上の位置を決めてテキストファイルとしてそれぞれ書き出すようにし、ユーザーにはそれを独立した端末で watch -n 0.1 "cat log | tail -n 10" のようにして動いている様子が確認できるようにする。できれば各コントローラやホスティングのプロセスでも同じようなログの出し方をしたい。
  • データサーバ実行において、端末セッション維持に使っている pexpect の実装を見直す。subprocess ライブラリを使うように変更するか、pexpect においてログが出るようにしながら、pexpect のタイムアウトでサーバーが強制終了しないようにする。
  • 各拡張機能が設定を持てるように実装する。
  • 拡張機能のファイルの読み書きが衝突しないようにするためのコントローラを実装し、ファイルの読み書きを各拡張機能モジュールに対して、統一したヘルパー関数を経由するようにする。ヘルパー関数は、各コントローラプロセスから、IPC によってファイルを読み書きするためのプロセスにアクセスし、リクエストを送り、結果を待機して各モジュールと接続される。各モジュールは、ファイル読み書きのヘルパー関数を呼び出すとき、リクエストだけではなく、リクエスト完了時やエラー時のコールバック関数を登録し、処理が続行されるようにする。

やったことの詳細

現状の動作の様子

 現状としてはかろうじて動くという感じでバグや不具合だらけである。DataServer なのか、それとも卒論の試作物で使いまわしているフロントエンドの実装がいけないのか、切り分けがついていないことも多く、しばらくは調査が必要そうである。特にページコントローラ周りが動作が不安定である様相が見受けられる。これは、拡張機能名とUUIDがモジュール名に加えて解決が必要になったことが影響していると考えられる。

 動画中では撮影中に気づいた不具合があり、動揺しているが表示と作ろうとしているページが一致していないことが分かった。これはフロントエンドのif,elseの実装が多分確認がまだできていないが良くない気がしており、未知のページタイプををすべてjsonとして扱うように実装していたのが影響している可能性がある。新しく実装したDataServerが返すページタイプのリストが、今の一時的にあてがっているフロントエンドに適合していないため、色々とそこら辺で不具合が発生しているように思われる。

拡張機能を導入するためにしたこと

モジュールに渡す引数をクラスで共通化して定義

 コントローラの引数のモジュール呼び出しの定義を変えると、連動して拡張機能側のモジュールも引数を正しい順番や名前で合わせる必要があり、煩雑になるため、クラスにまとめて小さな変更が大きな影響として波及しにくいようにした。

 主な役割として、mainSystem のサブシステムからモジュールに対して共通して提供する関数群や設定、必要があればフロントエンドとの接続についてなどを保持、伝搬することがある。

実装
class commandModuleArgs: 
    def __init__(self,request:dict,websocket:ServerConnection,Settings:dict) -> None:
        self.request    = request
        self.websocket  = websocket
        self.settings   = Settings
        self.funcs      = {
            # common lib
            "showJSONMessage"               : showJSONMessage,
            "dataKeyChecker"                : dataKeyChecker,
            "timeString"                    : timeString,
            "findNotes"                     : findNotes,
            "checkTheAbsolutePath"          : checkTheAbsolutePath,
            "deleteDataSafely"              : deleteDataSafely,
            "updateNotebookMatadata"        : updateNotebookMatadata,
            "readMetadataFormMarkdownPage"  : readMetadataFormMarkdownPage,
            "mkdir"                         : mkdir,

            # access passive controller 
            "callInterrupt"                 : callInterrupt,
            "getPageTemplate"               : getPageTemplate,
            "getPageTypeList"               : getPageTypeList,

            # server response 
            "NotImplementedResponse"        : NotImplementedResponse,
            "malformedRequestResponse"      : malformedRequestResponse,
            "internalServerErrorResponse"   : internalServerErrorResponse,
            "notFound"                      : notFound,
            "malformedRequestChecker"       : malformedRequestChecker,
        }

    # def getArgs(self) -> dict:
    #     return {
    #         "request"   : self.request,
    #         "websocket" : self.websocket,
    #         "funcs"     : self.funcs,
    #         "Settings"  : self.settings
    #     }

class interruptModuleArgs:
    def __init__(self,data:dict,websocket:ServerConnection,allWebSocketConnections:list[ServerConnection]) -> None:
        self.data           = data
        self.mainConnection = websocket
        self.allConnection  = allWebSocketConnections
        self.funcs          = {
            "sendInterrupt" : sendInterrupt
        }

    # def getArgs(self) -> dict:
    #     return {
    #         "data"          : self.data,
    #         "mainConnection": self.mainConnection,
    #         "allConnection" : self.allConnection,
    #         "funcs"         : self.funcs
    #     }

class pageModuleArgs:
    def __init__(self,data:dict) -> None:
        self.data   = data
        self.funcs  = {
            # common lib
            "showJSONMessage"               : showJSONMessage,
            "dataKeyChecker"                : dataKeyChecker,
            "timeString"                    : timeString,
            "findNotes"                     : findNotes,
            "checkTheAbsolutePath"          : checkTheAbsolutePath,
            "deleteDataSafely"              : deleteDataSafely,
            "updateNotebookMatadata"        : updateNotebookMatadata,
            "readMetadataFormMarkdownPage"  : readMetadataFormMarkdownPage,
            "mkdir"                         : mkdir,
        }

    # def getArgs(self) -> dict:
    #     return { 
    #         "data"  : self.data,
    #         "funcs" : self.funcs
    #     }


class taskModuleArgs:
    def __init__(self,Settings:dict) -> None:
        self.settings   = Settings
        self.funcs      = {
            # common lib
            "showJSONMessage"               : showJSONMessage,
            "dataKeyChecker"                : dataKeyChecker,
            "timeString"                    : timeString,
            "findNotes"                     : findNotes,
            "checkTheAbsolutePath"          : checkTheAbsolutePath,
            "deleteDataSafely"              : deleteDataSafely,
            "updateNotebookMatadata"        : updateNotebookMatadata,
            "readMetadataFormMarkdownPage"  : readMetadataFormMarkdownPage,
            "mkdir"                         : mkdir,
        }

    # def getArgs(self) -> dict:
    #     return {
    #         "funcs": self.funcs
    #     }

定義したクラスに合わせて、拡張機能側のモジュールの引数と内部実装を変更

 卒論で作った最初の試作物では、引数が今回の試作物の DataServer のコントローラとそのままでは互換性がないため、それに合わせる変更をした。具体的には、モジュールの引数の変更に加え、mainSysで共通に提供している関数や設定を使うように書き換えた。

ページ新規作成において、テンプレートを司るコントローラについて追記

 ページの新規作成のためのテンプレートにもコントローラがあり、動的に動作するようにしていたことが抜け落ちていたため、実装をした。以下は、コントローラの実装である。実際には、以下のコードを書くだけではなく、モジュール間の引数の共通化のためのクラスの定義と、ローダーの対応、拡張機能の実装の変更など広域で多岐にわたる調整を行った。

from controller.runtime import pageExtensionMoludes
from controller.common import pageModuleArgs

# when return None -> unknown pageType
def getPageTemplate(pageType:str,data:dict) -> str | None:
    for aPageType in pageExtensionMoludes.keys():
        if(aPageType == pageType):
            return pageExtensionMoludes[aPageType](pageModuleArgs(data))

    return None

def getPageTypeList():
    return pageExtensionMoludes.keys()

import 文を拡張機能のモジュールの各スクリプトに追加

 各拡張機能の、各モジュールの関数の引数は、専用の共通化されたクラスのインスタンスを受け取るようになっており、Python の型チェックを利用するために、Import を追加した。なお以下だと実装として不十分であり、そのままでは循環 Import となって動作しない。型チェックのための-import-が循環参照となってしまう問題について を参照すること。

# 当初追加していた Import 文。これだと、不十分である。
from controller.common import commandModuleArgs

拡張機能を導入した状態で生じた問題

拡張機能の ZIP ファイル名を変更

 当初予定していた[拡張機能名]-[UUID]の名前構文では、問題が生じることが分かった。Python の Import 文において、-は使えないため、その構文だと、Import できないという問題になった。

 そのため、解決策としてまず、以下のように拡張機能の ZIP のディレクトリを構成することで解決しようとした。

- [拡張機能名を名前に持つフォルダ]
    - [拡張機能のUUIDを`-`を取り除いて名前に持つフォルダ]
        - manifest.json など、拡張機能の中身

 しかしこれだと今度は、UUID 次第で Import 時に数字が先頭に来てしまい、Import エラーになってしまうことや、拡張機能のフォルダ構成が必要以上に複雑になるという問題があった。

 そこで、現状としては、拡張機能の ZIP の内部ディレクトリを以下のように構成することで、問題を解決とした。zip ファイル名自体は当初と同じ、[拡張機能名]-[UUIDハイフンあり].zipである。

- [拡張機能名とUUIDを合わせて名前に持つフォルダ]
    - manifest.json など、拡張機能の中身

 この解決策には制約があり、拡張機能名は、先頭に特殊文字や数字を含むことができないということになり、Python の Import 文に命名規則が制約される点がある。

型チェックのための Import が循環参照となってしまう問題について

 拡張機能モジュールで型チェックのためにインポートする記述が、mainSystem サブシステムにおいて循環参照の問題になる可能性があることが分かった。現状では、LLM が出した案が動いておりそれを採用しているが、そもそもなぜ循環参照になったのか、今後それが起こるリスクについてはどうなのかについて、十分説明がついていない。

 現状の理解では、以下のように記述することで、インポートしたモジュールなどが、型チェックのためのアノテーションとして使われ、直接関数を呼び出したりクラスをインスタンス化するなどといったことには使われていないことを明記するということかな?という感じである。

  • 現状の記述例
## LLM (DuckDuckGo ChatGPT) 出力 調査が必要 -------
## LLM (DuckDuckGo ChatGPT) 出力 調査が必要 -------
from __future__ import annotations
from typing import TYPE_CHECKING
if(TYPE_CHECKING):
    from controller.common import commandModuleArgs
## LLM (DuckDuckGo ChatGPT) 出力 調査が必要 -------
## LLM (DuckDuckGo ChatGPT) 出力 調査が必要 -------

async def template(moduleArgs:commandModuleArgs): # TODO: write command name
# ...
  • 循環参照になり mainSys が立ち上がらない例
python -u main.py
# Traceback (most recent call last):
#   File "/home/UserName/work/OneNoteAlternative/secondPrototype/backend/mainSys/main.py", line 5, in <module>
#     from controller.command     import init as command
#   File "/home/UserName/work/OneNoteAlternative/secondPrototype/backend/mainSys/controller/command.py", line 5, in <module>
#     from controller.runtime import commandExtensionMoludes
#   File "/home/UserName/work/OneNoteAlternative/secondPrototype/backend/mainSys/controller/runtime.py", line 11, in <module>
#     from Core589fe65d639c42a3b395fca2143afd75.backend.commands.notebookAndPage.createNotebook import createNotebook
#   File "/home/UserName/work/OneNoteAlternative/secondPrototype/backend/extensions/runtime/Core589fe65d639c42a3b395fca2143afd75/backend/commands/notebookAndPage/createNotebook.py", line 1, in <module>
#     from controller.common import commandModuleArgs
#   File "/home/UserName/work/OneNoteAlternative/secondPrototype/backend/mainSys/controller/common.py", line 4, in <module>
#     from controller.interrupt import callInterrupt
#   File "/home/UserName/work/OneNoteAlternative/secondPrototype/backend/mainSys/controller/interrupt.py", line 6, in <module>
#     from controller.runtime import interruptExtensionMoludes
# ImportError: cannot import name 'interruptExtensionMoludes' from partially initialized module 'controller.runtime' (most likely due to a circular import) (/home/UserName/work/OneNoteAlternative/secondPrototype/backend/mainSys/controller/runtime.py)

その他

 各コントローラにおいて、解決が失敗した場合にエラーを出すのではなく、Core拡張機能として暗黙に扱いそれで解決されるか試してからエラーを出すようにした。以下はその実装の一部の例である。CoreExtPrefixの中身は、Core-589fe65d-639c-42a3-b395-fca2143afd75/となっている。

async def callInterrupt(websocket:ServerConnection,interruptName:str,data:dict):
    from controller.common  import interruptModuleArgs, CoreExtPrefix
    from controller.runtime import interruptExtensionMoludes
    for AnInterrupt in interruptExtensionMoludes.keys():
        if(AnInterrupt == interruptName):
            return await interruptExtensionMoludes[AnInterrupt](interruptModuleArgs(data,websocket,websocketConnections))        
        elif(AnInterrupt == CoreExtPrefix + interruptName):
            return await interruptExtensionMoludes[AnInterrupt](interruptModuleArgs(data,websocket,websocketConnections))            
    print("callInterrupt ERROR: The interrupt does not exist.")
    return True

 またこのことから、いちいち名前を UUID まで含めて解決するのは実装として大変すぎる気がしたので以下のような階層を設定したいと構想することがあった。

    1. モジュール名だけで解決
    1. 拡張機能名とモジュール名で解決
    1. 拡張機能名とモジュール名と拡張機能のUUIDで解決

 この階層設定の問題点としては、コマンドなどを呼び出すフロントエンドの実装をどうするかということである。基本的には、拡張機能名もUUIDも必要としないと、拡張機能間でモジュール名が重複したり、拡張機能名が重複したりしたときにモジュールを解決できないことになる。

 モジュールの呼び出し自体をよりフロントエンドにおいても共通化する必要性が考えられる。DataServer での暗黙の自動解決は、Core 拡張機能までとして、他の拡張機能に対しては、常にモジュール名だけではなく、モジュールが属している拡張機能名や拡張機能UUIDをセットにするようにし、上記の三段階はフロントエンド側の実装で解決することも考えられる。

研究に関係ありそうなブログ記事とか

参考にしたサイトとか