diff --git a/docs/update.md b/docs/update.md
index c9b7f7f..9aa6bb0 100644
--- a/docs/update.md
+++ b/docs/update.md
@@ -15,6 +15,8 @@ GUI 常駐中に GitHub Releases を自動で確認し (起動 30 秒後と 1
置き換えた一式が起動できるかどうかまでは、SHA-256 では分からない。照合が保証するのは「公開されている配布物そのものを取れたこと」までである。そこでヘルパは、入れ替えた後に起動し直した App を数秒見る。そこで落ちていれば退避しておいた一式へ戻し、取得した zip を捨てて現行版で開き直す。戻せなかった場合だけは、壊れた一式を起動し直そうとせず、`.old` を手で戻す先を伝えて終える。
+戻したときは、その配布物を覚えておく。覚えないと、開き直した App が次の確認で同じものをまた取得し、次の起動でまた戻すことになる。起動のたびに数十〜百数十 MB を空振りする。覚えるのはタグと SHA-256 の組なので、**同じタグへ直した一式が上げ直されれば取りに行く。** その間、設定カードの「本体の更新」欄には取得しない旨を出す。
+
起動直後に終わること自体は異常ではない。既に別の App が動いていた場合と、取得しておいた更新をヘルパへ渡した場合は、どちらも正常な終了として扱われる。戻す判断に使うのは、終了コードが 0 以外だった場合だけである。
見ているのはプロセスが落ちたかどうかである。起動の途中で例外が出た場合はアプリごと落ちるので (下記)、ここで拾える。ウィンドウを出した後の描画で失敗するような、起動を抜けてから起こる形は拾えない。
diff --git a/src/VRCToolsDataSync.App/Services/UpdateManager.cs b/src/VRCToolsDataSync.App/Services/UpdateManager.cs
index 69eb777..6f7ffb1 100644
--- a/src/VRCToolsDataSync.App/Services/UpdateManager.cs
+++ b/src/VRCToolsDataSync.App/Services/UpdateManager.cs
@@ -186,6 +186,16 @@ public void MarkNotified(ReleaseInfo release)
/// 取得済みで置き換え待ちの記録。照合はせず、表示にだけ使う。
public StagedMetadata? Staged => _stage.TryLoadMetadata();
+ ///
+ /// この版は、一度置き換えて起動できなかったものか (issue #76)。
+ ///
+ /// 画面に出す文言を分けるために使う。取得しないことを伝えないと、
+ /// 「新しい版が出ています」と出たまま、いつまでも取得済みにならない。
+ ///
+ ///
+ public bool IsKnownStartFailure(ReleaseInfo release)
+ => _stage.IsKnownStartFailure(release.Tag, release.Asset?.DigestHex);
+
///
/// 置き換えまわりの後始末を行い、済んだことを画面へ伝える (issue #45 第 3 段階)。
///
@@ -376,6 +386,19 @@ private async Task RunDownloadAsync(ReleaseInfo release, UpdateChannel channel)
if (!MatchesSavedChannel(channel)) return;
if (!release.IsInChannel(channel)) return;
+ // 一度置き換えて起動できなかった配布物は取りに行かない (issue #76)。
+ // 取りに行くと、次の起動でまた適用して、また戻すことになる。起動の
+ // たびに数十〜百数十 MB を空振りする。
+ //
+ // 同じタグへ直した一式が上げ直された場合は digest が変わるので、
+ // そちらは覚えた記録に当たらず取りに行ける。
+ if (_stage.IsKnownStartFailure(release.Tag, asset.DigestHex))
+ {
+ _logger.LogInformation(
+ "{Tag} は置き換えても起動できなかったため取得しない", release.Tag);
+ return;
+ }
+
// タグだけでなく digest と大きさも見る。同じタグへ配布物を上げ直す
// 運用があり (release.yml の --clobber)、タグだけで済ませると
// 差し替え前のものを適用し続ける。stable の印も見る。プレリリース
diff --git a/src/VRCToolsDataSync.App/ViewModels/MainPageViewModel.cs b/src/VRCToolsDataSync.App/ViewModels/MainPageViewModel.cs
index d5a15fe..a31d05f 100644
--- a/src/VRCToolsDataSync.App/ViewModels/MainPageViewModel.cs
+++ b/src/VRCToolsDataSync.App/ViewModels/MainPageViewModel.cs
@@ -735,8 +735,15 @@ private void HandleUpdateCheckCompleted(UpdateCheckResult result, UpdateChannel
var current = _updates.CurrentVersion;
+ // 一度置き換えて起動できなかった版は取りに行かない (issue #76)。
+ // そのまま「出ています」とだけ出すと、いつまでも取得済みにならない
+ // 理由が分からない。
+ var startFailed = result.Release is { } found && _updates.IsKnownStartFailure(found);
+
UpdateStatus = result.Outcome switch
{
+ UpdateCheckOutcome.Available when startFailed =>
+ $"新しい版 {result.Release!.Tag} が出ていますが、この PC では起動できなかったため取得しません",
UpdateCheckOutcome.Available =>
$"新しい版 {result.Release!.Tag} が出ています (実行中: {current})",
UpdateCheckOutcome.UpToDate when result.Release is not null =>
@@ -756,14 +763,18 @@ private void HandleUpdateCheckCompleted(UpdateCheckResult result, UpdateChannel
if (result.Outcome == UpdateCheckOutcome.Available && result.Release is { } release)
{
- AppendLog($"新しい版 {release.Tag} が出ています (実行中: {current})");
+ AppendLog(startFailed
+ ? $"新しい版 {release.Tag} が出ていますが、この PC では起動できなかったため取得しません"
+ : $"新しい版 {release.Tag} が出ています (実行中: {current})");
if (!manual)
{
// 自動の確認は背後で走るので、画面を見ていない利用者にも届くよう
// バルーンを出す。押した人は画面の前に居るので出さない。
ToastRequested?.Invoke(
"VRCToolsDataSync の更新",
- $"新しい版 {release.Tag} が出ています。ウィンドウの設定から開けます。");
+ startFailed
+ ? $"新しい版 {release.Tag} は、置き換えても起動できなかったため取得しません。"
+ : $"新しい版 {release.Tag} が出ています。ウィンドウの設定から開けます。");
}
// 画面と通知に出せた後で覚える。出せなかった版まで覚えると、
// 利用者が一度も見ないまま以後の確認で抑止される。
diff --git a/src/VRCToolsDataSync.Cli/Program.cs b/src/VRCToolsDataSync.Cli/Program.cs
index 8f7c001..858b0ab 100644
--- a/src/VRCToolsDataSync.Cli/Program.cs
+++ b/src/VRCToolsDataSync.Cli/Program.cs
@@ -1064,6 +1064,12 @@ static int RollBackFailedStart(
return 7;
}
+ // 何を捨てるのかを、捨てる前に控えておく (issue #76)。捨てた後では読めない。
+ var stage = new UpdateStage(UpdateStage.DirectoryFor(target), logger);
+ StagedMetadata? failed = null;
+ try { failed = stage.TryLoadMetadata(); }
+ catch (Exception ex) { log(() => logger.LogWarning(ex, "起動できなかった配布物の記録を読めなかった")); }
+
// 取得を捨てる。残すと、開き直した旧版がまた同じ一式を適用しに行き、
// また起動できずに戻す、を繰り返す。
if (!TryDiscardStaged(target, logger, out var stageDirectory))
@@ -1075,6 +1081,15 @@ static int RollBackFailedStart(
return 8;
}
+ // 起動できなかった配布物を覚える (issue #76)。捨てただけでは、開き直した
+ // 旧版が次の確認で同じものをまた取得し、次の起動でまた適用しに行く。
+ // 起動のたびに数十〜百数十 MB を空振りすることになる。
+ if (failed is not null)
+ {
+ try { stage.RecordStartFailure(failed.Tag, failed.DigestHex); }
+ catch (Exception ex) { log(() => logger.LogWarning(ex, "起動できなかった配布物を覚えられなかった")); }
+ }
+
singleInstance?.Dispose();
TryRelaunchApp(target, logger, relaunchMinimized).Process?.Dispose();
return 10;
diff --git a/src/VRCToolsDataSync.Core/Infra/UpdateStage.cs b/src/VRCToolsDataSync.Core/Infra/UpdateStage.cs
index f1395a9..627ca2f 100644
--- a/src/VRCToolsDataSync.Core/Infra/UpdateStage.cs
+++ b/src/VRCToolsDataSync.Core/Infra/UpdateStage.cs
@@ -826,6 +826,68 @@ private Exception ClassifyExtractFailure(Exception ex)
$"{failures} 回続けて展開できなかったため配布物の問題として扱う: {ZipPath}", ex);
}
+ ///
+ /// 起動できなかった配布物の置き場所 (issue #76)。
+ ///
+ /// 記録 () とは別のファイルに置く。取得を捨てても
+ /// 残す必要があるためで、 はこれに触らない。
+ ///
+ ///
+ private string StartFailurePath => Path.Combine(Directory, "start-failure");
+
+ ///
+ /// 起動できなかった配布物を覚える (issue #76)。
+ ///
+ /// 置き換えた一式が起動できず、退避へ戻した後に呼ぶ。覚えないと、旧版で
+ /// 開き直した App が次の確認で同じ配布物をまた取得し、次の起動でまた適用
+ /// しに行く。起動のたびに数十〜百数十 MB を空振りすることになる。
+ ///
+ ///
+ /// タグと digest の組で覚える。同じタグへ配布物を上げ直す運用がある
+ /// (release.yml の --clobber)。直した一式が同じタグで公開された
+ /// 場合は digest が変わるので、そちらは覚えた記録に当たらず取りに行ける。
+ ///
+ ///
+ public void RecordStartFailure(string tag, string digestHex)
+ {
+ try
+ {
+ System.IO.Directory.CreateDirectory(Directory);
+ File.WriteAllText(StartFailurePath, $"{digestHex} {tag}");
+ _logger.LogInformation("起動できなかった配布物として覚える: {Tag}", tag);
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ // 覚えられなくても、失うのは次の確認 1 回ぶんの取得である。
+ _logger.LogWarning(ex, "起動できなかった配布物を覚えられなかった: {Path}", StartFailurePath);
+ }
+ }
+
+ ///
+ /// 覚えている「起動できなかった配布物」と同じか (issue #76)。
+ ///
+ /// 読めない場合は false を返す。読めないことを理由に更新を止めるほうが損が
+ /// 大きい。空振りは 1 回で済むが、止めると新しい版が入らなくなる。
+ ///
+ ///
+ public bool IsKnownStartFailure(string tag, string? digestHex)
+ {
+ if (string.IsNullOrEmpty(digestHex)) return false;
+
+ try
+ {
+ var parts = File.ReadAllText(StartFailurePath).Split(' ', 2);
+ return parts.Length == 2
+ && string.Equals(parts[0], digestHex, StringComparison.Ordinal)
+ && string.Equals(parts[1], tag, StringComparison.Ordinal);
+ }
+ catch (Exception)
+ {
+ // 覚えていない (ファイルが無い) 場合もここへ来る。
+ return false;
+ }
+ }
+
/// 同じ ZIP の展開をあきらめるまでの回数。
private const int MaxExtractAttempts = 3;
diff --git a/tests/VRCToolsDataSync.Core.Tests/UpdateStageTests.cs b/tests/VRCToolsDataSync.Core.Tests/UpdateStageTests.cs
index f7959a0..a70d054 100644
--- a/tests/VRCToolsDataSync.Core.Tests/UpdateStageTests.cs
+++ b/tests/VRCToolsDataSync.Core.Tests/UpdateStageTests.cs
@@ -372,6 +372,43 @@ public void AppliesWhenInstalledVersionIsOlder()
Assert.Equal("0.0.10", staged!.Tag);
}
+ [Fact(DisplayName = "起動できなかった配布物は、タグと digest の組で覚える")]
+ public void RemembersTheDistributionThatCouldNotStart()
+ {
+ var stage = CreateStage();
+ Directory.CreateDirectory(_directory);
+
+ // 覚える前は何にも当たらない。
+ Assert.False(stage.IsKnownStartFailure("0.0.10", "abc123"));
+
+ stage.RecordStartFailure("0.0.10", "abc123");
+
+ Assert.True(stage.IsKnownStartFailure("0.0.10", "abc123"));
+
+ // 同じタグへ直した一式が上げ直されると digest が変わる。そちらは
+ // 取りに行けなければならない (release.yml の --clobber)。
+ Assert.False(stage.IsKnownStartFailure("0.0.10", "def456"));
+
+ // 別のタグも当然に通す。
+ Assert.False(stage.IsKnownStartFailure("0.0.11", "abc123"));
+
+ // digest を持たない候補は判断しようがない。止めるより通す。
+ Assert.False(stage.IsKnownStartFailure("0.0.10", null));
+ }
+
+ [Fact(DisplayName = "起動できなかった記録は、取得を捨てても残る")]
+ public void TheStartFailureRecordSurvivesDiscard()
+ {
+ var stage = StageWith("0.0.10");
+ stage.RecordStartFailure("0.0.10", "abc123");
+
+ // 戻す側は「取得を捨てる」と「覚える」を続けて行う。捨てるほうが
+ // 覚えたものまで消すと、次の確認でまた取りに行ってしまう。
+ Assert.True(stage.Discard());
+
+ Assert.True(stage.IsKnownStartFailure("0.0.10", "abc123"));
+ }
+
[Fact(DisplayName = "同じパスの項目を持つ ZIP は展開せずに断る")]
public void RefusesArchiveWithDuplicateEntries()
{