Microsoft Store で 配布する WPF アプリのサイレントアップデート

## 概要

* Microsoft Store で MSIXでパッケージングした WPFアプリをサイレントアップデートする方法。
* StoreContext.TrySilentDownloadAndInstallStorePackageUpdatesAsync でアップデートを行い。
* RegisterApplicationRestart で アップデート後の自動での再起動を行います。
* 検証用のアプリは [こちら](https://github.com/lifework/WPF_SilentUpdate) になります。

## WPFアプリの MSIXへのパッケージング方法

[WPFアプリのmsixによるweb配布、自動更新方法](https://qiita.com/smi/items/17cdc56c08b8a5dab58e) が参考になります。

ストアでの配布方法は[WPF アプリを Microsoft Store に申請・登録する（アプリ作成編）](https://shinta0806be.ldblog.jp/archives/10462222.html)などを参考にしてください。


## MSIXアプリのアップデート方法

[ストアで公開されたアプリをコードから更新する](https://docs.microsoft.com/ja-jp/windows/msix/store-developer-package-update) が参考になります。

ユーザーの操作を伴わないサイレントアップデート(Silent Update) については、[StoreContext.TrySilentDownloadAndInstallStorePackageUpdatesAsync](https://docs.microsoft.com/ja-jp/uwp/api/windows.services.store.storecontext.trysilentdownloadandinstallstorepackageupdatesasync) で行うことができます。

詳細な解説とサンプルコードが、[Download and install package updates from the Store](https://github.com/MicrosoftDocs/windows-dev-docs/blob/docs/windows-apps-src/packaging/self-install-package-updates.md) で公開されているので、参考にすると良いです。

ただし、TrySilentDownloadAndInstallStorePackageUpdatesAsync は、アプリインストール後に、TrySilentDownloadAndInstallStorePackageUpdatesAsync の中で、強制的にアプリが終了してしまいます。

タスクトレイに常駐しているようなアプリをアップデートしたい場合、勝手に終了して欲しくないので、アップデート後に自動で再起動する必要があります。

アップデート後の再起動は、[更新後にアプリを自動的に再起動する](https://docs.microsoft.com/ja-jp/windows/msix/non-store-developer-updates#automatically-restarting-your-app-after-an-update)に記載されている通り、事前にRegisterApplicationRestart を呼び出しておき、その後に TrySilentDownloadAndInstallStorePackageUpdatesAsync でアップデートすることで、実現できます。

アプリ全体は[こちら](https://github.com/lifework/WPF_SilentUpdate) になりますが、重要なところだけ抜粋すると下記になります。

```csharp
namespace WPF_SilentUpdate
{
namespace WPF_SilentUpdate
{
    public partial class App : PrismApplication
    {
        protected override Window CreateShell()
        {
            return Container.Resolve<MainWindow>();
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
        }

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            uint hresult = RelaunchHelper.RegisterApplicationRestart();
            Console.WriteLine($"RelaunchHelper.RegisterApplicationRestart: {hresult}");

            StoreManager.CheckUpdateAndInstall();
        }
    }


    class StoreManager
    {
        public static void CheckUpdateAndInstall()
        {
            Task.Run(async () =>
            {
                while (true)
                {
                    try
                    {
                        Thread.Sleep(65 * 1000);
                        await SilentDownloadAndInstallUpdatesAsync();
                    }
                    catch (Exception e)
                    {
                        break;
                    }
                }
            });
        }

        public static async Task<bool> SilentDownloadAndInstallUpdatesAsync(bool preDownload = true)
        {

            StoreContext context = StoreContext.GetDefault();
            if (context.CanSilentlyDownloadStorePackageUpdates == false)
            {
                return false;
            }

            IReadOnlyList<StorePackageUpdate> storePackageUpdates = await context.GetAppAndOptionalStorePackageUpdatesAsync();
            if (storePackageUpdates.Count == 0)
            {
                return false;
            }

            if (preDownload)
            {
                StorePackageUpdateResult downloadResult = await context.TrySilentDownloadStorePackageUpdatesAsync(storePackageUpdates);
                if (downloadResult.OverallState != StorePackageUpdateState.Completed)
                {
                    return false;
                }
            }

            StorePackageUpdateResult installResult = await context.TrySilentDownloadAndInstallStorePackageUpdatesAsync(storePackageUpdates);
            if (installResult.OverallState != StorePackageUpdateState.Completed)
            {
                return false;
            }

            return true;
        }
    }

    // アプリケーションの再起動の登録
    // https://docs.microsoft.com/ja-jp/windows/win32/recovery/registering-for-application-restart
    //
    // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerapplicationrestart
    // Remarks に記載のことが大事。
    //
    // デスクトップアプリのリスタートマネージャー2(アプリ停止のためのウィンドウメッセージ)
    // https://nishy-software.com/ja/dev-sw/app-restart-manager-2/
    class RelaunchHelper
    {
        // 一般的な HRESULT 値
        // https://docs.microsoft.com/ja-jp/windows/win32/seccrypto/common-hresult-values?redirectedfrom=MSDN
        public static uint RegisterApplicationRestart()
        {          
            return RelaunchHelper.RegisterApplicationRestart(null, RelaunchHelper.RestartFlags.NONE);
        }

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
        internal static extern uint RegisterApplicationRestart(string? pwzCommandLine, RestartFlags dwFlags);

        [Flags]
        internal enum RestartFlags
        {
            NONE = 0,
            RESTART_NO_CRASH = 1,
            RESTART_NO_HANG = 2,
            RESTART_NO_PATCH = 4,
            RESTART_NO_REBOOT = 8
        }
    }
}

```

## 検証用アプリについて

実行すると下記画面のように、WPFアプリの ProcessId、バージョン、起動後経過秒数、アプリのインストール先フォルダが表示されます。

![スクリーンショット 2022-09-07 103731.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2709043/a8ae15b9-34ab-1d7b-20da-2647c0e0a411.png)

ProcessId を表示しているのは、[Windows アプリ認定キット](https://docs.microsoft.com/ja-jp/windows/uwp/debug-test-perf/windows-app-certification-kit) の rmlogotest.exe で、RegisterApplicationRestart 挙動をテストするためです。

rmlogotest.exe でのテストは、[デスクトップアプリのリスタートマネージャー5(テストツール)](https://nishy-software.com/ja/dev-sw/app-restart-manager-5/)で解説されていて、お世話になりました。 RegisterApplicationRestart の情報はほんとに少ない(T_T)。

uptime を表示しているのは、[RegisterApplicationRestart function (winbase.h)](https://docs.microsoft.com/ja-JP/windows/win32/api/winbase/nf-winbase-registerapplicationrestart) の Remarks に下記のように記載されているためです。

```
To prevent cyclical restarts, 
the system will only restart the application 
if it has been running for a minimum of 60 seconds.
```

「再移動する」ボタンを押すと、0除算でアプリがCRASHし、RegisterApplicationRestart によって再起動します。

## RegisterApplicationRestart 以外で試したこと

### TrySilentDownloadAndInstallStorePackageUpdatesAsync の前後に再起動

直前に再起動しておくとしても、アップデート前のバイナリで再起動してしまい意味がない。

直後は、TrySilentDownloadAndInstallStorePackageUpdatesAsync の中で終了するので、直後のコードが実行されるかはタイミングなのか不確定で、うまく動かない。

### ラッパーとして UWPを作り、windows.updateTask で起動

起動用にUWPアプリを作り、UpdateTask (IBackgroundTask) の中で、FullTrustProcessLauncher で WPFアプリ起動するようにしたら、アップデート後に起動しましたが、UWPのウィンドウが邪魔だけど消せない。


最終的に [かずき](https://twitter.com/okazuki)さんの Twitter で紹介されている C# Discordサーバーで質問させていただき、(かずきさんから) registerapplicationrestart の情報を教えていただき実現できたのでした。。。