.NET クライアントプログラムの自動更新
日常の開発で作成したクライアントプログラムを複数のホストに配置する場合、アップグレードが必要になると一台ずつ更新するのは非常に手間がかかります。そんなときに役立つのが、本稿で紹介する.NET クライアントプログラムの自動更新技術です。
本稿で説明する自動更新技術は、主にオープンソースの GeneralUpdate コンポーネントを使用します。これは Winform/WPF/ConsoleApp などのアプリケーションの自動更新に利用できます。
GeneralUpdate コンポーネントは、Microsoft MVP が開発・保守を行っており、リポジトリのアドレスとスクリーンショットは以下の通りです。作者が提供する使用ドキュメントや動画はやや簡略化されており、バージョンによって互換性の問題があることも十分に説明されていないため、初めてこのコンポーネントに触れる開発者は混乱するかもしれません。筆者は自身のプロジェクトでの実際の使用状況をもとに、このコンポーネントの使い方をより詳しく紹介します。
GitHub:https://github.com/WELL-E/AutoUpdater

Gitee:https://gitee.com/Juster-zhu/GeneralUpdate

自動更新フローチャート

元の図の説明が不明瞭だったため、上図では赤色の文字で新しい説明を追加しています。上図では3つのコンポーネントまたはサービスの相互作用のように見えますが、正確には4つです:
- クライアントプログラムバージョン検証サービス(必須ではない):このサービスは少なくとも2つのAPIを提供します。1つはクライアントプログラムに最新バージョンがあるかどうかを判断するAPI、もう1つは現在のクライアントのすべての更新バージョンを取得するAPIです。場合によっては検証サービスを個別に作成・配置したくないこともあり、その場合はデータベースで代用できます。クライアントプログラムは直接データベースを照会し、現在のプログラムのすべての更新バージョンを判断・取得します。
- クライアントプログラム(必須):自動更新機能を必要とする業務プログラムです。リフレクションを使用して自身のアセンブリのバージョン番号を取得し、サーバーやデータベースと比較して新しいバージョンがあるかどうかを判断します。
- 更新コンポーネント(必須):更新コンポーネントは実際には独立した実行可能ファイルであり、クライアントプログラムと同じディレクトリに配置されます。このコンポーネントの主な役割は、指定されたパスからクライアントプログラムのすべての更新圧縮パッケージをダウンロードし、1つずつ解凍して、クライアントプログラムのバージョンごとのアップグレードを実現することです。クライアントがサーバーから更新ファイルのパスを取得したら、プロセス間通信を介して更新コンポーネントを起動します。更新コンポーネントが起動したら、クライアントプログラムを閉じて、一部のファイルが占有されて更新に失敗するのを防ぎます。更新が成功したら、更新コンポーネントはクライアントを再起動し、自身を閉じて自動更新を完了します。
- ファイルサーバー(必須):クライアントプログラムの更新圧縮パッケージをファイルサーバーにアップロードし、各圧縮パッケージのURLを取得します。更新コンポーネントはそのURLに基づいてプログラムをダウンロードします。筆者が使用したファイルサーバーはHFSで、ダウンロード先はHFSダウンロードです。
コード構造の分析

上図で GeneralUpdate で始まるプロジェクトは、自動更新機能の中核コードです。nugetサーバーで各プロジェクトのパッケージを確認できます。どのパッケージを使用するかは、更新コンポーネント自体の更新、クライアントプログラムの更新、バージョン検証サービスの作成のどれを実現したいかによって異なります。フレームワークのREADME.mdの説明を参照してください。
注意すべき点は、上記のコンポーネントは下位互換性がありません。3.x.x バージョンのコンポーネントでは多くのメソッド名が変更されているため、2.x.x バージョンから直接アップグレードすることはできません。
上図で AutoUpdate で始まるプロジェクトは、自動更新フローチャートの3つの主要コンポーネントの簡易実装です:
ConsoleApp:更新コンポーネントのコンソール版デモ(ファイルサーバーと組み合わせて使用。GeneralUpdate.Core を導入)MauiApp-Sample:詳細未調査、不明MinimalService:クライアントバージョン検証サービスのデモ(GeneralUpdate.AspNetCore を導入)Test:更新コンポーネントの自己更新を行うWPF版デモ(MinimalService と組み合わせて使用。GeneralUpdate.ClientCore を導入)WpfApp:GeneralUpdate.Singleパッケージの使用デモ。シングルインスタンス版の更新コンポーネントを構築するために使用(GeneralUpdate.Single を導入)WpfNet6-Sample:更新コンポーネントを更新するWPF版プログラム。
Winform アプリケーションの自動更新実践
前節の説明からわかるように、クライアントバージョン検証サービスを作成せず、ファイルサーバーのみを使用してクライアントプログラムを更新したい場合、コンソール版の更新コンポーネントが1つあれば十分です。そのため、ConsoleApp プロジェクトのコードを参考にしてください。
更新コンポーネントのコンソール実装
説明:このサンプルでは GeneralUpdate.Core のバージョン2.1.6を使用しています。GitHub上のソースコードは3.x.x バージョンにアップグレードされており、.NET 6.0をサポートしていますが、筆者のPCには関連フレームワークが不足しておりコンパイルできなかったため、ソースコードの特定のコミットをチェックアウトしました。これにより、使用中に問題が発生してもソースコードをデバッグして解決できます。本稿の内容を十分に理解していれば、最新のnugetパッケージをインストールし、最新版のソースコードの関連サンプルを直接参照しても問題ありません。
using System;
using System.ComponentModel;
using System.Diagnostics;
using GeneralUpdate.Core;
using GeneralUpdate.Core.Strategys;
using GeneralUpdate.Core.Update;
using ProgressChangedEventArgs = GeneralUpdate.Core.Update.ProgressChangedEventArgs;
namespace AutoUpdate.ConsoleApp
{
class Program
{
static void Main(string[] args)
{
// args = new []{
// "1.0.1",
// "1.0.2",
// "",
// "http://127.0.0.1:7000/client_v1.0.2.zip",
// @"D:\Project",
// "36aad55a19f85ee6e1fbdc26510a26c1"
// };
KillProcess("あなたのクライアントプログラム名(exeは不要)");
GeneralUpdateBootstrap bootstrap = new GeneralUpdateBootstrap();
bootstrap.DownloadStatistics += OnDownloadStatistics;
bootstrap.ProgressChanged += OnProgressChanged;
bootstrap.Strategy<DefultStrategy>().
Option(UpdateOption.Format, "zip").
Option(UpdateOption.MainApp, "あなたのクライアントプログラム名(exeは不要)").
Option(UpdateOption.DownloadTimeOut, 60).
RemoteAddress(args).
Launch();
Console.ReadKey();
}
private static void OnProgressChanged(object sender, ProgressChangedEventArgs e)
{
if (e.Type == ProgressType.Updatefile)
{
var str = $"現在更新中:{e.ProgressValue}個目、更新ファイル総数:{e.TotalSize}";
Console.WriteLine(str);
}
if (e.Type == ProgressType.Done)
{
Console.WriteLine("更新完了");
}
if (e.Type == ProgressType.Fail)
{
Console.WriteLine(e.Message);
}
}
private static void OnDownloadStatistics(object sender, DownloadStatisticsEventArgs e)
{
Console.WriteLine($"ダウンロード速度:{e.Speed}、残り時間:{e.Remaining.Minute}:{e.Remaining.Second}");
}
private static void KillProcess(string processName)
{
foreach (var process in Process.GetProcesses())
{
if (!process.ProcessName.ToUpper().Contains(processName.ToUpper())) continue;
try
{
process.Kill();
process.WaitForExit();
}
catch (Win32Exception)
{
}
}
}
}
}
クライアント側の呼び出し
Version version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
var ver = $"{version.Major}.{version.Minor}.{version.Build}";
// データベースから現在のアセンブリバージョンより新しいバージョン情報を取得
var versionInfo = TOSBll.Instance.GetLastUpdateVersionInfo(1, ver);
if (versionInfo != null)
{
string para =
$"{ver} {versionInfo.VERSION} \"\" {versionInfo.URL} {Environment.CurrentDirectory} {versionInfo.MD5}";
ExecuteAsAdmin("AutoUpdate.ConsoleApp.exe", para);
return;
}
private static void ExecuteAsAdmin(string fileName, string args)
{
Process proc = new Process();
proc.StartInfo.FileName = fileName;
proc.StartInfo.UseShellExecute = true;
proc.StartInfo.Verb = "runas";
proc.StartInfo.Arguments = args;
proc.Start();
}
上記のコードからわかるように、クライアントはプロセス間通信を使用して更新コンポーネントを起動し、更新パラメータ情報を渡します。ここでは管理者権限で更新コンポーネントを起動して、更新の失敗を防ぎます(コンポーネントは更新時にファイルをシステムの一時ディレクトリにコピーし、更新後に削除するため、権限が不十分だとエラーが発生します)。ただし、筆者のテストではこの方法でも起動に失敗し、AutoUpdate.ConsoleApp.exe を右クリックして管理者権限を追加することで成功しました。
いくつかの不満点
- 重要なバージョンにタグが付けられておらず、nugetパッケージの2.1.6バージョンに切り替えたい場合、どのコミットをチェックアウトすればよいかわからない。
- 新しいバージョンのコンポーネントは古いバージョンと互換性がない。
- 単体テストファイルで使用されているコードは古いバージョンだが、コンポーネントのソースコードは新しいバージョンであり、初めてこのコンポーネントに触れる人を混乱させる。
- 現在もいくつかの小さなバグが存在する。例えば、
FileUtil.Update32Or64Libs()で例外がスローされる。これは、ディレクトリが2回削除されるため、初回更新時に失敗するが、2回目は成功する(すでに削除済みのため)。筆者はIssueを提出済みだが、作者の対応時期は不明。 - ドキュメントが簡素すぎる。
まとめ
GeneralUpdate コンポーネントにはいくつかの欠点がありますが、本稿の紹介により、このコンポーネントをどのように使用すれば問題を回避できるか理解いただけたと思います。全体的には、このコンポーネントの機能は非常に使いやすいものです。このコンポーネントは作者一人でメンテナンスされていることを考慮すると、すでによくできており、作者の貢献に感謝します。