Electron を試す 10 - Main プロセスのデバッグ
このシリーズ第一回で 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 プロセスをデバッグするための方法が掲載されている。
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 が割り当てられるはず。
あとは普段 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、Browserify や webpack といった 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 で複数ファイルを単一化しているとブレーク ポイントを貼りにくい。
outFiles
は program
と関連付けるファイル一覧を定義。Transpiler を利用しているなら変換先のエントリー ポイントになるファイルを指定。以前は sourceMaps
を true
に指定する必要もあったが現在はデフォルトで有効。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 ファイル。これはに変換元ファイルのパスとコード行などが定義されている。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.json
の name
が参照されることを期待するけど、実際にはアプリの実行ファイル名が使用されるようだ。例えば AudioPlayer.app
や AudioPlayer.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 などで知らせていただきたい。