feat(update): 置き換えた App が起動できなければ退避した一式へ戻す - #75
Conversation
更新の適用は、展開した一式を正規の位置へ入れ替えて App を起動し直すところで 終わっていた。起動し直した App が立ち上がったかどうかは見ていない。 digest が保証するのは「公開されている配布物そのものを取れたこと」までであって、 中身が起動できることではない。起動には hostfxr.dll などのランタイムも要るが、 必須ファイルを名前で数え上げる方針では一覧が数百件になり、増えるたびに追随 しなければならない。そもそも「起動できるか」を名前の一覧で近似すること自体に 無理がある。 ヘルパが起こした App を数秒見る形にした。落ちていれば退避しておいた一式へ 戻し、取得を捨てて現行版で開き直す。 早く終わること自体は異常ではない。既に別の App が動いていた場合と、取得して おいた更新をヘルパへ渡した場合は、どちらも 0 で終わる。戻す判断に使うのは 終了コードが 0 以外だった場合だけにした。戻す判断を誤ると、動く更新を戻した うえに取得まで捨てることになり、次の確認まで新しい版に上がれない。 Core には RollbackApplied を足した。Apply の中の巻き戻しはその場で退避した ものだけを対象にするので、「入れ替えは通ったが、その後で駄目だと分かった」 ときの戻し口が無かった。戻す前に退避がすべてそろっているかを見る。片方しか 残っていない状態で戻すと、旧 app と新 cli が混ざった一式ができる。digest も 実行ファイルの存在も通るうえ、どちらの版とも言えないものが正規の位置に居座る。 戻せなかった場合は UpdateRollbackException と同じ扱いにし、取得を捨てず、 壊れた一式を起動し直そうともしない。終了コードは、戻して開き直した場合を 10、 戻せなかった場合を既存の 7 とした。 TryRelaunchApp は起こしたプロセスも返すようにした。UseShellExecute の起動は 既に動いているものが使い回された場合に null を返しうるので、「起こせたか」と 「掴めたか」は分けてある。掴めなかった場合は生死を見る手が無いため、そのまま 任せる。 Refs #53
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 736d2136a4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex のレビューで挙がった 3 点。 ## ランチャーも戻す Apply は app と cli の入れ替えの後にルートの .cmd も新版で上書きするが、 戻すのは Parts の 2 つだけだった。ランチャーが起動する先や渡す引数は版に よって変わりうるので、旧版の一式に新版のランチャーが組み合わさる。ヘルパ からの直接の起動は通っても、次の通常の起動で食い違う。 差し替える前に .cmd.old へ退避し、戻すときに書き戻す。退避も書き戻しも 失敗は警告に留める。ここは元からそういう段であり、ランチャーは相対参照で app を起動するため版が食い違っても大抵は動く。app と cli を戻せている以上、 ここで「戻せなかった」と伝えるほうが実態から遠い。DiscardPrevious も この退避を拾うようにした。 ## 起こせなかった場合も戻す 公開された実行ファイルの形が不正だった場合や、置き換えた直後にウイルス対策 ソフトが隔離した場合は、Process.Start そのものが例外になる。置き換えたまま 残すと、利用者が手で起動しても同じ理由で失敗し、App を開く手立てが無くなる。 生死を見るまでもないので、そのまま戻す。 ## 抑止を掴めなければ戻さない TryHoldSingleInstance が null を返した結果を無視していた。別の App が動いて おり、その App は置き換えた一式で動いている。起動できない一式ではなかった ということなので、戻す理由のほうが怪しい。 掴まれたまま戻しに入ると、なお悪い。戻しは正規の位置を消してから退避を戻す ため、消す途中で失敗すると置き換えた一式まで欠ける。正規の位置には触らずに 引き下がる。 Refs #53
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02c0c64c74
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex のレビュー 2 巡目。 ## 退避の中身を確かめてから戻す 戻す前の点検がディレクトリの存在だけだった。ウイルス対策ソフトや手作業で app.old の中の exe や本体 DLL だけが失われていても、そろっていると判定する。 そのまま戻すと、置き換えた一式を消して欠けた旧版を正規の位置へ移すことに なる。しかも呼び出し側はこれを「戻せた」と受け取って取得しておいた ZIP まで 捨てるので、旧版も新版も起動できず、展開し直す材料も残らない。 ValidateLayout と LooksComplete が見るのと同じ組 (実行ファイルと本体 アセンブリ) を、それぞれの退避についても見る。断って新版を留めれば、 少なくとも ZIP は残り、展開し直せる。 ## 退避できなければランチャーを差し替えない 前回の退避 (.cmd.old) が掴まれている場合、今回の退避に失敗しても差し替えを 続けていた。戻すときに前々版のランチャーを書き戻すか、退避が無ければ旧版の app / cli に新版のランチャーが残る。 前回の退避を先に消し、今回の退避に失敗した場合は差し替えないようにした。 差し替えないほうの損は小さい。ランチャーは相対参照で app を起動するので、 旧ランチャーのままでも新しい app は起動できる。この段が元から警告で済むのと 同じ理由である。 Refs #53
観察の 5 秒間、ヘルパは適用の mutex を握ったままだった。起こした App は起動の 先頭 (取得しておいた更新の照合) でこのロックを待って止まる。観察はその止まって いる姿を「生きている」と読み、ヘルパが終わってロックが空いた後に WinUI の 初期化が落ちても、誰も戻さない。 捕まえられるのは、App が照合へ到達する前に落ちる場合だけになっていた。apphost がランタイムを読み込めない場合はそこで落ちるので見えるが、Windows App SDK の DLL を欠くような、managed のコードが動き出した後の失敗は見えない。issue #53 が 挙げている「起動には他にも hostfxr.dll などのランタイムが要る」の後半が抜ける。 起こす前にロックを返し、戻すときに取り直すようにした。返しても二重適用には ならない。起こした App の照合は、インストール先に入っている版と突き合わせて 素通りする (#52)。置き換えは済んでいるので、入っている版は取得しておいた版と 同じである。 取り直せない場合は正規の位置に触らない。返していた間に別のプロセスが適用へ 入っている可能性があり、相手の置き換えと重なると、どちらの版とも言えないものが 残る。上限は短くてよい。起こした App は照合の間しか握らず、それは既に終わって いる (終わったから観察が落ちたと読んだ)。 適用の mutex は ApplyLockHolder で持つようにした。返すのと取り直すのを何度でも 呼べるようにするためで、掴み方と上限はこれまでと同じである。 Refs #53
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2b15d1e72b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
適用のロックを観察の間だけ返すようにしたことで、起こした App が CleanUpAfterSuccessfulStart まで進めるようになった。後始末は DiscardPrevious で退避 (.old) と取得を消すので、その後で App が落ちると、戻そうとしても退避が 無く、RollbackApplied が「戻せない」と投げる。利用者には「インストール先が 壊れた可能性があります」と出るが、実態は違う。 後始末はウィンドウを立てられた後にしか走らない。退避が消えているということは、 置き換えた一式はそこまで動いたということである。起動は成り立っており、その後で 落ちた。戻す相手が違う。 戻しへ入る前に退避が残っているかを見て、無ければ正規の位置に触らずに引き下がる ようにした。判定は適用のロックを取り直した後に行う。後始末も同じロックの下で 走るので、その時点で走り終えているか、まだ始まっていないかのどちらかになる。 あわせて docs の記述を直した。見ているのはプロセスが落ちたかどうかであり、 落ちずに止まる形の失敗は捕まらない。 Refs #53
issue #53 の対応。issue 本文の案(ヘルパが起こした App を数秒見て、落ちていれば
.oldを戻す)をそのまま採った。何が起きていたか
更新の適用は、展開した一式を正規の位置へ入れ替えて App を起動し直すところで終わっていた。起動し直した App が立ち上がったかどうかは見ていない。
立ち上がらない一式が入ると、利用者は App を開けなくなる。退避した
.oldは残っているが(後始末はウィンドウを立てられた後に行うため)、戻す経路が無い。手でapp.oldをappへ戻す必要がある。#48では正規の位置へ触る前にapp/cliのディレクトリ・実行ファイル・本体アセンブリの存在と、展開したappの埋め込み版を確かめるようにした。ただし配布は自己完結・複数ファイルであり、起動にはhostfxr.dllなどのランタイムも要る。必須ファイルを名前で数え上げていく方針だと一覧は数百件になり、増えるたびに追随しなければならない。そもそも「起動できるか」を名前の一覧で近似していること自体に無理がある。観察できる範囲
見ているのはプロセスが落ちたかどうかである。 自己完結のランタイムを欠いた一式のように apphost が落ちる形は捕まえられる。落ちずに止まる形(
UnhandledExceptionがe.Handled = trueにしているため、OnLaunchedの途中で投げてもプロセスが残る)は捕まらない。そちらは #77 へ切り出した。この PR の前から成立していた形であり、この PR が作ったものではない。変更内容
Cli
ヘルパが起こした App を 5 秒見る。落ちていれば次の順で戻す。
.old) が残っているかを見るRollbackAppliedで退避した一式へ戻す起こす前に適用のロックを返す。 握ったままだと、起こした App は起動の先頭(取得しておいた更新の照合)でこのロックを待って止まる。観察はその止まっている姿を「生きている」と読み、ロックが空いた後で落ちても誰も戻さない。返しても二重適用にはならない。起こした App の照合は、インストール先に入っている版と突き合わせて素通りする(#52 / PR #74)。
適用の mutex は
ApplyLockHolderで持つ。返すのと取り直すのを何度でも呼べるようにするためで、掴み方と上限(65 分)はこれまでと同じである。取り直せない場合は正規の位置に触らない。返していた間に別のプロセスが適用へ入っている可能性があり、相手の置き換えと重なると、どちらの版とも言えないものが残る。退避が消えていれば戻さない。 退避を消すのは後始末 (
DiscardPrevious) だけで、後始末はウィンドウを立てられた後にしか走らない。消えているということは、置き換えた一式はそこまで動いたということである。起動は成り立っており、その後で落ちた。戻す相手が違う。早く終わること自体は異常ではない。 既に別の App が動いていた場合(
Program.Mainの抑止)と、取得しておいた更新をヘルパへ渡した場合(TryHandOverToStaged→Environment.Exit(0))は、どちらも終了コード 0 で終わる。戻す判断に使うのは 0 以外で終わった場合だけ にした。戻す判断を誤ると、動く更新を戻したうえに取得まで捨てることになり、次の確認まで新しい版に上がれない。起こせなかった場合も戻す。 公開された実行ファイルの形が不正だった場合や、置き換えた直後にウイルス対策ソフトが隔離した場合は
Process.Startそのものが例外になる。置き換えたまま残すと、利用者が手で起動しても同じ理由で失敗し、App を開く手立てが無くなる。抑止を掴めなければ戻さない。 別の App が動いており、その App は置き換えた一式で動いている。起動できない一式ではなかったということなので、戻す理由のほうが怪しい。掴まれたまま戻しに入ると、正規の位置を消す途中で失敗して置き換えた一式まで欠ける。
TryRelaunchAppは起こしたプロセスも返す。UseShellExecuteの起動は、既に動いているものが使い回された場合にnullを返しうる。「起こせたか」と「掴めたか」は分けてあり、掴めなかった場合は生死を見る手が無いのでそのまま任せる。取得を捨てる処理は 2 か所から呼ぶようになったので
TryDiscardStagedへ寄せた。中身は元のままである。Core
UpdateInstaller.RollbackAppliedを足した。Applyの中で使う巻き戻し(RollbackSwapped)はその場で退避したものだけを対象にするため、「入れ替えは通ったが、その後で駄目だと分かった」ときの戻し口が無かった。戻す前に、退避 (
.old) がそろっているかを見る。ディレクトリの存在だけでは足りない。 中身が欠けた退避を正規の位置へ置くと、戻したつもりの側も起動できない。しかも呼び出し側はこれを「戻せた」と受け取って取得しておいた ZIP まで捨てるので、旧版も新版も起動できず、展開し直す材料も残らない。ValidateLayout/LooksCompleteが見るのと同じ組(実行ファイルと本体アセンブリ)を、それぞれの退避についても見る。断って新版を留めれば、少なくとも ZIP は残る。ランチャーも戻す。
Applyはapp/cliの入れ替えの後にルートの.cmdも新版で上書きする。起動する先や渡す引数は版によって変わりうるので、app/cliだけを戻すと旧版の一式に新版のランチャーが組み合わさる。差し替える前に.cmd.oldへ退避し、戻すときに書き戻す。DiscardPreviousもこの退避を拾う。退避できない場合は差し替えない。差し替えたうえで戻せないと、旧版の
app/cliに新版のランチャーが残る。前回の退避が掴まれたまま残っている場合に備えて、退避の前にそれを消す(残したまま今回の退避に失敗すると、戻すときに前々版のランチャーを書き戻すことになる)。UpdateInstaller.HasBackupsを足した。戻せるかどうかの目安であると同時に、「起動が成り立ったか」の手掛かりでもある。終了コード
self-update apply(ヘルプ非表示)に 10 を足した。戻せなかった場合は
UpdateRollbackExceptionと同じ扱いにして 7 を返す。取得を捨てず、壊れた一式を起動し直そうともしない。検証
UpdateRollbackExceptionで断ることApplyがランチャーを退避し、DiscardPreviousがそれを消すことapp\から本体 DLL を抜いた ZIP)で、戻して旧版が開き直ること残るもの
Refs #53, #48, #45, #76, #77