LoginSignup
14
12

More than 1 year has passed since last update.

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

Posted at

概要

  • Microsoft Store で MSIXでパッケージングした WPFアプリをサイレントアップデートする方法。
  • StoreContext.TrySilentDownloadAndInstallStorePackageUpdatesAsync でアップデートを行い。
  • RegisterApplicationRestart で アップデート後の自動での再起動を行います。
  • 検証用のアプリは こちら になります。

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

WPFアプリのmsixによるweb配布、自動更新方法 が参考になります。

ストアでの配布方法はWPF アプリを Microsoft Store に申請・登録する(アプリ作成編)などを参考にしてください。

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

ストアで公開されたアプリをコードから更新する が参考になります。

ユーザーの操作を伴わないサイレントアップデート(Silent Update) については、StoreContext.TrySilentDownloadAndInstallStorePackageUpdatesAsync で行うことができます。

詳細な解説とサンプルコードが、Download and install package updates from the Store で公開されているので、参考にすると良いです。

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

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

アップデート後の再起動は、更新後にアプリを自動的に再起動するに記載されている通り、事前にRegisterApplicationRestart を呼び出しておき、その後に TrySilentDownloadAndInstallStorePackageUpdatesAsync でアップデートすることで、実現できます。

アプリ全体はこちら になりますが、重要なところだけ抜粋すると下記になります。

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

ProcessId を表示しているのは、Windows アプリ認定キット の rmlogotest.exe で、RegisterApplicationRestart 挙動をテストするためです。

rmlogotest.exe でのテストは、デスクトップアプリのリスタートマネージャー5(テストツール)で解説されていて、お世話になりました。 RegisterApplicationRestart の情報はほんとに少ない(T_T)。

uptime を表示しているのは、RegisterApplicationRestart function (winbase.h) の 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のウィンドウが邪魔だけど消せない。

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

14
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
12