사용 중인 exe/DLL을 어떻게 교체할까 ── Restart Manager와 자동 업데이트의 「파일 사용 중」 문제

· · Restart Manager, 자동 업데이트, 인스톨러, MSI, Windows API, C#, P/Invoke, 배포

「앱을 업데이트하니 전원 X 버튼으로 닫아 주세요, 라고 업데이트할 때마다 사내 방송을 하고 있다」「야간에 파일 서버상의 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에서 저장되지 않은 데이터를 대피시킨 뒤 종료합니다. 이 세 가지를 구현한 앱은 「업데이트를 위해 닫히더라도 업데이트 후 원래대로 다시 실행」되게 됩니다.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 같은 업데이트 프레임워크도 본질적으로는 이 특성 위에서 성립합니다. 주의할 점은 세 가지입니다. 이름 변경은 같은 볼륨 내에서만 사용할 수 있다는 점(다른 볼륨으로의 이동은 복사+삭제가 되어 삭제에서 실패합니다), .old 파일의 정리를 설계에 포함해야 한다는 점, 그리고 「지금 동작 중인 기존 프로세스」는 어디까지나 이전 버전인 채로 계속 동작하기 때문에 신구 프로세스가 공존하는 시간대가 있다는 점입니다.

참고로 「애초에 누가 붙잡고 있는가」를 장애 조사 차원에서 직접 알아내고 싶다면, Process Explorer나 Handle 명령으로 핸들·로드된 DLL을 검색하는 것이 지름길입니다. 절차는 같은 날 공개한 자매 기사 「Process Explorer / Handle / VMMap으로 행(hang)과 누수 추적하기」에 정리했습니다. 이 기사에서는 이를 프로그램에서, 즉 업데이터 자체에 내장하는 방법으로 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 세션을 닫는다

「언제 파일을 교체하는가」가 가장 틀리기 쉬운 부분이므로 시간축으로 정리해 둡니다. 교체해도 되는 것은 RmShutdown이 반환된 후, RmRestart를 호출하기 전의 그 짧은 순간뿐입니다.

사용 중인 앱(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

알아 두어야 할 사양이 몇 가지 있습니다.

  • 정지 순서와 재시작 순서. 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

3.1 세션 경계를 어떻게 넘을까

「야간에 서비스에서 업데이트를 흘려보낸다」는 설계에서 반드시 부딪히는 것이 이 제약입니다. LocalSystem(세션 0)에서 로그온 중인 사용자 세션의 앱은 종료·재시작할 수 없습니다.2 회피책은 「세션 0에서 손을 뻗는」 것이 아니라, 대상 세션 안에 손발을 두는 것으로 귀결됩니다. 현실적인 선택지는 세 가지입니다.

방법 무엇을 하는가 적합·부적합
(A) 태스크 스케줄러로 대상 사용자로 실행 업데이트 에이전트 태스크를 대상 사용자 계정으로 「사용자가 로그온해 있을 때만 실행」하도록 등록하고, 업데이트 신호로 실행한다 가장 손쉽다. 로그온 중인 사용자를 특정할 수 있는 사내 단말에 적합
(B) 로그온 시 상주 에이전트를 실행 각 사용자 세션에서 작은 상주 프로세스를 동작시키고, 공유 폴더나 이벤트로 업데이트 신호를 기다린다 대상 사용자가 정해져 있지 않거나 복수 동시 로그온이어도 유효하다. 상주 프로세스의 유지보수 비용이 든다
(C) 서비스에서 사용자 세션으로 프로세스를 일으킨다 LocalSystem 서비스가 대상 세션의 토큰을 획득하고, 그 토큰으로 업데이터를 실행한다 자유도는 가장 높지만, 구현과 권한 관리가 가장 무겁다

(A)는 schtasks로 이렇게 등록합니다. /RU에 대상 사용자를, /IT를 붙여 「그 사용자가 로그온해 있을 때만 대화형으로 실행하는」 태스크로 만드는 것이 요점입니다.

:: 업데이트 에이전트를 "대상 사용자의 세션 안에서" 동작시키는 태스크를 등록한다
:: 자격 증명 지정(/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)를 선택하는 경우에는 WTSGetActiveConsoleSessionIdWTSEnumerateSessions로 대상 세션 ID를 구하고, WTSQueryUserToken으로 그 사용자의 프라이머리 토큰을 얻은 뒤, CreateProcessAsUser로 업데이터를 실행합니다. WTSQueryUserToken 호출에는 LocalSystem 계정으로 동작하고 있을 것과 SE_TCB_NAME 특권이 필요하며, 공식 문서에도 「고도로 신뢰받는 서비스용」이며 「토큰이 유출되지 않도록 주의하고, 사용이 끝나면 반드시 핸들을 닫을 것」이라고 명시되어 있습니다.8 다루는 방법을 잘못하면 권한 상승의 구멍이 되므로, (A)나 (B)로 충분하다면 무리해서 선택할 필요는 없습니다.

어느 방법이든, 세션 안에서 동작하는 프로세스가 RmStartSession부터 RmRestart까지를 담당한다는 구도는 같습니다. 세션 0의 서비스는 「업데이트 파일을 배포한다」, 「신호를 보낸다」까지만 담당합니다.

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 / 자체 updater의 판단표」에서 다루고 있습니다.

4. C#으로 「누가 붙잡고 있는가」를 출력하기 ── RmGetList 진단 코드

Restart Manager 중에서 업데이트 처리를 작성하지 않는 사람에게도 유용한 것이 RmGetList입니다. 「이 파일을 사용 중인 프로세스와 서비스 목록」을 API로 얻을 수 있어서, 업데이터의 오류 메시지를 「파일이 사용 중입니다」에서 「경리 단말의 MyApp.exe(PID 4132)가 붙잡고 있습니다」로 바꿀 수 있습니다.

4.1 골격 ── 호출하는 것은 네 개뿐

P/Invoke 선언과 구조체 정의를 제외하면 처리의 본체는 이것뿐입니다. 먼저 이 형태를 머릿속에 넣은 뒤, 다음의 완전판을 읽어 주세요.

// 【골격】구조체 정의와 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 재시작 가능=False 세션=0 이름=MyAppAgent 서비스=MyAppAgent

읽는 방법은 세 가지입니다. 유형(RM_APP_TYPE)에 따라 종료시키는 방법이 결정되고, 재시작 가능(bRestartable)이 False면 업데이트 후 자동으로 다시 실행되지 않으며, 세션(TSSessionId)이 자신과 다르면 3장의 「세션을 넘나들 수 없다」는 제약에 걸립니다.

유형의 숫자와 의미는 다음과 같습니다.9

이름 의미
0 RmUnknownApp 다른 어느 것에도 분류할 수 없는 앱. 강제 종료로만 멈출 수 있다
1 RmMainWindow 단독 프로세스로 동작하며 최상위 창을 가진 Windows 앱
2 RmOtherWindow 단독 프로세스가 아니고 최상위 창도 갖지 않는 Windows 앱
3 RmService Windows 서비스
4 RmExplorer Windows 탐색기
5 RmConsole 단독 콘솔 앱
1000 RmCritical 정지할 수 없어 설치 완료를 위해 OS 재부팅이 필요. 크리티컬 프로세스, 권한 부족, Restart Manager를 시작한 본인 프로세스 중 하나

실무에서는 0과 1000이 나온 시점에 「온건한 업데이트는 무리」라고 판단할 수 있습니다. 1·5라면 3장의 작법(WM_QUERYENDSESSION / CTRL_C_EVENT)으로 닫을 수 있고, 3이라면 서비스 제어 관리자를 통해 정지됩니다.

4.3 완전판

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})");

            // 필요 건수를 조회 → 배열 확보 → 취득. 두 호출 사이에 프로세스가
            // 늘어나면 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에서
        }
    }
}

요점은 세 가지입니다. 첫째, 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 실무 가이드(DllImport / LibraryImport / CsWin32)」를 참조해 주세요.

이 코드에 RmShutdown/RmRestart라는 두 개의 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 앱의 중복 실행 방지 ── 네임드 Mutex와 재실행 시 창 활성화」).

이 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

어느 패턴이든 공통되는 주의점이 두 가지 있습니다. 하나는 서비스의 업데이트로, Windows 서비스는 스스로 자신을 교체할 수 없기 때문에 「SCM으로 Stop → 교체 → Start」를 수행하는 업데이트용 프로세스(또는 업데이트 전용 서비스)를 분리하는 것이 정석입니다(서비스 설계의 기본은 「Windows 서비스 만드는 방법과 운영 ── 태스크 스케줄러와의 구분 사용부터 BackgroundService의 서비스화까지」). 다른 하나는 업데이트 파일의 검증입니다. 교체 구조를 직접 만든다는 것은 「임의의 exe를 배포해서 실행시키는 구조」를 직접 만드는 것이기도 하며, 서명 검증이나 다운로드 경로 보호를 생략하면 업데이트 메커니즘 자체가 공격 경로가 됩니다. 이 논점은 「자동 업데이트 기능의 보안 기본 - 나쁜 패턴과 베스트 프랙티스」에서 독립적으로 다루고 있습니다.

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 9

  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

  8. Microsoft Learn, WTSQueryUserToken function (wtsapi32.h). 세션 ID를 지정해 로그온 중인 사용자의 프라이머리 액세스 토큰을 취득하는 함수라는 점, 호출에는 LocalSystem 계정 컨텍스트로 동작하고 있을 것과 SE_TCB_NAME 특권이 필요하다는 점, 고도로 신뢰받는 서비스용이며 토큰이 유출되지 않도록 주의하고 취득한 핸들은 반드시 CloseHandle로 닫아야 한다는 점, 세션 ID의 열거에 WTSEnumerateSessions를 사용할 수 있다는 점에 대해. 

  9. 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 응답을 구현해 두면, 업데이트 후 앱이 원래대로 다시 실행되는 「무중단에 가까운 업데이트」를 실현할 수 있습니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기