使用中のexe/DLLをどう差し替えるか ── Restart Managerと自動更新の「ファイル使用中」問題

· 更新日: · · Restart Manager, 自動更新, インストーラー, MSI, Windows API, C#, P/Invoke, 配布

「アプリを更新するので全員×ボタンで閉じてください、と更新のたびに館内放送している」「夜間にファイルサーバー上のexeを差し替えるバッチを仕込んだが、朝見ると『プロセスはファイルにアクセスできません』で失敗していた」── 業務アプリの配布・更新の相談で、この「ファイル使用中」問題は本当によく出てきます。インストーラーが最後に出す「再起動が必要です」も根は同じで、誰かが掴んでいるファイルを差し替えられないことがすべての原因です。

やっかいなのは、この問題が「たまにしか起きない」ことです。開発機では自分がアプリを閉じてから更新するので再現せず、本番の共有環境では「誰か一人がアプリを開きっぱなしで帰宅した」だけで夜間更新が全滅します。そして誰が掴んでいるのかは、エラーメッセージからは分かりません。

実はWindowsには、この問題のためにOS標準の仕組みがあります。「誰が掴んでいるか」を列挙し、行儀よく終了させ、更新後に再起動までさせるRestart Manager APIです。さらに「実行中のexeはリネームできる」という性質を使えば、プロセスを止めずに次回起動から新版に切り替える更新も設計できます。この記事では、業務アプリの自動更新・インストーラーで「ファイルが使用中です」に悩む開発者を対象に、ロックの仕組みからRestart Managerの使い方、アプリ側の作法、自動更新の実装パターンまでを、公式ドキュメントの裏付けとC#の診断コード付きで整理します。

1. まず結論

  • 実行中のexe・ロード済みDLLは「上書き」と「削除」はできませんが、同一ボリューム内の「リネーム(移動)」は可能です。古いファイルを退避名にリネームしてから新しいファイルを置くrename-then-replaceが、独自アップデーターの基本形です。
  • Restart ManagerはWindows Vista以降のOS標準APIで、「使用中ファイル問題」のために存在します。更新対象ファイルを登録すると占有しているアプリ・サービスを列挙し、終了と再起動までを面倒みてくれます。目的はOS再起動の削減・排除です。1
  • APIの流れは RmStartSession → RmRegisterResources → RmGetList → RmShutdown → (更新) → RmRestart → RmEndSession です。停止はGUIアプリ→コンソールアプリ→サービス→エクスプローラーの順、再起動は逆順で行われます。12
  • RmGetListだけでも「誰が掴んでいるか」を出す診断ツールが作れます。C#からのP/Invokeで数十行です(後述のコード参照)。3
  • MSI(Windows Installer 4.0以降)はRestart Managerを自動で使います。パッケージにMsiRMFilesInUseダイアログを入れておくと、ユーザーに「アプリを自動的に閉じて再起動する」選択肢を提示できます。4
  • アプリ側にも作法があります。RegisterApplicationRestartで再起動コマンドラインを登録し、WM_QUERYENDSESSION(lParam=ENDSESSION_CLOSEAPP)に応答し、WM_ENDSESSIONで未保存データを退避して終了する。この3点を実装したアプリは「更新のために閉じられても、更新後に元通り立ち上がる」ようになります。56
  • 再起動ループ防止のため、起動から60秒以上経っていないアプリは再起動されません。また、Restart Managerによる強制終了のタイムアウトはアプリ30秒・サービス20秒です。62
  • どうしても差し替えられないときの最終手段が MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT(OS再起動時の置換予約)です。管理者権限が必要で、予約はレジストリのPendingFileRenameOperationsに記録されます。7

2. なぜ使用中のファイルは差し替えられないのか ── そして「リネームは通る」という抜け道

Windowsがexeを実行するとき、またはDLLをロードするとき、OSはそのファイルをメモリマップドファイル(イメージセクション)としてマップします。ページングの仕組み上、実行中もファイルの実体が参照され続けるため、中身の書き換えと削除はブロックされます。エクスプローラーでコピーしようとすれば「別のプロセスが使用中です」、File.Copy なら IOException(共有違反)、File.Delete なら UnauthorizedAccessException が返る、あの挙動です。

ここで重要なのが、Windowsのあまり知られていない性質です。ファイルの「中身」はロックされていても、ディレクトリ上の「名前」は変更できます。実行中のexeでも、同一ボリューム内であればリネーム(移動)は成功します。実行イメージが掴んでいるのはファイルの実体であって、パス名ではないからです。

この非対称性から、独自更新の基本形であるrename-then-replaceパターンが導かれます。

1. MyApp.exe(実行中)を MyApp.exe.old にリネームする   ← 実行中でも成功する
2. 新しい MyApp.exe を本来のパスに配置する             ← 名前が空いたので成功する
3. 次回起動から新しい MyApp.exe が使われる
4. 旧プロセスの終了後、どこかのタイミングで .old を削除する
   (実行中は削除できないため、次回更新時や起動時の掃除処理で消す)

プロセスを止めずに「次回起動から新版」を実現できるのがこのパターンの価値で、ChromeのアップデーターやSquirrel/Velopackといった更新フレームワークも本質的にはこの性質の上に成り立っています。注意点は3つあります。リネームは同一ボリューム内でしか使えないこと(別ボリュームへの移動はコピー+削除になり、削除で失敗します)、.old ファイルの掃除を設計に含めること、そして「今動いている旧プロセス」はあくまで旧版のまま動き続けるため、新旧プロセスが混在する時間帯があることです。

なお「そもそも誰が掴んでいるのか」を障害調査として手で突き止めたい場合は、Process ExplorerやHandleコマンドでハンドル・ロード済みDLLを検索するのが早道です。手順は同日公開の姉妹記事「Process Explorer / Handle / VMMapでハングとリークを追う」にまとめました。本記事ではこれをプログラムから、つまりアップデーター自身に組み込む方法としてRestart Managerを使います。

3. Restart Managerの仕組み ── RmStartSessionからRmRestartまで

Restart ManagerはWindows Vista / Windows Server 2008以降に標準搭載されたAPI群(rstrtmgr.dll)で、目的はドキュメントの冒頭に明快に書かれています。「インストールや更新がシステム再起動を要求する主な理由は、更新対象ファイルが実行中のアプリケーションやサービスに使用されているからであり、Restart Managerは重要なもの以外のアプリ・サービスを終了・再起動させることで、この再起動を減らすか不要にする」1。MSIパッケージのインストール中に「以下のアプリケーションが使用中のファイルを…」とプロセス一覧のダイアログが出るのを見たことがあるはずですが、Visual StudioやOfficeのような大型製品のインストーラー・更新もこの仕組みの利用者です。

APIの流れは一直線です。

ステップ 関数 やること
1 RmStartSession セッションを開始し、ハンドルとセッションキー(GUID文字列)を得る
2 RmRegisterResources 更新したいファイルパス(プロセス・サービス名も可)を登録する
3 RmGetList 登録リソースを使用中のアプリ・サービスを列挙する3
4 RmShutdown それらを終了させる(通常は穏当に、指定すれば強制)2
5 ── この間にファイルを差し替える
6 RmRestart 再起動登録済みのアプリを再起動する
7 RmEndSession セッションを閉じる

押さえておきたい仕様がいくつかあります。

  • 停止順序と再起動順序。GUIアプリ→コンソールアプリ→サービス→エクスプローラーの順に停止し、更新後は登録済みアプリを逆順で再起動します。1
  • 終了のさせ方は「行儀の良い順」です。GUIアプリにはWM_QUERYENDSESSION/WM_ENDSESSION(lParam=ENDSESSION_CLOSEAPP)を送り、応じないアプリにはWM_CLOSEも送ります。コンソールアプリにはCTRL_C_EVENT、サービスはSCM経由で停止されます。RmForceShutdownを指定した場合も、まず穏当な終了を試みてから、応答しないアプリを30秒(サービスは20秒)で強制終了します。52
  • 再起動できるのはRegisterApplicationRestartで登録済みのアプリだけです。RmShutdownにRmShutdownOnlyRegisteredを指定すると「全員が再起動登録済みのときだけ終了する」という安全側の動きにできます。25
  • セッションをまたぐ終了はできません。LocalSystemのサービスとして動くインストーラーは、ユーザーセッションで動いているアプリを終了・再起動できません。夜間の無人更新を設計するときに最も引っかかるのがここで、対象セッション内で終了・再起動を担うプロセスを別途起動するなどの工夫が必要になります。2
  • 重要なシステムサービスとクリティカルプロセスは対象外です。この場合はOS再起動が必要という判定(RM_REBOOT_REASON)が返ります。13

MSIとの関係も整理しておきます。Windows Installer 4.0以降のMSIは、Restart Managerを自動的に使います。既定の動作は「OSを再起動するのではなく、可能ならアプリを終了・再起動する」です。パッケージ作者ができるのは、フルUI時に「アプリケーションを自動的に閉じて、再起動する」の選択肢をユーザーに出すMsiRMFilesInUseダイアログの追加(古いInstallerでは従来のFilesInUseダイアログにフォールバック)、MSIRESTARTMANAGERCONTROL等のプロパティによる挙動制御、そしてカスタムアクションからMsiRestartManagerSessionKeyプロパティ経由でRmJoinSessionを呼んでリソースを追加登録することです(このカスタムアクションは、使用中ファイルの検出が行われるInstallValidateアクションより前に並べます)。サイレントインストールでは常にRestart Managerが使われ、アプリは自動的に終了されます。4

つまり、MSI配布をしているなら「Restart Managerを呼ぶコード」を自分で書く必要はほとんどなく、アプリ側の作法(次々節)を整えることが本体です。独自アップデーターを書く場合にだけ、このAPIを直接呼ぶ価値が出てきます。配布方式そのものの選定は「Windowsアプリ配布方式の選び方 - MSI/MSIX/ClickOnce/xcopy/独自更新」で扱っています。

4. C#で「誰が掴んでいるか」を出す ── RmGetList診断コード

Restart Managerの中で、更新処理を書かない人にも役立つのがRmGetListです。「このファイルを使っているプロセスとサービスの一覧」をAPIで取れるので、アップデーターのエラーメッセージを「ファイルが使用中です」から「経理端末のMyApp.exe(PID 4132)が掴んでいます」に変えられます。C#からのP/Invokeで書くとこうなります(.NET Framework 4.8 / .NET 8いずれでも動作します)。

// WhoLocks.cs ── 指定ファイルを「誰が掴んでいるか」をRestart Managerで列挙する
// 使い方: WhoLocks.exe C:\App\MyApp.exe C:\App\MyLib.dll
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

internal static class Program
{
    private const int CCH_RM_SESSION_KEY = 32;      // セッションキーはGUID文字列(32文字+終端)
    private const int CCH_RM_MAX_APP_NAME = 255;
    private const int CCH_RM_MAX_SVC_NAME = 63;
    private const int ERROR_MORE_DATA = 234;

    [StructLayout(LayoutKind.Sequential)]
    private struct RM_UNIQUE_PROCESS
    {
        public uint dwProcessId;
        public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
    }

    // RM_APP_TYPE: 1=GUIアプリ, 2=他ウィンドウ, 3=サービス, 4=Explorer, 5=コンソール, 1000=クリティカル

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct RM_PROCESS_INFO
    {
        public RM_UNIQUE_PROCESS Process;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)]
        public string strAppName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)]
        public string strServiceShortName;
        public int ApplicationType;
        public uint AppStatus;
        public uint TSSessionId;
        [MarshalAs(UnmanagedType.Bool)]
        public bool bRestartable;
    }

    // Restart Manager APIはGetLastErrorではなく戻り値でWin32エラーコードを返す
    [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
    private static extern int RmStartSession(
        out uint pSessionHandle, int dwSessionFlags, StringBuilder strSessionKey);

    [DllImport("rstrtmgr.dll")]
    private static extern int RmEndSession(uint dwSessionHandle);

    [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
    private static extern int RmRegisterResources(uint dwSessionHandle,
        uint nFiles, string[] rgsFileNames,
        uint nApplications, RM_UNIQUE_PROCESS[] rgApplications,
        uint nServices, string[] rgsServiceNames);

    [DllImport("rstrtmgr.dll")]
    private static extern int RmGetList(uint dwSessionHandle,
        out uint pnProcInfoNeeded, ref uint pnProcInfo,
        [In, Out] RM_PROCESS_INFO[] rgAffectedApps, out uint lpdwRebootReasons);

    private static void Main(string[] args)
    {
        if (args.Length == 0)
        {
            Console.Error.WriteLine("使い方: WhoLocks <ファイルパス> ...");
            Environment.Exit(2);
        }

        var sessionKey = new StringBuilder(CCH_RM_SESSION_KEY + 1);
        int rc = RmStartSession(out uint session, 0, sessionKey);
        if (rc != 0) throw new Win32Exception(rc, $"RmStartSession失敗 (rc={rc})");
        try
        {
            // APIの契約上、登録するファイル名は完全パスでなければならない。
            // 相対パスで呼ばれても動くよう、登録前に正規化する
            var fullPaths = Array.ConvertAll(args, Path.GetFullPath);
            rc = RmRegisterResources(session, (uint)fullPaths.Length, fullPaths, 0, null, 0, null);
            if (rc != 0) throw new Win32Exception(rc, $"RmRegisterResources失敗 (rc={rc})");

            // 必要件数を問い合わせ→配列を確保→取得。2回の呼び出しの間にプロセスが
            // 増えているとERROR_MORE_DATAが再度返るため、ループで再試行する
            uint count = 0;
            uint rebootReasons;
            RM_PROCESS_INFO[] apps = null;
            while (true)
            {
                rc = RmGetList(session, out uint needed, ref count, apps, out rebootReasons);
                if (rc == 0) break;
                if (rc != ERROR_MORE_DATA) throw new Win32Exception(rc, $"RmGetList失敗 (rc={rc})");
                count = needed;
                apps = new RM_PROCESS_INFO[needed];
            }

            Console.WriteLine($"使用中のプロセス/サービス: {count}件 (RebootReasons={rebootReasons})");
            for (int i = 0; i < count; i++)
            {
                RM_PROCESS_INFO a = apps[i];
                Console.WriteLine(
                    $"  PID={a.Process.dwProcessId,-6} 種別={a.ApplicationType} " +
                    $"再起動可={a.bRestartable} セッション={a.TSSessionId} " +
                    $"名前={a.strAppName} サービス={a.strServiceShortName}");
            }
        }
        finally
        {
            RmEndSession(session);   // セッションの解放は必ずfinallyで
        }
    }
}

要点は3つです。第一に、Restart Manager APIは戻り値がそのままWin32エラーコードで、GetLastError は使いません(DllImportSetLastError = true は不要です)。第二に、RmGetListは「バッファが足りなければERROR_MORE_DATA(234)と必要件数を返す」呼び出し規約なので、問い合わせ→確保→取得のループで書きます。3 第三に、lpdwRebootReasons が0(RmRebootReasonNone)以外なら「アプリ終了ではダメでOS再起動が必要」という判定です。3 構造体の文字列を ByValTStr でマーシャリングする書き方や、この種のP/Invokeを安全に書く一般論は「C#からWin32 APIを安全に呼ぶ ── P/Invoke実務ガイド」を参照してください。

このコードにRmShutdown/RmRestartの2つのP/Invokeを足せば「列挙→穏当に終了→差し替え→再起動」の独自アップデーターの骨格になります。ただしRmShutdownを呼んでよいのは自分でRmStartSessionしたプロセスだけで、MSIのカスタムアクションからは呼んではいけない(MSI自身がセッションを管理しているため)点に注意してください。4

5. アプリ側の作法 ── 「行儀よく閉じられ、元通り立ち上がる」ための3点セット

Restart ManagerもMSIも、terminateで問答無用にプロセスを殺すことは(強制指定しない限り)しません。アプリ側が応答しなければ、結局「使用中のファイル」ダイアログでユーザーの手を煩わせることになります。業務アプリを「更新に強く」するには、アプリ側に次の3点セットを実装します。5

(1) RegisterApplicationRestartで再起動を登録する。起動直後に一度呼んでおきます。Restart Managerが再起動できるのは登録済みアプリだけで、これが唯一の「再起動時のコマンドラインをOSに伝える手段」です。コマンドラインにexe名は含めない(OSが付けてくれます)、最大長はRESTART_MAX_CMD_LINE、そして起動から60秒間は再起動ループ防止のため再起動されない、が主な仕様です。クラッシュ・ハング時の再起動が不要なら、RESTART_NO_CRASH RESTART_NO_HANG を指定して「更新時のみ再起動」に絞れます。6
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int RegisterApplicationRestart(string commandLine, int flags);

// 起動時に登録しておく。「/restored」フラグで再起動後の復元処理に分岐できる
// RESTART_NO_CRASH(1) | RESTART_NO_HANG(2) = 更新による終了のときだけ再起動する
RegisterApplicationRestart("/restored", 1 | 2);

(2) WM_QUERYENDSESSION(lParam=ENDSESSION_CLOSEAPP)に応答する。Restart Managerは終了前にこのメッセージで「閉じてもよいか」を尋ねてきます。ここではまだ終了せず、準備ができているならTRUEを返します(他のアプリの準備が整っていない可能性があるためです)。FALSEを返せばシャットダウンをキャンセルできますが、強制指定時は結局終了させられるため、FALSEに頼る設計は避けます。更新の局面でRegisterApplicationRestartを呼び直す最後のチャンスもこのタイミングです。復元に必要な状態(開いていた帳票のIDなど)をコマンドライン引数に焼き込んで登録し直すと、再起動後に「元の画面」まで戻せます。56

(3) WM_ENDSESSIONで未保存データを退避して終了する。実際の終了指示はWM_ENDSESSION(wParam=TRUE、lParam=ENDSESSION_CLOSEAPP)で届きます。タイムアウト(強制時はアプリ30秒)以内に、未保存データの自動保存・作業状態のシリアライズ・接続のクローズを済ませて終了します。公式ガイドラインは「そもそも定期的にユーザーデータを保存しておくこと」を推奨しています。コンソールアプリの場合はWM_の代わりにCTRL_C_EVENTが届くので、SetConsoleCtrlHandler(C#なら Console.CancelKeyPress)で同じことをします。52

WPF/WinFormsでは Application.SessionEnding / Form.FormClosing(CloseReason.WindowsShutDown)が入り口になりますが、ENDSESSION_CLOSEAPPフラグ(OS再起動ではなく「アプリだけ閉じて後で再起動」の区別)まで見るならウィンドウプロシージャのフックが必要です。また、再起動後のプロセスが「終了しかけの前回の自分」とぶつからないよう、多重起動防止のMutex設計もセットで見直してください(「Windowsアプリの多重起動防止」)。

この3点セットが揃うと、MSI更新の体験は「全員に館内放送して閉じてもらう」から「更新が走るとアプリが自動で閉じ、更新後に元の画面まで自動復帰する」に変わります。Officeの「再起動後にドキュメントを開き直してくれる」挙動と同じ仕組みです。

6. 自動更新の実装パターン ── 別プロセス、rename-then-replace、再起動時置換

独自の自動更新を組む場合の設計パターンを整理します。大前提はひとつ、自分自身(実行中のexe)を自分で上書きすることはできないので、更新処理はどこかで「別プロセス」か「別名」に逃がす必要があります。

パターンA: 別プロセスのアップデーター + 待って差し替え。本体アプリが更新を検知したら、アップデーターexe(またはコピーした一時exe)を起動して自分は終了します。アップデーターは本体プロセスの終了を待ち(Process.WaitForExit やMutexの解放待ち)、ファイルを差し替えてから本体を再起動します。素朴で確実ですが、「本体がなかなか終了しない」「複数プロセス構成で待つ相手が多い」場合に備え、前節のRmGetListで占有者を確認してからRmShutdownで閉じる、という組み合わせが効きます。

パターンB: rename-then-replace(停止させない更新)。第2節のとおり、実行中のexe/DLLをリネームして新版を置き、「次回起動から新版」にします。ユーザーの作業を中断しないのが利点で、常駐型・長時間稼働型の業務アプリ向きです。SquirrelやVelopackといった.NET向け更新フレームワークも、バージョン別フォルダー+エントリーexeの差し替えという形でこの性質を利用し、「更新は裏で適用、次回起動から新版」を枠組みごと提供してくれます。自前実装なら、ダウンロード→検証→展開→リネーム退避→配置→次回起動時に退避ファイルを掃除、という手順に落ちます。

パターンC: MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT(再起動時置換)。サービスのホストプロセスやシェル拡張のように「どうしても止められない・リネームでも逃げられない」ものの最終手段が、OS再起動時の置換予約です。予約内容はレジストリの HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations に記録され、次回起動の早い段階(AUTOCHK直後、ページファイル作成前)で登録順に実行されます。管理者グループまたはLocalSystemでしか呼べないこと、MOVEFILE_COPY_ALLOWEDと併用できない(=同一ボリューム限定)こと、置換先ファイルが既に存在する場合は MOVEFILE_REPLACE_EXISTING の併用が必要なこと(併用しないと予約自体は成功しても再起動時の置換が行われません)、そして関数の成功は「予約の成功」であって実際の置換結果は分からないことを理解した上で使います。インストーラーが「再起動が必要です」と言うとき、内部ではまさにこれが積まれています。7

どのパターンでも共通する注意が2つあります。ひとつはサービスの更新で、Windowsサービスは自分で自分を差し替えられないため、「SCMでStop→差し替え→Start」を行う更新用プロセス(または更新専用サービス)を分離するのが定石です(サービス設計の基本は「Windowsサービスの作り方と運用」)。もうひとつは更新ファイルの検証です。差し替えの仕組みを自作するということは「任意のexeを配って実行させる仕組み」を自作するということでもあり、署名検証やダウンロード経路の保護を省くと更新機構そのものが攻撃経路になります。この論点は「自動アップデートのセキュリティ設計 - HTTPSだけでは足りない理由」で独立して扱っています。

7. 更新方式の判断表

方式 向いている状況 注意点
MSI + OS再起動を許容 配布頻度が低い(年数回)、夜間・休日にメンテナンス時間を取れる 一番実装が楽。ただし「再起動が必要です」をユーザーに見せ続けることになる。再起動時置換(PendingFileRenameOperations)頼み7
MSI + Restart Manager統合(MsiRMFilesInUse + アプリ側3点セット) MSI配布を維持したまま更新体験を改善したい。社内配布の標準アプリ インストーラー側はほぼ自動。効果はアプリ側のRegisterApplicationRestart/WM_QUERYENDSESSION実装次第45
独自アップデーター(別プロセス + rename-then-replace、Squirrel/Velopack含む) 更新頻度が高い(週次以上)、ユーザーに管理者権限がない、作業を中断させたくない 掃除処理・署名検証・失敗時ロールバックを自作する覚悟が必要。RmGetList/RmShutdownで「掴んでいる誰か」対策を入れると堅い3
ClickOnce 社内Windowsクライアントで「起動時に自動更新」だけ欲しい 起動時更新モデルなら使用中問題自体が起きにくい。制約は「ClickOnce とは何か」参照
サービス側の自己更新(更新用プロセス分離) 無人環境・装置PCで24時間動くサービスを止めずに保てない サービス自身は自分を差し替えられない。Stop→差し替え→Startを担う別プロセスが必須。セッション境界(LocalSystemからユーザーアプリは閉じられない)に注意2

迷ったらまず「MSI + Restart Manager統合」を検討してください。OS標準の仕組みに乗るため実装量が最小で、アプリ側の3点セットは独自アップデーターに移行する場合にもそのまま資産になります。

8. まとめ

  • 使用中のexe/DLLは上書き・削除できませんが、同一ボリューム内のリネームは可能です。この非対称性を使ったrename-then-replaceが「止めない更新」の基本形です。
  • Restart Managerは「誰が掴んでいるか」の列挙(RmGetList)、穏当な終了(RmShutdown)、更新後の再起動(RmRestart)までを面倒みるOS標準APIで、Windows Installer 4.0以降のMSIは自動的にこれを使います。
  • アプリ側の作法は3点セットです。RegisterApplicationRestartで再起動を登録し、WM_QUERYENDSESSION(ENDSESSION_CLOSEAPP)にTRUEを返し、WM_ENDSESSIONで未保存データを退避して終了する。これだけで「更新後に元通り立ち上がるアプリ」になります。
  • 60秒ルール(起動直後は再起動されない)、強制終了タイムアウト(アプリ30秒・サービス20秒)、セッション境界(LocalSystemからユーザーアプリは閉じられない)が実運用のつまずきどころです。
  • どうしても差し替えられないときはMoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOTでOS再起動時に置換予約します。管理者権限必須・同一ボリューム限定・成否は予約の成否です。
  • 更新機構の自作は「任意のexeを配る仕組み」の自作です。署名検証・経路保護・ロールバックまで含めて設計してください。

関連記事

関連する相談領域

合同会社小村ソフトでは、業務アプリの自動更新機構の設計・実装(Restart Manager統合、独自アップデーター、Squirrel/Velopack導入)、「更新のたびに全員にアプリを閉じてもらっている」運用の改善、既存インストーラーの「ファイル使用中」「再起動が必要です」問題の調査と対策を扱っています。

参考リンク

  1. Microsoft Learn, About Restart Manager. Restart Managerの目的(使用中ファイルによる再起動の削減・排除)、GUIアプリ→コンソールアプリ→サービス→エクスプローラーの順で停止し逆順で再起動すること、セッションをまたぐシャットダウンが未サポートであること、Windows Installer 4.0が自動的にRestart Managerを使うこと、重要なシステムサービスはOS再起動なしには停止できないことについて。  2 3 4 5

  2. Microsoft Learn, RmShutdown function. RmForceShutdown指定時も応答しないアプリは30秒・サービスは20秒で強制終了されること、RmShutdownOnlyRegisteredで「全員が再起動登録済みのときだけ終了」にできること、LocalSystemのサービスは他のユーザーセッションのアプリを終了・再起動できないこと、ERROR_FAIL_NOACTION_REBOOT等の戻り値について。  2 3 4 5 6 7 8

  3. Microsoft Learn, RmGetList function. 登録済みリソースを使用中のアプリ・サービスをRM_PROCESS_INFO配列で返すこと、バッファ不足時にERROR_MORE_DATA(234)と必要件数を返す呼び出し規約、lpdwRebootReasonsでOS再起動が必要な理由(RM_REBOOT_REASON)が返ること、Windows Vista以降でrstrtmgr.dllが提供することについて。  2 3 4 5 6

  4. Microsoft Learn, Using Windows Installer with Restart Manager. Windows Installer 4.0がRestart Managerを自動的に使い、既定でOS再起動よりアプリの終了・再起動を優先すること、MsiRMFilesInUseダイアログでフルUI時に自動クローズ・再起動の選択肢を出せること(旧環境ではFilesInUseにフォールバック)、サイレントインストールでは常にRestart Managerが使われアプリが終了されること、カスタムアクションはInstallValidateより前に並べMsiRestartManagerSessionKey経由でRmJoinSessionを呼ぶべきでRmShutdown等を呼んではならないこと、MSIRESTARTMANAGERCONTROL等の制御プロパティについて。  2 3 4

  5. Microsoft Learn, Guidelines for Applications (Restart Manager). GUIアプリへWM_QUERYENDSESSION(lParam=ENDSESSION_CLOSEAPP)が送られ、準備ができていればTRUEを返しその時点では終了しないこと、実際の終了はWM_ENDSESSIONで行うこと、応じないアプリにはWM_CLOSEも送られること、コンソールアプリにはCTRL_C_EVENTが送られること、再起動にはRegisterApplicationRestartによる登録が必須であることについて。  2 3 4 5 6 7

  6. Microsoft Learn, RegisterApplicationRestart function. 再起動時のコマンドライン登録(exe名は含めない、最大RESTART_MAX_CMD_LINE)、RESTART_NO_CRASH/NO_HANG/NO_PATCH/NO_REBOOTフラグ、更新による再起動は自動でクラッシュ・ハング時はユーザー同意が必要なこと、ループ防止のため起動から60秒以上経過していないと再起動されないこと、更新時の最終登録チャンスがWM_QUERYENDSESSION処理中であることについて。  2 3 4

  7. Microsoft Learn, MoveFileExW function. MOVEFILE_DELAY_UNTIL_REBOOTがHKLM\SYSTEM\CurrentControlSet\Control\Session ManagerのPendingFileRenameOperations(REG_MULTI_SZ)に予約を書き込み、AUTOCHK実行後・ページファイル作成前に登録順で実行されること、管理者グループまたはLocalSystemの権限が必要なこと、MOVEFILE_COPY_ALLOWEDと併用不可であること、移動先ファイルが既に存在する場合はMOVEFILE_REPLACE_EXISTINGの指定が必要なこと、戻り値が予約の成否であって実際の移動の成否ではないこと、lpNewFileNameにNULLを渡すと再起動時削除になることについて。  2 3

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

実行中のexeやロード済みDLLは、なぜ上書きできないのにリネームはできるのですか?
Windowsは実行中のexeやロード済みDLLをメモリマップ(イメージセクション)として掴んでおり、ファイルの中身の書き換えと削除はエラー(共有違反やアクセス拒否)になります。一方、ディレクトリ上の「名前」はファイルの中身とは独立しているため、同一ボリューム内でのリネーム(移動)は実行中でも成功します。この非対称性を利用したのがrename-then-replaceパターンで、古いexeを退避名にリネームしてから新しいexeを本来の名前で置き、次回起動から新版が使われるようにします。ChromeやSquirrel/Velopack系の自動更新も本質的にはこの性質の上に成り立っています。
Restart Managerは何をしてくれるAPIですか?
更新したいファイルを登録すると「そのファイルを今使っているアプリケーション・サービスの一覧」を列挙し、可能ならそれらを終了させ、更新後に再起動までさせられるWindows標準API(Windows Vista以降)です。流れはRmStartSessionでセッションを作り、RmRegisterResourcesで対象ファイルを登録、RmGetListで占有プロセスを列挙、RmShutdownで終了、RmRestartで再起動、最後にRmEndSessionです。GUIアプリ→コンソールアプリ→サービス→エクスプローラーの順に停止し、再起動は逆順で行われます。「再起動が必要です」を減らすための仕組みで、Windows Installer 4.0以降のMSIは自動的にこれを使います。
RegisterApplicationRestartを呼んでおくと何が起きますか?
アプリが自分自身の再起動コマンドラインをOSに登録でき、更新によるシャットダウン後にRestart Managerがそのコマンドラインでアプリを自動再起動してくれます(Restart Managerが再起動できるのは登録済みアプリだけです)。クラッシュやハング時にも再起動の対象になりますが、その場合はユーザーの同意が挟まり、更新による再起動は自動で行われます。ループ防止のため起動から60秒以上経過していないと再起動されない点、コマンドラインにexe名を含めてはいけない点に注意してください。復元に必要な状態(開いていたファイルなど)はコマンドライン引数に含めて登録し直すのが作法です。
MoveFileExのMOVEFILE_DELAY_UNTIL_REBOOTはどんなときに使いますか?
プロセスをどうしても止められず、rename-then-replaceも使えない場合の最終手段で、OS再起動時にファイルの移動・削除を予約します。予約はレジストリのPendingFileRenameOperations(HKLM\SYSTEM\CurrentControlSet\Control\Session Manager)に書き込まれ、次回起動の早い段階(AUTOCHK後、ページファイル作成前)で登録順に実行されます。呼び出しには管理者グループまたはLocalSystemの権限が必要で、MOVEFILE_COPY_ALLOWEDとは併用できないため別ボリュームへの移動には使えません。関数の成否は「予約の成否」であって実際の置換の成否ではない点も押さえておくべきです。
MSIインストーラーで「使用中のファイル」を穏当に処理するにはどうしますか?
Windows Installer 4.0以降はRestart Managerと自動的に連携し、既定でOS再起動よりアプリの終了・再起動を優先します。パッケージにMsiRMFilesInUseダイアログを追加しておくと、フルUIでのインストール時に「アプリケーションを自動的に閉じて再起動する」選択肢がユーザーに提示されます。古いWindows Installerで実行された場合は従来のFilesInUseダイアログにフォールバックするため、両方を入れておくのが定石です。挙動はMSIRESTARTMANAGERCONTROLなどのプロパティで制御でき、サイレントインストールでは常にRestart Managerが使われアプリは自動終了されます。アプリ側がRegisterApplicationRestartとWM_QUERYENDSESSION応答を実装していれば、更新後にアプリが元通り立ち上がる「無停止に近い更新」が実現できます。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る