Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ GUI 常駐中に GitHub Releases を自動で確認し (起動 30 秒後と 1

置き換えた一式が起動できるかどうかまでは、SHA-256 では分からない。照合が保証するのは「公開されている配布物そのものを取れたこと」までである。そこでヘルパは、入れ替えた後に起動し直した App を数秒見る。そこで落ちていれば退避しておいた一式へ戻し、取得した zip を捨てて現行版で開き直す。戻せなかった場合だけは、壊れた一式を起動し直そうとせず、`.old` を手で戻す先を伝えて終える。

戻したときは、その配布物を覚えておく。覚えないと、開き直した App が次の確認で同じものをまた取得し、次の起動でまた戻すことになる。起動のたびに数十〜百数十 MB を空振りする。覚えるのはタグと SHA-256 の組なので、**同じタグへ直した一式が上げ直されれば取りに行く。** その間、設定カードの「本体の更新」欄には取得しない旨を出す。

起動直後に終わること自体は異常ではない。既に別の App が動いていた場合と、取得しておいた更新をヘルパへ渡した場合は、どちらも正常な終了として扱われる。戻す判断に使うのは、終了コードが 0 以外だった場合だけである。

見ているのはプロセスが落ちたかどうかである。起動の途中で例外が出た場合はアプリごと落ちるので (下記)、ここで拾える。ウィンドウを出した後の描画で失敗するような、起動を抜けてから起こる形は拾えない。
Expand Down
23 changes: 23 additions & 0 deletions src/VRCToolsDataSync.App/Services/UpdateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,16 @@ public void MarkNotified(ReleaseInfo release)
/// <summary>取得済みで置き換え待ちの記録。照合はせず、表示にだけ使う。</summary>
public StagedMetadata? Staged => _stage.TryLoadMetadata();

/// <summary>
/// この版は、一度置き換えて起動できなかったものか (issue #76)。
/// <para>
/// 画面に出す文言を分けるために使う。取得しないことを伝えないと、
/// 「新しい版が出ています」と出たまま、いつまでも取得済みにならない。
/// </para>
/// </summary>
public bool IsKnownStartFailure(ReleaseInfo release)
=> _stage.IsKnownStartFailure(release.Tag, release.Asset?.DigestHex);

/// <summary>
/// 置き換えまわりの後始末を行い、済んだことを画面へ伝える (issue #45 第 3 段階)。
/// <para>
Expand Down Expand Up @@ -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 の印も見る。プレリリース
Expand Down
15 changes: 13 additions & 2 deletions src/VRCToolsDataSync.App/ViewModels/MainPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand All @@ -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} が出ています。ウィンドウの設定から開けます。");
}
// 画面と通知に出せた後で覚える。出せなかった版まで覚えると、
// 利用者が一度も見ないまま以後の確認で抑止される。
Expand Down
15 changes: 15 additions & 0 deletions src/VRCToolsDataSync.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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;
Expand Down
62 changes: 62 additions & 0 deletions src/VRCToolsDataSync.Core/Infra/UpdateStage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,68 @@ private Exception ClassifyExtractFailure(Exception ex)
$"{failures} 回続けて展開できなかったため配布物の問題として扱う: {ZipPath}", ex);
}

/// <summary>
/// 起動できなかった配布物の置き場所 (issue #76)。
/// <para>
/// 記録 (<see cref="MetadataPath"/>) とは別のファイルに置く。取得を捨てても
/// 残す必要があるためで、<see cref="Discard"/> はこれに触らない。
/// </para>
/// </summary>
private string StartFailurePath => Path.Combine(Directory, "start-failure");

/// <summary>
/// 起動できなかった配布物を覚える (issue #76)。
/// <para>
/// 置き換えた一式が起動できず、退避へ戻した後に呼ぶ。覚えないと、旧版で
/// 開き直した App が次の確認で同じ配布物をまた取得し、次の起動でまた適用
/// しに行く。起動のたびに数十〜百数十 MB を空振りすることになる。
/// </para>
/// <para>
/// タグと digest の組で覚える。同じタグへ配布物を上げ直す運用がある
/// (release.yml の <c>--clobber</c>)。直した一式が同じタグで公開された
/// 場合は digest が変わるので、そちらは覚えた記録に当たらず取りに行ける。
/// </para>
/// </summary>
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);
}
}

/// <summary>
/// 覚えている「起動できなかった配布物」と同じか (issue #76)。
/// <para>
/// 読めない場合は false を返す。読めないことを理由に更新を止めるほうが損が
/// 大きい。空振りは 1 回で済むが、止めると新しい版が入らなくなる。
/// </para>
/// </summary>
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;
}
}

/// <summary>同じ ZIP の展開をあきらめるまでの回数。</summary>
private const int MaxExtractAttempts = 3;

Expand Down
37 changes: 37 additions & 0 deletions tests/VRCToolsDataSync.Core.Tests/UpdateStageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading