
アプリを終了不可能にした「安全チェック」
自動アップデートは、私たちが出荷した中で最も難しい機能でした — アプリを文鎮化させないために、4日間で6回のリリース。最悪のバグは、慎重であろうとして自ら仕込んだものでした: 失敗したアップデートを、文字通り終了できないプロセスに変えてしまった、500ミリ秒の「安全」チェックです。
デスクトップアプリの開発者は誰でも、ちょうど一度だけ自動アップデートを甘く見ます。解決済みの問題のように見えるのです — ライブラリが新しいバージョンをダウンロードし、アプリを再起動する。ところが出荷してみると、「アプリを再起動する」というのが、プログラムに頼めることの中でも最も危険なもののひとつだと学ぶことになります。なぜなら、それはアプリが自分自身を解体している、まさにその瞬間に起こるからです — 誤りに対する余裕が最も少ないときに。
GeekByeの自動アップデートは、安定するまで4日間で6回のリリースを要しました — v1.5.14からv1.5.19まで。これは、その期間で最悪のバグの物語です。慎重であろうとして、私たち自身が引き起こしたものでした。
6回のリリース、そして本当に重要だった1回
始まりは平凡でした。v1.5.14は、恥ずかしい誤字レベルのバグを修正しました: アップデートフィードが存在しないGitHubリポジトリ名を指していたため、アップデータは404をチェックしていたのです。v1.5.15は手動の「Check for Updates」ボタンと、まともなエラーメッセージを追加しました。そこからquitAndInstallのバグが始まり、リリースが立て続けに出ました — なぜなら、アップデートの仕組みが壊れているとき、その修正をアップデートの仕組みを通して出荷することはできないからです。反復のたびに手動での再インストールという賭けになります。
本当に重要だったのはv1.5.18です。その内容の全部が、今でも私を身をすくませるタイトルの付いた、たった1つのコミットでした: 終了不可能なアプリを防ぐため、元のquitAndInstallの挙動を復元する。
「慎重であること」がどうやってアプリを文鎮化させたか
状況はこうです。アップデートがダウンロードされると、ElectronのquitAndInstallがアプリを閉じ、新しいバージョンに入れ替えることになっています。以前のリリースで、誰かが — もっともなことに — 無条件に終了するのは危険だと心配しました。もしインストールが失敗したら? すべてが健全に見えるときにだけ終了する方が安全ではないか?
そこでコードには、賢明に見えるガードが加わりました:
autoUpdater.quitAndInstall(false, true)
setTimeout(() => {
if (this.updateDownloaded)
app.quit() // only quit if the update is still "good"
else console.log('Error detected — keeping app open')
}, 500)
ロジックはこうです: インストールを発火させ、半秒待ち、updateDownloadedフラグがまだtrueの場合にのみ最終的なapp.quit()を強制する — そうでなければ、ユーザーが取り残されないようアプリを開いたままにする。
罠は一行先、エラーハンドラの中にあります。そのハンドラはthis.updateDownloaded = falseを設定していました。だから失敗したインストールを思い描いてください: errorイベントが発火してフラグをクリアします。しかしquitAndInstallはすでに後始末を始めていました — ウィンドウを閉じ、アプリの終了リスナーを取り除いていたのです。そこへ500msのタイマーが目を覚まし、いまやfalseになったフラグをチェックし、「エラーを検出、アプリを開いたままにする」と判断し、app.quit()をスキップします。
こうしてあなたは、まさにmacOSに特有の、最悪の状態に置かれます。macOSは、最後のウィンドウが閉じたというだけではアプリを終了しません — それがあらゆるMacアプリが依存するwindow-all-closedの挙動です。だからプロセスはまだ生きているのに、**ウィンドウがなく、メニューバーへの経路もなく、終了リスナーは引き抜かれています。**クリックするものが何もない。Cmd-Qは話しかける相手がいない。唯一の脱出口はActivity MonitorからのForce Quitです。この「安全」チェックは、失敗したアップデート — 回復可能な厄介事 — を、殺せないゾンビに変えてしまったのです。
修正: 後始末は無条件でなければならない
v1.5.18での訂正は、ほとんど攻撃的なまでに退屈です。そしてそれこそが要点です。賢さを取り除きます:
- 干渉しうる
window-all-closedとbefore-quitのリスナーを取り除く。 - すべてのウィンドウを破棄する —
window.close()ではなくwindow.destroy()。closeはハンドラによって拒否されうるが、destroyはされない。シャットダウンに踏み切るとき、丁寧に頼んだりはしない。 quitAndInstallを呼ぶ。app.quit()を無条件に呼ぶ。
フラグなし、タイマーなし、「念のため開いたままにする」なし。なぜなら、シャットダウン経路についての真実は、中途半端に終わったシャットダウンはどちらの結末よりも悪い、ということだからです。完全に終了するのは問題ない。完全に開いたままなのも問題ない。決して到達してはならない唯一の状態は解体されているのにまだ動いているであり — そしてそれこそ、条件付きの終了があなたを立ち往生させかねない、まさにその状態なのです。
同じ週が教えた、あと2つの教訓
終了不可能なバグが目玉ですが、6回のリリースの奔走は、盗む価値のある他の2つの習慣を鍛えてくれました。
**クラッシュテレメトリは、広いキーワードではなく正確なシグネチャでフィルタリングせよ。**奔走の最中に、私たちのエラー報告がpermission、token、microphoneといった単語を含むものを何でも捨てるように設定されているのを見つけました — ノイズを削るための試みが、たまたまそれらの単語に触れた本物のクラッシュを静かに飲み込んでいたのです。私たちはその包括的なフィルタを引きはがし、正確な拒否文字列(権限が拒否されたときにmacOSが発する特定のメッセージ)や、ERR_NETWORK_CHANGEDのような特定の一時的なネットワークコードに置き換えました。ノイズ削減とバグ隠しは、同じつまみを逆向きに回したものです。雰囲気でフィルタリングすれば、見る必要のあったものをフィルタリングして消してしまいます。
**すべての自動経路には、手動の脱出口が必要だ。**自動アップデートは本質的にベストエフォートです — ネットワークは不安定になり、インストールは失敗します。だから各失敗モードに人間向けのフォールバックを付けました: 手動の「Check for Updates」ボタン、指数バックオフのリトライ、再チェックのタイマー、そして — ユーザーの手動の試みが失敗した場合のためだけに用意した — 「アプリを削除してウェブサイトから再インストールしてください」という平易な言葉のメッセージ。自動経路は利便性であり、手動経路は保証です。
要点
- **不可逆な操作を囲むガードは、その操作そのものより危険だ。**条件付きの終了は、悪いアップデートがアプリを終了させるのを防ごうとして、代わりに終了するよりもしないよりも悪い状態を生み出しました。シャットダウンとインストールの経路は無条件かつ冪等であるべきで — 別のハンドラがあなたの足元でひっくり返せる可変フラグに、決して依存させてはいけません。
- **macOSでは、「ウィンドウがない」は「アプリがない」ではない。**あらゆる後始末のロジックは、ウィンドウのないプロセスが動き続けるプラットフォームを考慮しなければなりません。ハッピーパスだけでなく、失敗経路を、実際のOS上でテストしてください。
- **アップデートシステムを通して出荷する機能は、アップデートシステムを通してテストできない。**その非対称性こそ、自動アップデートが偏執的で、無条件で、手動で徹底的に検証されたコードに値する理由です。簡単なやり方で修正できるのは、それがすでに動くようになった後だけなのです。
これは、やがてGeekBye v2になった信頼性の取り組みの、最も初期の一章です。その道がどこへ通じたかは、バージョン2に本当に必要なもの(v2.0.0)と、その全体の流れを完璧なソフトウェアを出荷する解剖学でご覧ください。