Electron を試す 10 – Main プロセスのデバッグ

2017年8月23日 0 開発 ,

このシリーズ第一回node-inspector を利用した Main プロセスのステップ実行を紹介した。しかし node-inspector は現在、非推奨である。Node v6 でサポートされた --inspect を使用することになっている。

Node 同様に Electron も v1.7.2--inspect が追加された。つまり Node/Electron 共に単体でステップ実行を利用できる。現時点の latest version は v1.7.5 なので開発者としても正式にこの機能を採用する時がきた。

というわけで、Electron が提供するデバッグ機能についてまとめる。

サンプル プロジェクトは以下。

Chrome

Electron 公式サイトに Main プロセスをデバッグするための方法が掲載されている。

Debugging the Main Process | Electron

Electron CLI を実行する際に --inspect=[port] オプションを指定することで、Electron の Main プロセスと外部デバッガーを連携できる。例えば

electron --inspect=5858 your/app

のようにする。package.json の npm-scripts に定義する場合は

{
  "scripts": {
    "app": "electron --inspect=5858 src/"
  }
}

こんな感じになる。実際に Terminal から実行すると

$ npm run app

> electron-audio-player@1.4.0 app .../examples-electron/audio-player
> electron --inspect=5858 src/

Debugger listening on port 5858.
Warning: This is an experimental feature and could change at any time.
To start debugging, open the following URL in Chrome:
    chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:5858/ee7b65f0-dc0d-46f7-9d9e-997441981f52

外部デバッガーとして使用する Chrome 用の URL が出力される。これを Chrome のアドレス バーに入力してページを表示すれば DevTools が割り当てられる。

--inspect と Chrome

あとは普段 DevTools を利用しているように Sources タブからブレーク ポイントを貼ってステップ実行したり、変数の内容を参照するなどのデバッグが可能となる。

なお --inspect=[port]=[port] は省略可能。その場合は自動的にポート 5858 が選択されたものとして扱われる。省略するかはお好みで。開発環境によってはポート番号が競合するかもしれないので、変更可能であることを明示するため規定値でもそのまま指定しておくのもよいだろう。私はそうしている。

Visual Studio Code

Atom と並んで Web アプリ開発者に人気のテキスト エディター Visual Studio Code (以下、vscode) にはデバッグ機能が実装されている。これは vscode の大きな特徴であり、エディターにも関わらず IDE のようなデバッグを可能とする。

これは汎用的に設計してあり、様々な言語へ対応できるようになっている。Microsoft 謹製の TypeScript や C# は当然として Python、Ruby、PHP といった言語から Node (JavaScript) まで幅広くサポート。もちろん Electron からも利用可能。

Main プロセスを素の Node で実装している場合は上記の公式資料にある設定だけで使い始められる。しかし Babel などの Transpiler、Browserifywebpack といった Bundler を利用して JavaScript を変換しているならば対象となるコードを指定するために工夫が要る。

vscode のデバッグ機能と設定ファイルの詳細は以下を参照のこと。

launch.json

はじめに vscode 用の設定ファイルを定義する。vscode で読み込むプロジェクトのルートに .vscode フォルダーを作成し、その中へ launch.json というファイルを作成。内容は以下。

{
  // Use IntelliSense to learn about possible Node.js debug attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Main Process",
      "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
      },
      "program": "${workspaceRoot}/src/js/main/Main.js",
      "outFiles": [
        "${workspaceRoot}/src/assets/main.js"
      ]
    }
  ]
}

configurations 内に設定を定義してゆく。

type はデバッグ実行のプラットフォームを表す。Electron 用の設定ではなく Node として実行する。request は新たにプロセスを起動する launch と起動されているものを利用する attach のいずれかを指定する。基本、launch でいい。name は役割で命名しておく。

${workspaceRoot} は特別な変数で、プロジェクトのルート フォルダーをあらわす。基本的に設定はこのフォルダー内で完結するだろうから、パスはこれで開始しておけばよい。

runtimeExecutable はデバッグ実行で使用する Electron のパスになる。Windows だけ起動方法が変わる (cmd.exe 経由になる) ため windows 内へ個別に定義が必要。

program はデバッグ実行のエントリー ポイントになるコードを指定する。Transpiler を利用しているなら変換元のエントリー ポイントになるファイルを指定する。変換先を指定しても動作するが、Bundler で複数ファイルを単一化しているとブレーク ポイントを貼りにくくなる。

outFilesprogram と関連付けるファイル一覧を定義。Transpiler を利用しているなら変換先のエントリー ポイントになるファイルを指定する。以前は sourceMapstrue に指定する必要もあったが、現在はデフォルトで有効になっている。Transpiler/Bundler と Source Maps を利用した最小の設定としては今回のもので十分。

他にもデフォルトが変更されて省略可能なものがあったり、outDir が deprecated で outFiles が代替になっているなどの変化がある。この記事ですら古びるかもしれないので、自分で設定する際は前述の公式資料を必ず確認すること。

Source Maps の注意点

私の環境は Babel + Browserify になる。元のコードは ES.next (例えば ES2015 の Modules など) で書いて、Babel により Electron 向けに変換している。これらのタスクは package.json の npm-scripts で組んでおり、デバッグに関連する定義は以下となる。

{
  "babel": {
    "presets": [
      ["env", {"targets": {"electron": "1.7"}}],
      "react"
    ]
  },
  "scripts": {
    "watch:js-main": "watchify -v -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node -o \"exorcist --base ./src/assets ./src/assets/main.js.map > ./src/assets/main.js\" -d"
  },
  "devDependencies": {
    "babel-preset-env": "^1.6.0",
    "babel-preset-power-assert": "^1.0.0",
    "babel-preset-react": "^6.24.1",
    "babelify": "^7.3.0",
    "browserify": "^14.4.0",
    "electron": "^1.7.5",
    "exorcist": "^0.4.0",
    "watchify": "^3.9.0"
  },
}

watch:js-main が browserify (watchify) によるファイル監視つきの JavaScript 変換になる。

browserify により変換された内容を exorcist に渡して Source Maps となる main.js.map を生成し、その後に変換結果となる main.js を出力している。

これらのうち、vscode のデバッグで重要なのは Source Maps にあたる map ファイルである。map ファイルは変換元ファイルのパスとコード行などが定義されている。vscode のデバッグ機能で map ファイルを使用する場合、変換元ファイルのパスは map からの相対にしなければならない。

exorcist の README によれば、オプションを何も指定しないと変換元ファイルのパスは absolute path (絶対パス) になる。実際には exorcist を実行したフォルダーをルートにした相対パスになるようだが、どちらにせよ map ファイルから見た変換元ファイルへのパスにはならない。

この状態で vscode のデバッグを開始すると変換元を見つけられず program に指定されたファイルをそのまま解釈してしまう。デバッガーは Node として動作するため、現時点で未対応の import 構文などを見つけると Syntax Error 例外でプロセスが中断される。

これを防ぐため exorcist の --base オプションに map と変換されたファイルの出力先フォルダーを指定。変換元ファイルへのパスがそこから見た相対となるようにした。

exorcist --base ./src/assets ./src/assets/main.js.map > ./src/assets/main.js

--base [PATH] の有無で map ファイルの sources が変化することを確認できる。

デバッグ実行

すべての設定が正しく定義されていれば vscode のデバッグ機能を利用できる。JavaScript 変換 & map ファイル生成された状態で vscode のメニューから「デバッグ」、「デバッグの開始」を選ぶとアプリが起動されるはず。

次に vscode でブレーク ポイントを貼りたいファイルを開き、対象行の行番号の左側をクリックすると赤い丸印アイコンが表示される。

ブレーク ポイント

この状態でその場所を通るような操作をアプリから実行すると、その箇所でプロセスが一時停止 (ブレーク) する。このとき vscode のサイドバーからデバッグを開いていれば、変数なども確認できる。

デバッグ実行

コード編集に使用しているエディターとデバッガーがひとつになっていることの便利さ (IDE 感) を実感。これはいい。

userData のパス問題

Electron でアプリ固有のデータを保存する場合は app.getPath('userData') で得られたフォルダーを利用することが多いだろう。このパスは Electron の Web Storage や IndexedDB などを保存する場所で、OS のユーザー単位かつアプリ単位で作成される。

userData には

The directory for storing your app’s configuration files, which by default it is the appData directory appended with your app’s name.

とある。この説明なら package.jsonname が参照されることを期待するけど、実際にはアプリの実行ファイル名が使用されるようだ。例えば AudioPlayer.appAudioPlayer.exe
であれば AudioPlayer というフォルダーになる。

Chrome によるデバッグでは Electron プロセスを単体実行して Chrome と関連付けているだけなので、userData は通常のアプリと同じ場所になる。しかし vscode のデバッグ実行だとフォルダー名は Electron 一択となる。

そのため複数のアプリでデバッグ実行している場合、データが競合するかもしれない。これに気付いたのは、これまで --inspect を利用して IndexedDB などに保存していたデータが vscode のデバッグだと読み込めていなかったから。userData のパスを調べたら、どのプロジェクトでも Electron になっていた。app.getName()Electron を返す。

かなり致命的な問題と思うのだけど、ざっと調べたかぎり vscode 公式のデバッグ関連資料や GitHub issues にはこれについての言及が見つからなかった。後で issue あげるかも。

いちおう app.setPath('userData', path) でパスの上書きは可能だが app.getName()Electron なのでアプリ名を抽象化しきれず、ハード コードが発生する。そもそも開発環境の都合でアプリ側のコードが左右されるのも好ましくない。

この件について情報をお持ちの方は、コメント欄や Twitter などで知らせていただきたい。


REPLY

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です