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

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

更新履歴(4件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
100行を超えていたコード例を、先に骨格(呼ぶAPIは4つ)を示してから完全版を置く構成に分けました。あわせてRmGetListが返す情報の形とアプリ種別の一覧、サービスからユーザーセッションを越える3つの方法、処理の流れの図を追加しています。
参考リンクなどで縦棒(パイプ)記号を含む行が表として表示され、リンクが押せなくなっていた表示崩れを修正しました。本文の内容は変えていません。
初版公開
この記事を引用する(DOI(登録済みアーカイブ): 10.5281/zenodo.21547431)

以下のDOIは過去に登録されたアーカイブを指しており、現在の本文とは一致しない場合があります。現在の本文を参照するときは、このページのURLを使用してください。

小村 豪(2026)「使用中のexe/DLLをどう差し替えるか ── Restart Managerと自動更新の「ファイル使用中」問題」合同会社小村ソフト. https://comcomponent.com/blog/restart-manager-file-in-use-update-guide/

DOI(登録済みアーカイブ)
10.5281/zenodo.21547431
DOI(前回登録した版)
10.5281/zenodo.21733140

「更新するので、全員アプリを閉じてください」と毎回案内している。夜間にexeを差し替えるバッチを組んだのに、朝になると「ファイルが使用中です」で失敗している。業務アプリの配布では、こうした問題が繰り返し起こります。

開発機では自分がアプリを閉じてから更新するため、問題が見えません。本番では、誰か一人がアプリを開いたまま帰宅しただけで更新が止まります。しかも、エラーメッセージだけでは誰がどのファイルを使っているのかが分かりません。使用中ファイルを差し替えられないことは、インストーラーがOS再起動を要求する主な理由でもあります。1

この問題は、「占有者を調べる」「終了してもらう」「新版へ切り替える」の3段階に分けると整理できます。Windows標準のRestart Managerは、占有しているアプリ・サービスの列挙と、終了・再起動を支援するAPIです。一方、実行中ファイルのリネームを使い、作業を中断せず「次回起動から新版」にする更新方式もあります。

この記事では、自動更新やインストーラーを作る開発者に向けて、ファイルのロック、Restart Managerの流れ、C#の診断コード、アプリ側の終了・復元処理、更新方式の選び方を順に説明します。

1. まず結論

まずは、次の3点を押さえてください。

  1. 「誰が使っているか」の診断と、終了・更新は分けられます。Restart ManagerはWindows Vista以降のOS標準APIです。対象ファイルを登録して RmGetList を呼ぶだけでも、占有アプリ・サービスを列挙する診断ツールになります。終了・再起動まで行うときの流れは RmStartSession → RmRegisterResources → RmGetList → RmShutdown → (更新) → RmRestart → RmEndSession です。123
  2. 更新方式は「終了を待って差し替える」か「次回起動から新版にする」かで考えます。実行中のexe・ロード済みDLLは上書き・削除できなくても、同一ボリューム内でのリネームは可能です。旧ファイルを退避名にして新版を置くrename-then-replaceが、独自アップデーターの基本形です。それでも差し替えられない場合に限り、MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT によるOS再起動時の置換予約を検討します。4
  3. MSIの標準連携と、アプリ側の協力を先に整えます。Windows Installer 4.0以降はRestart Managerを自動利用します。アプリ側は再起動コマンドラインの登録、終了確認への応答、未保存データの退避と終了を実装します。インストーラー側だけを直しても、安全な終了と元の状態への復帰は完成しません。567

運用では、停止はGUIアプリ→コンソールアプリ→サービス→エクスプローラー、再起動は逆順です。また、再起動には起動後60秒以上という条件があり、強制終了指定時のタイムアウトはアプリ30秒・サービス20秒です。セッション境界や再起動予約の権限も含め、以下で条件を読み分けます。173

目的別の読み方

知りたいこと 先に読む章
使用中のexe/DLLで、上書きとリネームは何が違うのか 第2章: ファイルの実体と名前
終了・差し替え・再起動の順序を確認したい 第3章: Restart Managerの流れ
占有者を表示する診断ツールを作りたい 第4章: 骨格→出力の読み方→完全版
MSIの更新体験を改善したい 第3.2節: MSIとの連携と第5章: アプリ側の作法
サービスからユーザーアプリを更新したい 第3.1節: セッション境界と第6章: 実装パターン
更新後に作業中の画面へ戻したい 第5章: アプリ側の3点セット
独自更新・次回起動時の切り替え・OS再起動時の置換を選びたい 第6章: 実装パターンと第7章: 判断表

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全22件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

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

ファイルの実体と、ディレクトリ上の名前は別

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

一方、ディレクトリ上の名前はファイルの実体とは別です。実行イメージが保持しているのは実体であって、パス名ではありません。そのため、実行中のexeでも同一ボリューム内でリネーム(移動)できます。

ここでいう「リネームできる」は、実行イメージとして使われていること自体がリネームを禁止するわけではない、という意味です。アクセス権や、別のハンドルによる共有制約まで無視できるわけではないので、実際の操作結果は確認します。84

rename-then-replaceは、次に起動する版を切り替える

この違いを利用し、先に旧ファイルの名前を空けてから新版を配置するのがrename-then-replaceパターンです。次の手順は、リネームと配置が成功する場合の基本形です。

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

プロセスを止めずに「次回起動から新版」を実現できるのが、このパターンの価値です。ChromeのアップデーターやSquirrel/Velopackといった更新フレームワークも、本質的にはこの性質を利用しています。ただし、ファイルを配置した時点で、今動いているプロセスまで新版になるわけではありません。

設計に含めること 理由
同一ボリューム内でリネームする 別ボリュームへの移動はコピーと削除になり、使用中の元ファイルを削除できないため、同じ手順にはなりません
.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)です。重要なものを除くアプリ・サービスを終了・再起動し、使用中ファイルを解放することで、インストールや更新に必要なOS再起動を減らす、または不要にします。1

MSIの「以下のアプリケーションが使用中のファイルを…」という一覧ダイアログは、この仕組みを利用する場面の一つです。Visual StudioやOfficeのような大型製品のインストーラー・更新でも使われます。

APIの流れと、ファイルを差し替える位置

先に全体の流れを見ます。ファイルの差し替えそのものはアップデーターの仕事で、Restart Managerはその前後の列挙・終了・再起動を受け持ちます。

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

この手順では、対象を終了できたことを確認してから、RmRestart を呼ぶ前にファイルを差し替えます。次の図は、終了に成功して更新へ進める場合の流れです。RmShutdown が単に戻っただけで、必ず全ファイルが解放されたと判断してはいけません。戻り値を確認し、終了できなかった対象がある場合は、RmGetList の更新された状態を調べます。3

使用中のアプリ(GUI/サービス)Restart Manager(rstrtmgr.dll)アップデーター使用中のアプリ(GUI/サービス)Restart Manager(rstrtmgr.dll)アップデーターここで「誰が掴んでいるか」が確定するRebootReasons≠0 ならOS再起動が必要⑤ ここでファイルを差し替える← ロックが外れているのはこの区間だけ① RmStartSessionセッションハンドル + セッションキー② RmRegisterResources(更新対象ファイル)③ RmGetList使用中のプロセス一覧 + RebootReasons④ RmShutdownWM_QUERYENDSESSION / WM_ENDSESSION(コンソールには CTRL_C_EVENT)保存して終了終了完了⑥ RmRestart登録済みアプリを逆順で再起動⑦ RmEndSession

図のプロセス一覧は取得時点の情報です。RmShutdown は実際の終了処理に入る前にも一覧を更新します。一部のアプリを終了できない場合は ERROR_FAIL_SHUTDOWN、OS再起動が必要で終了処理を行わない場合は ERROR_FAIL_NOACTION_REBOOT が返ります。終了に失敗しても、既に停止したアプリ・サービスを戻すため、RmRestart を呼ぶ復帰経路は必要です。3

終了方法と、再起動できる条件を分ける

停止順序はGUIアプリ→コンソールアプリ→サービス→エクスプローラーで、再起動は逆順です。1 終了要求の送り方は、対象の種類で異なります。

対象 終了要求の送り方
GUIアプリ WM_QUERYENDSESSION / WM_ENDSESSION(ENDSESSION_CLOSEAPP)。応答しないアプリには WM_CLOSE も送ります
コンソールアプリ CTRL_C_EVENT
サービス サービス制御マネージャー(SCM)経由の停止

RmForceShutdown を指定しても、まずは穏当な終了を試みます。応答しない場合の強制終了タイムアウトはアプリ30秒・サービス20秒です。63

再起動できるアプリは RegisterApplicationRestart で登録済みのものです。RmShutdownOnlyRegistered を指定すれば、全員が再起動可能な場合にだけ終了する安全側の動きにできます。一つでも再起動できないプロセス・サービスがあれば、どれも終了しません。36

さらに、次の制約は終了方法とは別に確認します。

制約 更新設計への影響
ユーザーセッションをまたぐ終了・再起動はできない LocalSystemのサービスから、別のユーザーセッションのアプリをRestart Managerで終了・再起動することはできません。第3.1節の構成が必要です
重要なシステムサービス・クリティカルプロセスは対象外 OS再起動が必要という判定(RM_REBOOT_REASON)を確認します

これらは、管理者権限を付ければすべて解決するという話ではありません。132

3.1 セッション境界をどう越えるか

夜間にLocalSystemのサービス(セッション0)から更新を流す場合でも、別のユーザーセッションのアプリをRestart Managerで直接終了・再起動することはできません。対象セッションの中に更新を担当するプロセスを置くのが基本です。3

現実的な選択肢は次の3つです。

方法 何をするか 向き・不向き
(A) タスクスケジューラで対象ユーザーとして起動 更新エージェントのタスクを、対象ユーザーのアカウントで「ユーザーがログオンしているときのみ実行」として登録し、更新の合図で起動する いちばん手軽。ログオン中のユーザーが特定できる社内端末向き
(B) ログオン時に常駐エージェントを起動 各ユーザーセッションで小さな常駐プロセスを動かし、共有フォルダーやイベントで更新の合図を待つ 対象ユーザーが不定・複数同時ログオンでも効く。常駐プロセスの保守コストがかかる
(C) サービスからユーザーセッションにプロセスを起こす LocalSystemのサービスが対象セッションのトークンを取得し、そのトークンでアップデーターを起動する 自由度は最も高いが、実装と権限管理がいちばん重い

タスクスケジューラを使う場合

(A)は schtasks で登録します。/RU に対象ユーザー、/IT に「そのユーザーがログオンしているときだけ対話的に実行する」という条件を指定するのが要点です。

これは確認用コマンドではなく、タスクを作成・実行する例です。 アカウント名、アップデーターのパス、時刻は環境に合わせます。前半の/createは登録、後半の/runは登録済みタスクの即時実行なので、必ず続けて実行する手順ではありません。

:: 更新エージェントを「対象ユーザーのセッション内で」動かすタスクを登録する
:: 資格情報の指定(/RP)や既存タスクの上書き(/F)は、環境のポリシーに合わせて調整する
schtasks /create /TN "MyApp Update Agent" /TR "C:\App\Updater.exe /shutdown-and-update" ^
         /SC ONCE /ST 02:00 /RU CORP\taro /IT

:: 夜間バッチ側からは、時刻を待たずに即時実行させることもできる
schtasks /run /TN "MyApp Update Agent"

サービスからプロセスを起こす場合

(C)では、WTSGetActiveConsoleSessionId や WTSEnumerateSessions で対象セッションIDを求め、WTSQueryUserToken でそのユーザーのプライマリトークンを取得し、CreateProcessAsUser でアップデーターを起動します。

WTSQueryUserToken にはLocalSystemアカウントと SE_TCB_NAME 特権が必要です。高度に信頼されたサービス向けのAPIであり、取得したトークンを漏らさず、使い終わったハンドルを必ず閉じます。9 取り扱いを誤ると権限昇格の穴になるため、(A)や(B)で足りるなら、無理に(C)を選ぶ必要はありません。

どの方式でも、セッション内のプロセスが RmStartSession から RmRestart までを担当し、セッション0のサービスは更新ファイルの配布や合図を受け持つ、という役割分担です。

3.2 MSIを使う場合は、標準連携を先に利用する

Windows Installer 4.0以降のMSIは、Restart Managerを自動利用します。既定では、OS再起動よりも、可能ならアプリの終了・再起動で更新を完了させます。パッケージ作者が整える点は次のとおりです。5

整えるもの 役割・条件
MsiRMFilesInUse ダイアログ フルUIで「アプリケーションを自動的に閉じて、再起動する」を選べるようにします。旧Installerでは従来の FilesInUse にフォールバックします
MSIRESTARTMANAGERCONTROL などのプロパティ Restart Managerとの連携動作を制御します
カスタムアクションによるリソース追加 MsiRestartManagerSessionKey からセッションキーを取得し、RmJoinSession で参加します。使用中ファイルを検出する InstallValidate より前に並べます

サイレントインストールでは、標準動作としてRestart Managerによる自動終了が使われます。無人実行を計画するときも、上記の制御プロパティやポリシーを含めて確認します。カスタムアクションがMSIに代わって RmShutdown / RmRestart を呼ぶ構成にはしません。5

MSI配布なら、Restart Managerを直接呼ぶコードを自作するより、第5章のアプリ側の作法を整えることが中心です。独自アップデーターの場合に、このAPIを直接呼ぶ価値が出てきます。配布方式そのものの比較は「Windowsアプリ配布方式の選び方 - MSI/MSIX/ClickOnce/xcopy/独自更新」で扱っています。

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

更新処理を実装しない人にも役立つのが RmGetList です。占有アプリ・サービスの一覧を取得し、「ファイルが使用中です」というメッセージを、アプリ名やPIDを示す具体的な説明にできます。

以下は実行した端末での診断を前提にしたコードです。 例えば経理端末上で実行して、MyAppとPIDが出れば、その端末で動くプロセスを調べる手掛かりになります。出力に端末名は含まれず、共有ファイルを使う別端末の一覧を自動収集するコードではありません。

この章のコードは列挙だけを行い、アプリの終了やファイルの差し替えは行いません。骨格でAPIの順序をつかみ、出力の見方を確認してから、完全版を読む構成です。

4.1 骨格 ── 呼ぶのは4つだけ

P/Invoke宣言と構造体定義を除くと、本体は次の形です。セッション開始、ファイル登録、一覧取得、セッション終了という4つのAPIを使います。 この骨格だけをコピーしてもコンパイルはできません。 宣言と構造体は4.3の完全版で確認してください。

// 【骨格】構造体定義とP/Invoke宣言は4.3の完全版に載せています
var sessionKey = new StringBuilder(CCH_RM_SESSION_KEY + 1);

// ① セッションを開始する
int rc = RmStartSession(out uint session, 0, sessionKey);
if (rc != 0) throw new Win32Exception(rc);
try
{
    // ② 調べたいファイルを登録する(完全パスであること)
    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);

    // ③ 使用中のプロセス/サービスを列挙する
    //    バッファ不足なら ERROR_MORE_DATA と必要件数が返るので、確保して取り直す
    uint count = 0;
    RM_PROCESS_INFO[] apps = null;
    while (true)
    {
        rc = RmGetList(session, out uint needed, ref count, apps, out uint reasons);
        if (rc == 0) break;
        if (rc != ERROR_MORE_DATA) throw new Win32Exception(rc);
        count = needed;
        apps = new RM_PROCESS_INFO[needed];
    }

    // apps[0] 〜 apps[count-1] に「掴んでいる誰か」が入っている
}
finally
{
    RmEndSession(session);   // ④ 必ず閉じる
}

4.2 何が返ってくるか ── 出力の形とRM_APP_TYPE

完全版(4.3)を WhoLocks.exe C:\App\MyApp.exe のように実行すると、出力は次の形になります。値は説明用の例であり、実際の内容は環境によって異なります。

使用中のプロセス/サービス: 2件 (RebootReasons=0)
  PID=4132   種別=1 再起動可=True  セッション=2 名前=MyApp サービス=
  PID=6284   種別=3 再起動可=True  セッション=0 名前=MyAppAgent サービス=MyAppAgent

まずは、次の3項目を読み分けます。

出力項目 読み取ること
種別(RM_APP_TYPE) Restart Managerがどの方法で終了させる対象なのか
再起動可(bRestartable) False なら、更新後に自動で立ち上がるとは扱えません。サービスでは仕様上、常に True です10
セッション(TSSessionId) アプリが自分と異なるセッションなら、第3.1節の制約を確認します。このフィールドはサービスと重要なシステムプロセスでは使われません10

上のサービス行に セッション=0 と表示されていても、その値をアプリと同じように判定には使いません。10

RebootReasonsはOS再起動が必要な理由です。0は「その理由がない」という値であり、この診断コードが更新を完了したという意味ではありません。

種別の数値と意味は次のとおりです。11

値 名前 意味
0 RmUnknownApp 他のどれにも分類できないアプリ。強制終了でしか止められない
1 RmMainWindow 単独プロセスとして動き、トップレベルウィンドウを持つWindowsアプリ
2 RmOtherWindow 単独プロセスではなく、トップレベルウィンドウも持たないWindowsアプリ
3 RmService Windowsサービス
4 RmExplorer Windowsエクスプローラー
5 RmConsole 単独のコンソールアプリ
1000 RmCritical 停止できないため、インストールの完了にOS再起動が必要。クリティカルプロセス、権限不足、Restart Managerを開始した本人のプロセス、のいずれか

実務では、0と1000は、Restart Managerによる穏当な終了を前提にできないサインです。1・5にはそれぞれ WM_QUERYENDSESSION / CTRL_C_EVENT で終了を求め、3はSCM経由で停止します。アプリ側の応答と、実際に終了できたかの確認も必要です。

4.3 完全版

C#からのP/Invokeを含めた完全版です。.NET Framework 4.8 / .NET 8で使用できる形になっています。Windows上で使う診断コードで、コマンドライン引数には調べたいファイルを指定します。相対パスは登録前に完全パスへ変換します。

// 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で
        }
    }
}

戻り値・バッファ・OS再起動理由を別々に確認する

エラーはAPIの戻り値で確認します。Restart Manager APIはWin32エラーコードを直接返すため、GetLastError は使いません。DllImport に SetLastError = true を付ける必要もありません。

一覧のバッファは、必要なら確保し直します。RmGetList は不足時に ERROR_MORE_DATA(234)と必要件数を返します。問い合わせの間にもプロセス数が増え得るため、問い合わせ→確保→取得をループで行います。2

列挙の成功と、OS再起動が不要なことは別です。lpdwRebootReasons が0(RmRebootReasonNone)以外なら、アプリ終了だけでは足りず、OS再起動が必要という判定です。2

構造体の文字列を ByValTStr でマーシャリングする方法など、P/Invoke全般の注意点は「C#からWin32 APIを安全に呼ぶ ── P/Invoke実務ガイド」を参照してください。

診断から更新処理へ広げる場合

RmShutdown / RmRestart のP/Invokeを追加すれば、「列挙→穏当に終了→差し替え→再起動」という独自アップデーターの骨格になります。ただし、RmShutdown を呼べるのは、自分で RmStartSession を開始したインストーラーだけです。MSIのカスタムアクションでは、MSI自身がセッションを管理するため呼んではいけません。5

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

Restart Managerに穏当な終了を任せるには、アプリ側の協力が必要です。強制終了を指定しない限り、応答しないアプリを問答無用に終了できるわけではありません。アプリ側が対応していなければ、結局「使用中のファイル」ダイアログでユーザーの手を煩わせます。6

準備は、再起動の登録→終了確認への応答→実際の保存と終了の3段階です。

(1) RegisterApplicationRestartで再起動を登録する

起動直後に、再起動時のコマンドライン引数をOSへ登録します。Restart Managerで再起動するアプリには、この登録が必要です。7

確認項目 条件
登録するコマンドライン exe名は含めません。OSが付加します。最大長は RESTART_MAX_CMD_LINE です
起動後の時間 再起動ループ防止のため、60秒以上実行していることが必要です
再起動の契機 クラッシュ・ハング時はユーザー同意が必要ですが、更新による再起動は自動です
クラッシュ・ハングを除外する場合 RESTART_NO_CRASH と RESTART_NO_HANG を指定し、更新時の再起動に絞ります

次の例はアプリへ組み込む抜粋です。P/Invoke宣言はクラス内に、呼び出しは起動処理などのメソッド内に置きます。/restoredを受け取って実際に状態を復元する処理は、アプリ側で用意します。

[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には、まだ終了せずに応答する

Restart Managerは、WM_QUERYENDSESSION(lParam=ENDSESSION_CLOSEAPP)で「閉じてもよいか」を確認します。準備ができていれば TRUE を返しますが、ここではまだ終了しません。他のアプリの準備が整っていない可能性があるためです。FALSE で終了を拒否しても、強制指定時は終了させられるため、拒否に頼る設計は避けます。6

更新時に RegisterApplicationRestart を呼び直せる最後のタイミングも、このメッセージの処理中です。開いていた帳票のIDなど、復元に必要な状態をコマンドライン引数に含めて再登録すれば、再起動後に元の画面へ戻す手掛かりになります。7

(3) WM_ENDSESSIONで保存し、終了する

実際の終了指示は WM_ENDSESSION(wParam=TRUE、lParam=ENDSESSION_CLOSEAPP)です。未保存データの自動保存、作業状態のシリアライズ、接続のクローズを行い、タイムアウト内に終了します。強制指定時はアプリ30秒という制約があるため、終了時にすべてを任せず、平常時から定期的にユーザーデータを保存しておくことも重要です。63

コンソールアプリには、WM_ メッセージの代わりに CTRL_C_EVENT が届きます。SetConsoleCtrlHandler、C#なら Console.CancelKeyPress を入口に、保存と終了を行います。6

WPF・WinFormsでは、イベントと復元処理をつなぐ

WPFの Application.SessionEnding、WinFormsの Form.FormClosing(CloseReason.WindowsShutDown)が入口です。ENDSESSION_CLOSEAPP のフラグまで見て、OS再起動とアプリだけの終了・再起動を区別する場合は、ウィンドウプロシージャをフックします。

再起動後のプロセスが、終了しかけの前回の自分と衝突しないよう、Mutexによる多重起動防止も合わせて見直します。詳しくは「Windowsアプリの多重起動防止」を参照してください。

この3点セットと復元処理を整えることで、「全員に閉じてもらう」更新から、自動で閉じ、更新後に元の画面へ復帰する更新へ近づけられます。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でOS再起動時の置換を予約する

サービスのホストプロセスやシェル拡張など、どうしても止められず、リネームでも対応できない場合の最終手段です。MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT で、OS再起動時の移動・削除を予約します。4

予約は HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations に記録され、次回起動の早い段階、AUTOCHK直後・ページファイル作成前に、登録順で実行されます。インストーラーの「再起動が必要です」の背後にある代表的な処理です。

確認項目 条件・注意点
呼び出し権限 管理者グループまたはLocalSystemのコンテキストが必要です
移動先のボリューム MOVEFILE_COPY_ALLOWED と併用できないため、同一ボリューム内に限ります
置換先が既に存在する場合 MOVEFILE_REPLACE_EXISTING も指定します。指定しないと、予約できても再起動時の置換が行われません
関数の戻り値 予約できたかどうかです。再起動時に実際の置換が成功したかは、この戻り値では分かりません

特に最後の点を混同しないでください。予約成功を、更新完了として扱わない設計が必要です。4

また、この予約はネットワークが利用可能になる前に実行されるため、リモート共有上のファイルには使えません。冒頭の共有環境の問題へ、そのまま適用できる方法ではない点にも注意してください。4

どの方式でも、サービス更新と更新ファイルの検証は別に設計する

サービス自身の差し替えは、更新用プロセスへ分離します。SCMでStop→差し替え→Startを行うプロセス、または更新専用サービスを用意します。基本は「Windowsサービスの作り方と運用」で扱っています。

署名検証・ダウンロード経路の保護・失敗時のロールバックも必要です。更新機構の自作は「任意のexeを配って実行させる仕組み」の自作でもあります。検証を省けば更新機構自体が攻撃経路になります。詳しくは「自動アップデートのセキュリティ設計 - HTTPSだけでは足りない理由」を参照してください。

7. 更新方式の判断表

最後に、配布方式と運用条件を合わせて選びます。更新頻度、作業を止められる時間、MSIを維持する必要があるかが判断の軸です。

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

迷ったら、まずMSI + Restart Manager統合を検討してください。OS標準の仕組みを利用でき、アプリ側の3点セットは、後で独自アップデーターへ移行する場合にもそのまま活かせます。

8. まとめ

占有者の確認、終了・復元、更新方式の選択を分けることが、「ファイル使用中」問題を整理する出発点です。診断なら RmGetList だけで始められます。更新まで行う場合は、RmShutdown で終了を確認し、差し替え後に RmRestart で再起動します。MSIなら、この標準連携を先に利用します。

アプリ側は、RegisterApplicationRestart による再起動登録、WM_QUERYENDSESSION への応答、WM_ENDSESSION での保存と終了を準備します。60秒の再起動条件、強制終了時のアプリ30秒・サービス20秒、セッション境界を忘れず、元の状態へ復元する処理まで含めて設計してください。

作業を止めたくない場合は、同一ボリューム内のリネームを利用するrename-then-replaceで、次回起動から新版へ切り替えます。旧ファイルの掃除と新旧混在への対応は必要です。これも使えない場合に、管理者権限で MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT を使います。予約の成功と置換の成功は別です。どの方式でも、署名検証・経路保護・ロールバックを更新機構の一部として扱います。

関連記事

関連する相談領域

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

参考リンク

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

  2. 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

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

  4. 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 ↩4 ↩5 ↩6

  5. 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

  6. 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 ↩8

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

  8. Microsoft Learn, CreateFileW function. FILE_SHARE_DELETEが後続の削除・リネーム要求を許可すること、既存ハンドルの共有モードと競合するアクセスが共有違反になることについて。 ↩

  9. Microsoft Learn, WTSQueryUserToken function (wtsapi32.h). セッションIDを指定してログオン中ユーザーのプライマリアクセストークンを取得する関数であること、呼び出しにはLocalSystemアカウントのコンテキストで動作していることとSE_TCB_NAME特権が必要であること、高度に信頼されたサービス向けでトークンを漏らさないよう注意し取得したハンドルは必ずCloseHandleで閉じるべきこと、セッションIDの列挙にWTSEnumerateSessionsを使えることについて。 ↩

  10. Microsoft Learn, RM_PROCESS_INFO structure. bRestartableがサービスでは常にTRUEであること、TSSessionIdはサービスと重要なシステムプロセスでは使われないことについて。 ↩ ↩2 ↩3

  11. Microsoft Learn, RM_APP_TYPE enumeration (restartmanager.h). RM_PROCESS_INFO構造体が示すアプリケーションの種別として、RmUnknownApp(0、他に分類できず強制終了でしか停止できない)、RmMainWindow(1、トップレベルウィンドウを持つ単独プロセスのWindowsアプリ)、RmOtherWindow(2、単独プロセスでもトップレベルウィンドウも持たないWindowsアプリ)、RmService(3、Windowsサービス)、RmExplorer(4、Windowsエクスプローラー)、RmConsole(5、単独のコンソールアプリ)、RmCritical(1000、プロセスを停止できないためインストールの完了にシステム再起動が必要。クリティカルプロセス、権限不足、Restart Managerを開始したインストーラー自身のいずれかが理由)が定義されていることについて。 ↩

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

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

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

よくある質問

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

実行中の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 ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る