CDPのアビス — 全4編
現在地:浅層編
Executive Summary — 地上への帰還報告
- 新規プロファイルは、最初のEdge起動中なら5件のダウンロードに成功した。しかし、そのプロファイルを再利用してEdgeを起動すると、2回目または3回目の起動中にクラッシュした。
- 画面を表示するheaded実行と、表示しないheadless実行の両方で再現した。headless固有の問題ではなかった。
- Playwrightの
Download.save_as()を使わない経路でも再現した。save_as()はクラッシュ原因ではなく、Edge終了後に異常を観測した場所だった。- 浅層で目立つ容疑者を外しても異変は残ったため、次はPlaywrightそのものを置いて、CDPと直接通信する深層へ降りる必要があった。
この層の呪い:新品のプロファイルが最初の探索に成功し、問題が消えたように見せる。
この層の祝福:条件を一つずつ外すたび、無実の容疑者を地上へ帰せる。
深層碑文:「一度の無事な帰還は、安全の証明ではない。次の降下が地形を変えることもある。」
現在深度:浅層から中層
地上編では、TargetClosedError が最深部の原因を説明していないことを確認した。
この編で調べるのは、地上から見えやすい三つの容疑者である。
浅層1 古いプロファイルの破損
浅層2 headlessという特殊な実行方法
中層3 Download.save_as()という保存API
調査の基本ルールは単純だ。
一度に一つの装備だけを外し、それ以外の条件を変えない。
複数の条件を同時に変えると、成功しても失敗しても理由が分からなくなる。
第一層:古いプロファイルだけが呪われているのか
最初にクラッシュしたのは、以前から使っていたEdgeプロファイルだった。
プロファイルにはCookie、キャッシュ、設定、ログイン状態など、多くのデータが蓄積する。古いプロファイルだけが壊れているという仮説は自然である。
そこで、既存の個人プロファイルから完全に離れ、テスト専用の空フォルダからEdgeに新規プロファイルを作らせた。
その結果、最初の起動では5つのリンクから5件すべてをダウンロードできた。
新規プロファイル
└─ Edge起動1回目
├─ download 1: 成功
├─ download 2: 成功
├─ download 3: 成功
├─ download 4: 成功
└─ download 5: 成功
ここで探索を終えれば、「新しいプロファイルへ交換すれば直る」と判断してしまう。
しかし、同じプロファイルを持ち帰り、Edgeを正常終了し、もう一度使うと異変が現れた。
同じ新規プロファイル
├─ Edge起動1回目: 成功
└─ Edge起動2回目または3回目: ダウンロード中にクラッシュ
複数の独立した新規プロファイルで同じ傾向を確認した。したがって、元の個人プロファイルだけの破損では説明できない。
重要なのは「古いか新しいか」ではなく、一度使ったプロファイルを次のEdgeプロセスへ引き継ぐことだった。
第二層:暗闇で潜るheadlessが悪いのか
ブラウザ自動化には二つの見た目がある。
| モード | 説明 |
|---|---|
| headed | 通常のブラウザ画面を表示する |
| headless | 画面を表示せず、裏側で実行する |
画面のないheadlessは特殊に見えるため、トラブル時に疑われやすい。
しかし、結果は両方とも同じだった。
headless: 起動1回目は成功、起動2回目に切断
headed: 起動1回目は成功、起動2回目に切断
さらに、後で回収した両モードのクラッシュダンプは、msedge.dll 内の同じ命令位置を示した。
暗闇で潜ったから落ちたのではない。画面を表示していても、同じ深度で同じ事故が起きていた。
第三層:save_as() が足場を壊したのか
地上へ返ってきた例外には、次の名前が刻まれていた。
Download.save_as: Target page, context or browser has been closed
もっとも目立つのは save_as() である。そこで保存方法を変えた。
| 検証 | 結果 |
|---|---|
Download.save_as() で保存 |
Edgeが終了し、TargetClosedError |
download.path() で一時ファイルを参照 |
同じクラッシュを確認 |
| PlaywrightのDownload APIを使わないraw CDP | 同じEdgeネイティブクラッシュを確認 |
時系列を並べると理解しやすい。
1. Edge内部でネイティブクラッシュ
2. Edgeプロセスが終了
3. CDP接続が失われる
4. PlaywrightのBrowser / Context / Pageが閉じた状態になる
5. save_as() がTargetClosedErrorを返す
save_as() は最後に悲鳴を上げた。しかし、足場を壊した本人ではなかった。
ダウンロード方式を変えても残るのか
Playwright 1.62.0が使うダウンロード許可方式に近い allowAndName と、より単純な allow も比較した。
pipe + allow : 再現
pipe + allowAndName : 再現
port + allowAndName : 連続成功
ファイル名の扱いだけでは、pipe経路のクラッシュを説明できなかった。
起動引数という装備を外す
PlaywrightはChromium系ブラウザを安定して操作するため、複数の起動引数を自動で付ける。
そこで、次の候補を段階的に外した。
- Playwrightの長い
--disable-features=...指定 -
DestroyProfileOnBrowserCloseに関係する差 - Edge固有の更新・互換性関連スイッチ
- そのほか再現に不要なPlaywright既定引数
それでも、最小化したpipe起動でクラッシュは残った。試行によって発生が2回目から3回目へ動くことはあったが、消えなかった。
これは「すべての起動引数が無関係」と証明するものではない。ただし、特定の分かりやすい一引数を外せば直る、という単純な形ではなかった。
浅層の探索結果
ここまでの仮説を整理する。
| 仮説 | 判定 | 根拠 |
|---|---|---|
| 元のプロファイルだけが破損していた | 棄却 | 新規プロファイルでも再利用後に再現 |
| headlessだけの問題 | 棄却 | headedでも再現し、ダンプ位置も一致 |
save_as() が必要原因 |
棄却 | PlaywrightのDownload APIなしでも再現 |
allowAndName だけの問題 |
棄却 |
allow でも再現 |
| 一つの既定起動引数だけが原因 | 支持されず | 最小化したpipe起動でも再現 |
浅層で目立っていた容疑者は帰った。
残った大きな問いは一つである。
Playwrightを完全に外しても、同じEdgeクラッシュへ辿り着けるのか。
次の層へ:案内役を置いていく
ここまではPlaywrightという案内役と一緒に降りてきた。
次の深界編では、Playwright、Puppeteer、Selenium、外部npmパッケージをすべて置いていく。Node.jsの標準機能だけでEdgeへCDPメッセージを送り、同じ5 MiBファイルをダウンロードさせる。
そこで現れるのが、ほぼ同じ縦穴に見える pipe と port の分岐である。
深度案内
- 地上編:
TargetClosedErrorは最深部の事故を説明しない - 浅層編:新品のプロファイルも、再利用すると様子が変わる(本記事)
- 深界編:Playwrightを置いてraw CDPでpipe / port分岐へ降りる
- 最深部編:Crashpadの記録石を読み、帰還路を選ぶ