Завершение работы Windows со стороны приложения — уведомления, перезагрузка и отключение питания

· Обновлено: · · Windows, Завершение работы, Разработка под Windows, Службы Windows, ПК оборудования, Сохранность данных, Длительная работа, UPS

История изменений (первая версия, опубликована 21 Aug 2026)
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.22176541)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Го Комура (2026). Завершение работы Windows со стороны приложения — уведомления, перезагрузка и отключение питания. KomuraSoft LLC. https://comcomponent.com/ru/blog/windows-shutdown-handling-for-apps/

DOI (зарегистрированный архив)
10.5281/zenodo.22176541
DOI (последняя зарегистрированная версия)
10.5281/zenodo.22176542

«Ночная перезагрузка Windows Update испортила файл, в который мы писали измерения.» «Выход из системы на общем ПК стирает то, что я редактировал.» Чтобы такого не случалось, завершение работы нельзя считать исключением: запрос выйти, пришедший снаружи, нужно проектировать как штатное поведение приложения.

Одного кода, который принимает уведомление о выходе, недостаточно. Времени после уведомления мало, а внезапное отключение питания не несёт уведомления вовсе. Центральная идея статьи — сделать штатное сохранение, короткие завершающие действия и восстановление при следующем запуске одним непрерывным целым.

Для сотрудников ИТ малых и средних компаний и разработчиков Windows-приложений, особенно тех, кто работает с ПК оборудования и долгоживущими приложениями, статья проходит выбор пути уведомления, реализацию, восстановление после перезагрузки, подготовку к отключению питания и проверку результата. Техническая основа — первичные источники Microsoft Learn на август 2026 года, на которые опирался оригинал.

1. Сначала вывод — уменьшите объём, который нужно сохранить, до того как ждать уведомления

Держите приложение в состоянии, из которого за несколько секунд после уведомления можно выйти, и сделайте так, чтобы следующий запуск мог восстановиться даже когда уведомления нет. Это базовая политика обработки завершения работы.

Вместо того чтобы сохранять большой объём данных после уведомления о выходе, сохраняйте на каждой вехе работы и оставляйте на выходе небольшую дельту. Для GUI-приложений и закрытия консоли ключевая цифра — около 5 секунд. У служб другой льготный срок, но ни один из них не гарантирован целиком.123

1.1. Выберите путь уведомления для своего приложения

Тип приложения Куда приходит запрос на выход Что сделать правильно в первую очередь Раздел
GUI-приложение Win32 WM_QUERYENDSESSION и WM_ENDSESSION На запрос сразу отвечать TRUE как правило. Завершающие действия делать после подтверждения выхода в WM_ENDSESSION Раздел 3
WinForms / WPF FormClosing / SessionEnding, плюс хук сообщений где нужно Оба события относятся к этапу запроса. Завершающие действия, которые нельзя отменить, переносите на подтверждённое уведомление Раздел 3
Обычное консольное приложение SetConsoleCtrlHandler и аналоги Закрытие и выход из системы / завершение работы уведомляются при разных условиях Раздел 5
Служба Windows SHUTDOWN / PRESHUTDOWN от SCM Объявите флаг принимаемых команд и сразу вернитесь из обработчика управления Раздел 6
Generic Host / Worker Service IHostApplicationLifetime и StopAsync Сосредоточьте завершающие действия на пути остановки Host и явно задайте ShutdownTimeout Разделы 5 и 6

В .NET не опирайтесь только на AppDomain.ProcessExit. Начиная с .NET 10 среда не даёт обработчик по умолчанию для CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT. Этот путь внешнего сигнала отделён от обычного выхода вроде возврата из Main. Подробности — в разделе 5.2.4

1.2. Вне обработки уведомлений нужны ещё две подготовки

Когда уведомление приходит, не спрашивайте пользователя «Сохранить?»; закончите короткими завершающими действиями и выйдите. Временную блокировку для работы, которую действительно нельзя прервать, разбирает раздел 4, автоматическое восстановление после перезагрузки — раздел 7.

Другая подготовка — оставить данные, которые ещё можно прочитать после отключения питания без уведомления. Запись во временный файл, сброс, подмена, резервная копия и проверка при запуске собраны в разделе 8. Когда путь уведомления реализован, переходите к проекту сохранения и восстановления в разделе 8 и к проверке в разделе 9.

Проект сохранения, общий для уведомлений о выходе и отключения питанияШтатное сохранение держит несохранённую дельту маленькой; при уведомлении следуют короткие завершающие действия, а без него сохранённые данные проверяют и восстанавливают до возобновления работыДаНетСохранять на каждой вехеЕсть уведомление при выходе?Сохранить только оставшуюся дельту и выйтиЗавершающие действия на месте невозможныПроверить и восстановить при следующем запускеВозобновить работу

Рис. 1: Вместо того чтобы напрягаться только при уведомлении, сделайте штатное сохранение до следующего запуска одним непрерывным процессом.

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 14, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Сначала различите — выход из системы, завершение работы, перезагрузка и отключение питания

2.1. Смотрите на сеанс пользователя и на ядро отдельно

Если разделить «что заканчивается» на две части, обработку проще организовать. Сеанс пользователя — область, в которой работают приложения этого пользователя. Ядро и драйверы, напротив, принадлежат стороне ОС и продолжают работать после выхода пользователя.

Операция Сеанс пользователя Ядро и драйверы Уведомления
Выход из системы Заканчивается Продолжают работать GUI получает запрос на выход и подтверждённое уведомление. Службы при выходе из системы не останавливаются
Завершение работы при включённом быстром запуске Заканчивается Сохраняет состояние гибернации в hiberfil.sys Уведомления GUI о конце сеанса и уведомление службам о завершении работы
Перезагрузка Заканчивается Заканчивается полностью и в следующий раз делает полную загрузку То же, что выше
Внезапное отключение питания Теряется мгновенно Теряется мгновенно Уведомления нет

В GUI WM_QUERYENDSESSION бит ENDSESSION_LOGOFF в lParam указывает на выход из системы. Если lParam равен 0, это завершение работы или перезагрузка, и их нельзя различить. Считайте lParam битовой маской.1

Для несохранённых данных приложения выход из системы и завершение работы требуют одной и той же подготовки. Не разделяйте их на «при выходе из системы сохранять не нужно»; направляйте оба в общую процедуру сохранения. Это, однако, не делает условия уведомления консольных приложений и служб одинаковыми; проверяйте пути в разделах 5 и 6 для каждого.

Думайте о выходе из системы и выходе системы отдельноВыход из системы заканчивает приложения пользователя, а службы продолжают работать; завершение работы или перезагрузка системы останавливает и службы; отключение питания не уведомляет никогоВыход из системыПриложения пользователя выходятСлужбы продолжают работатьЗавершение работы или перезагрузкаСлужбы тоже останавливаютсяОтключение питанияУведомления нет никому

Рис. 2: Выход из системы позволяет прогнать обработку выхода GUI, но это не считается проверкой остановки службы.

2.2. Почему «завершение работы не чинит, а перезагрузка чинит»

На клиентских ОС начиная с Windows 8 быстрый запуск по умолчанию включён на многих ПК, которые поддерживают гибернацию. В этой конфигурации завершение работы выводит пользователя, но состояние ядра и драйверов устройств сохраняется в файл гибернации и восстанавливается при следующей загрузке. Отключение питания не обязательно значит, что всё состояние ОС было сброшено.5

Это поведение условно. Там, где гибернация отключена (powercfg /hibernate off), где быстрый запуск выключен политикой или в параметрах электропитания, и на Windows Server получается традиционное полное завершение. Проверяйте параметры электропитания и через powercfg /a смотрите, доступен ли быстрый запуск.

«Перезагрузка», напротив, всегда выполняет полный цикл загрузки. В процедуре изоляции сбоев драйверов пишите явно «перезагрузить», а не «выключить и включить».5

Быстрый запуск против полной загрузкиЗавершение работы при включённом быстром запуске сохраняет и восстанавливает состояние ядра и драйверов, а полное завершение или перезагрузка инициализируют всё полной загрузкойДаНетЗавершение работыИспользовать быстрый запуск?Увести ядро и драйверы в гибернациюВосстановить состояние при следующем запускеПолное завершение работыИнициализировать полной загрузкой в следующий разПерезагрузка

Рис. 3: Отключение питания — это не то же самое, что сброс ядра и драйверов.

Из командной строки shutdown /s запрашивает явное полное завершение, а shutdown /s /hybrid — гибридное поведение. Shutdown.exe по умолчанию делает полное завершение, поэтому важно не считать его тем же, что пункт «Завершение работы» на экране.5

Отключать быстрый запуск как обход не рекомендуется. Стройте приложение так, чтобы оно работало и при включённом, и при выключенном. Например, не оценивайте «накопленное время работы» устройства только по времени загрузки ОС; в проекте учитывайте, что состояние ядра может переноситься.

3. GUI-приложения — отделите запрос от подтверждённого выхода

3.1. WM_QUERYENDSESSION — вопрос, WM_ENDSESSION — результат

Приложение, у которого есть окно и очередь сообщений, уведомляется о конце сеанса в два этапа.1

Сообщение Смысл Что делать
WM_QUERYENDSESSION Запрос «можно выходить?» Сразу вернуть TRUE как правило. DefWindowProc тоже по умолчанию даёт TRUE
WM_ENDSESSION, wParam=TRUE Конец сеанса подтверждён Сделать короткие завершающие действия: сохранить, отключить и т. п.
WM_ENDSESSION, wParam=FALSE Конец сеанса отменён Приложение продолжает работать. Не делайте завершающие действия, которые допустимы только после подтверждения выхода

Сам возврат TRUE на запрос не значит, что выход уже решён. Другое приложение может отказать, и выход отменяется. Если на этом этапе отключить или отдать ресурсы, которые ещё нужны, приложение, которое не вышло, больше не сможет работать. Поэтому завершающие действия переносят на подтверждённое уведомление.1

Запрос GUI и результат выходаДаже после возврата TRUE на WM_QUERYENDSESSION другое приложение может отменить выход, поэтому завершающие действия после подтверждения выполняются только когда wParam у WM_ENDSESSION равен TRUEДаНетWM_QUERYENDSESSIONСразу вернуть TRUE как правилоWM_ENDSESSIONwParam равен TRUE?Завершающие действия после подтвержденияВыход отменён; продолжать работу

Рис. 4: Не путайте ответ, который разрешает выход, с уведомлением, что выход подтверждён.

Есть случаи, когда можно вернуть FALSE и отказать в выходе, но правило — уважать намерение пользователя выйти. Приложение, которое отказывает, показывают как препятствующее завершению работы. У консольных приложений и приложений без видимого окна тоже есть ограничения: в обычной конфигурации приложение, которое не отвечает в течение 5 секунд, может быть завершено автоматически. Считайте блокировку исключительной обработкой раздела 4 и не используйте её для обычного сохранения.6

3.2. Около 5 секунд — это не гарантия, что успеете сохранить

Если задержать ответ примерно на 5 секунд на этапе WM_QUERYENDSESSION или WM_ENDSESSION, система показывает экран со списком приложений, которые препятствуют завершению работы, и пользователь может выбрать принудительное завершение. После принудительного завершения возможности дописать сохранение уже нет.6

Мера — сохранять штатно и уменьшать дельту на выходе. Несохранённое рабочее состояние кладите во временное место и восстанавливайте при следующем запуске. Не проектируйте приложение так, чтобы при завершении работы показывать диалог подтверждения и ждать. Обычное подтверждение выхода и запрос на выход от ОС — разные вещи.1

Держите работу на выходе маленькойПроект, который сохраняет штатно, оставляет на выходе маленькую дельту, а проект, который копит в памяти до выхода, не укладывается в короткий льготный срок и рискует потерей через принудительное завершениеСохранять штатноДельта, оставшаяся на выходе, маленькаяВыйти с короткими завершающими действиямиКопить в памяти до выходаСохранить всё на выходеСлишком мало льготного срока; риск принудительного завершения

Рис. 5: Подготовка к выходу за несколько секунд идёт до уведомления о выходе.

3.3. В WinForms и WPF отделите сохранение от завершающих действий, которые нельзя отменить

В WinForms соответствие — FormClosing с CloseReason.WindowsShutDown; в WPF — Application.SessionEnding. WPF можно зарегистрировать и через атрибут SessionEnding в XAML, и обработать, переопределив OnSessionEnding.

Оба, однако, — события этапа запроса. Здесь допустимо самое большее «идемпотентное сохранение снимка»: безвредно, если выход отменят, и даёт тот же результат, сколько бы раз ни выполнилось. Работу, которая возможна только после подтверждения выхода, например отключение, делают, принимая WM_ENDSESSION с wParam=TRUE в WndProc WinForms или в хуке WPF.

Разделение обработки выхода в WinForms и WPFFormClosing и SessionEnding сохраняют снимок, который безопасен даже если выход отменят, а хук на WM_ENDSESSION с TRUE выполняет завершающие действия, которые нельзя отменитьЭтап запросаFormClosingSessionEndingИдемпотентное сохранение снимкаWM_ENDSESSION с TRUEПринять в WndProc или хукеРабота после подтверждения, например отключение

Рис. 6: Не запихивайте работу после подтверждения в события платформы.

Следующие два примера — часть, которая на этапе запроса сохраняет только рабочее состояние. Примеры кода — выдержки реализации; содержимое функции сохранения, обработка сбоев, регистрация событий и прочее зависят от приложения.

// WinForms: FormClosing вызывается и при завершении работы, и при выходе из системы
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    if (e.CloseReason == CloseReason.WindowsShutDown)
    {
        // Только идемпотентное сохранение снимка. Диалог не показывать.
        // e.Cancel = true (отказ) тоже не ставить.
        SaveWorkingStateToTempFile();
        return;
    }

    // В обычных случаях, например пользователь закрывает кнопкой X, подтверждение здесь нормально
}
// WPF: App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
    base.OnSessionEnding(e);

    // ReasonSessionEnding.Logoff / Shutdown можно различить, но
    // базовая схема — одно и то же сохранение снимка для обоих
    SaveWorkingStateToTempFile();

    // e.Cancel = true не ставить без очень веской причины
}

В обоих случаях направляйте данные, которые используются при обычном выходе, при завершении работы и, если возможно, при сбое, в общую функцию и формат сохранения. Если не создавать отдельный формат для каждого пути сохранения, логика восстановления при следующем запуске тоже может быть одна. Проектирование так, чтобы при сбое оставалась информация, разобрано в проектировании Windows-приложений, которые оставляют логи и дампы при сбое.

4. Блокируйте временно и только для работы, которую нельзя прервать

4.1. Регистрация причины и отказ выйти — разные работы

Работа, которая физически уничтожается, если её прервать, например запись CD или прошивка, — исключение. Зарегистрируйте причину через ShutdownBlockReasonCreate когда работа начинается, и снимите через ShutdownBlockReasonDestroy когда она заканчивается. Зарегистрированная причина показывается на экране со списком приложений, которые препятствуют завершению работы.7

Учтите, однако, что одной регистрации строки причины недостаточно, чтобы остановить завершение работы. Совместите её с флагом «защищено» и только пока он установлен возвращайте FALSE на WM_QUERYENDSESSION. Когда работа заканчивается, снимите и причину, и флаг защиты.

Разделение ролей во временной блокировке выходаТолько пока идёт непрерываемая работа, совмещайте регистрацию причины с отказом на запрос и снимайте оба по завершении; принудительное завершение пользователем всё равно нельзя предотвратитьОтменитьПринудительноНачинается непрерываемая работаЗарегистрировать причину и поставить флаг защитыОбрабатывать на рабочем потокеЗакончить; снять причину и флагТем временем приходит запрос на выходВернуть FALSE и показать причинуРешение пользователяПродолжать работуМожет быть завершено

Рис. 7: Одна регистрация причины — это не отказ, и даже реализация отказа не предотвращает принудительное завершение.

4.2. Держите UI-поток способным принять запрос на выход

Регистрируйте и снимайте причину из потока, который создал целевое окно. Вызовы из других потоков завершаются ошибкой.7 Саму долгую непрерываемую работу, напротив, переносите на рабочий поток. Если UI-поток заблокирован синхронной обработкой, приложение становится «Не отвечает» ещё до того, как будет обработано сообщение, нужное для отказа.

[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, string pwszReason);

[DllImport("user32.dll", SetLastError = true)]
static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd);

// Вызывать из потока, который создал главное окно (вызовы из других потоков завершаются ошибкой)
_criticalOperationInProgress = true;
ShutdownBlockReasonCreate(this.Handle, "Запись данных измерений в файл");
try
{
    // Непрерываемую работу выполнять на рабочем потоке. Синхронный запуск на
    // UI-потоке останавливает насос сообщений, и приложение принудительно продолжают как
    // «Не отвечает» до того, как сможет выполниться код отказа WM_QUERYENDSESSION ниже
    await Task.Run(() => WriteMeasurementData());
}
finally
{
    ShutdownBlockReasonDestroy(this.Handle);
    _criticalOperationInProgress = false;
}

// Кроме того, возвращать FALSE на WM_QUERYENDSESSION только пока защищено, чтобы отказать
protected override void WndProc(ref Message m)
{
    const int WM_QUERYENDSESSION = 0x0011;
    if (m.Msg == WM_QUERYENDSESSION && _criticalOperationInProgress)
    {
        m.Result = IntPtr.Zero;   // Отказ. Зарегистрированная строка причины показывается в полноэкранном UI
        return;
    }
    base.WndProc(ref m);
}

Этот пример — каркас, который совмещает регистрацию причины, обработку на рабочем потоке и отказ на запрос. Обработка ошибок, включая сбои API, нужна отдельно. Держите строку причины короткой и конкретной и ограничивайте регистрацию временем, когда работу действительно нельзя прервать, а не держите её зарегистрированной всю жизнь приложения.7

Даже тогда пользователь может выбрать принудительное завершение. Есть и пути принудительного выхода вроде ENDSESSION_CRITICAL, поэтому никогда не делайте возможность блокировать предпосылкой сохранности данных. Подготовка к случаю, когда остановить не удалось, — проект сохранения и восстановления раздела 8.6

5. Консольные приложения и .NET — проверяйте условия, при которых приходят уведомления

5.1. Уведомления и льготные сроки, которые принимает SetConsoleCtrlHandler

В консольном приложении управляющие сигналы приходят обработчику, зарегистрированному через SetConsoleCtrlHandler. В отличие от обработки сообщений GUI, обработчик выполняется на отдельном потоке.2

Сигнал Когда возникает Льготный срок по умолчанию
CTRL_C_EVENT / CTRL_BREAK_EVENT Ctrl+C / Ctrl+Break Явного тайм-аута нет
CTRL_CLOSE_EVENT Закрытие консоли, «Снять задачу» в Диспетчере задач и т. п. Около 5 секунд
CTRL_SHUTDOWN_EVENT Процессы служб при завершении работы системы Около 20 секунд

Принудительное завершение процесса со вкладки «Подробности» Диспетчера задач и подобное — немедленное завершение без уведомления и вне этой таблицы.2

Не проектируйте консольное приложение в интерактивном сеансе так, чтобы оно ждало CTRL_LOGOFF_EVENT или CTRL_SHUTDOWN_EVENT. Интерактивные приложения завершаются в момент выхода из системы, поэтому на практике эти события могут принять только процессы, работающие как службы. Кроме того, процесс, который загрузил gdi32.dll или user32.dll, считается приложением Windows, и обработчики LOGOFF/SHUTDOWN не вызываются. Официальный обход в этом случае — создать скрытое окно и принимать WM_QUERYENDSESSION / WM_ENDSESSION.28

Условия приёма уведомлений о выходе консолиИнтерактивная консоль может принять уведомление о закрытии, но не может рассчитывать на уведомления о выходе из системы или завершении работы, а даже службе нужен другой путь уведомления, как только она загружает GUI-DLLИнтерактивный сеансСлужбаНетДаКонсольный процессЗакрытие идёт в обработчик управленияЖдать LOGOFF или SHUTDOWN?На это уведомление опираться нельзяЗагружены GUI-DLL?Обработать соответствующий управляющий сигналПринимать через скрытое окно

Рис. 8: Не считайте обработку закрытия консоли обработкой завершения работы.

Следующая выдержка готовится к Ctrl+C и закрытию консоли. Она удерживает делегат, чтобы сборщик мусора его не собрал, и после коротких завершающих действий переходит к обработчику по умолчанию. Одна эта регистрация не гарантирует уведомления о завершении работы интерактивному приложению.

// Консольное приложение: завершающие действия на Ctrl+C и закрытие консоли
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);

delegate bool HandlerRoutine(int ctrlType);   // 2 = CTRL_CLOSE_EVENT

static readonly HandlerRoutine s_handler = OnCtrlEvent;  // Держать ссылку, чтобы сборщик не собрал

static bool OnCtrlEvent(int ctrlType)
{
    // Только завершающие действия, которые укладываются в 5 секунд
    FlushAndCloseDataFile();
    return false;   // Перейти к обработчику по умолчанию; процесс выходит
}

static void Main()
{
    SetConsoleCtrlHandler(s_handler, add: true);
    // ...
}

5.2. Начиная с .NET 10 не кладите завершающие действия только в ProcessExit

Начиная с .NET 10 среда больше не даёт обработчик по умолчанию для CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT. Без собственного обработчика или обработчика из библиотеки верхнего уровня обработка ОС по умолчанию завершает процесс, и на этом пути не срабатывают ни AppDomain.ProcessExit, ни AssemblyLoadContext.Unloading.4

Это изменение поведения по умолчанию для внешних сигналов завершения. Это не значит, что ProcessExit больше не срабатывает при обычном выходе вроде возврата из Main.

Снятие зависимости от обработчика завершения .NET по умолчаниюСтарая среда связывала соответствующие сигналы с ProcessExit, но начиная с .NET 10 обработчик по умолчанию не предоставляется, поэтому используйте путь уведомления модели приложенияНетДаСигнал CLOSE или SHUTDOWNОбработка старой среды по умолчаниюПоднимает ProcessExit и подобныеНет обработки по умолчанию начиная с .NET 10Приложение обрабатывает?Завершено обработкой ОС по умолчаниюЗавершающие действия, которые соответствуют модели

Рис. 9: Различайте обычный выход и завершение внешним сигналом и дайте обработчик, который нужна модели приложения.

Сосредоточьте завершающие действия там, где это соответствует модели приложения. Для GUI это события и подтверждённое уведомление раздела 3; для Generic Host — IHostApplicationLifetime и BackgroundService.StopAsync; для обычного консольного приложения — SetConsoleCtrlHandler или PosixSignalRegistration. Когда используете последнее, выбирайте сигналы, которые соответствуют целевым путям завершения, например SIGINT, SIGTERM и SIGHUP.4

В Generic Host явно задайте HostOptions.ShutdownTimeout. Однако одна настройка на стороне Host не растягивает внешний льготный срок GUI, консоли или SCM. Даже хотя у Ctrl+C нет явного тайм-аута, всё равно нужно готовиться к отключению питания и другим путям завершения. Во всех моделях политика одна: держите состояние сохранённым и держите работу после уведомления маленькой.

Остановка Host и внешний льготный срок выходаОстановка Generic Host сосредоточена в StopAsync, но задание тайм-аута HostOptions не растягивает льготный срок выхода ОС или SCM, поэтому проверяйте обаЗапрос на выход от ОС или SCMПуть остановки HostЗавершающие действия в StopAsyncЗадать время остановки на стороне HostВнешний льготный срок существует отдельноИзмерить, укладывается ли быстро

Рис. 10: Проверяйте лимиты времени на стороне Host и на стороне ОС отдельно и не ограничивайтесь растягиванием настройки.

6. Службы Windows — сразу возвращайтесь из обработчика управления

6.1. Разница между SHUTDOWN и PRESHUTDOWN

Службы не останавливаются при выходе из системы, но останавливаются при завершении работы и перезагрузке. Уведомление приходит от SCM (Service Control Manager), и служба должна объявить флаг, соответствующий коду управления, который хочет принимать.3

Флаг для объявления Код управления, который доставляется Когда использовать
SERVICE_ACCEPT_SHUTDOWN SERVICE_CONTROL_SHUTDOWN Обычное уведомление о завершении работы. Льготный срок по умолчанию около 20 секунд и зависит от WaitToKillServiceTimeout
SERVICE_ACCEPT_PRESHUTDOWN SERVICE_CONTROL_PRESHUTDOWN Доставляется раньше обычного SHUTDOWN. SCM ждёт, пока служба остановится или истечёт настроенный тайм-аут
Этапы уведомления службам о выходеSCM сначала уведомляет службы, которые принимают PRESHUTDOWN, ждёт их остановки или тайм-аута, затем переходит к обычному уведомлению SHUTDOWNНачинается завершение работы системыУведомить службы, принимающие PRESHUTDOWNЖдать остановки или настроенного срокаУведомить службы, принимающие SHUTDOWNПосле льготного срока перейти к выходу системы

Рис. 11: Быть уведомлённым раньше и быть ожидаемым — разные условия; у PRESHUTDOWN тоже есть настроенный срок.

Тайм-аут PRESHUTDOWN задаётся через ChangeServiceConfig2(SERVICE_CONFIG_PRESHUTDOWN_INFO). По умолчанию 10 секунд начиная с Windows 10 Creators Update (сборка 15063) и 3 минуты до того. Если предполагать «PRESHUTDOWN всегда даёт 3 минуты», вы не получите ожидаемого времени.9

Поскольку PRESHUTDOWN задерживает завершение работы всей системы, используйте его только когда действительно нужно. Переписывать обычный WaitToKillServiceTimeout со стороны службы, чтобы растянуть его, тоже не рекомендуется.3

6.2. Отделите приём уведомления от фактической остановки

Обработчик управления должен вернуться в течение 30 секунд, но вместо того чтобы считать это 30 секундами, которыми можно пользоваться, структурируйте так, чтобы подать сигнал остановки и сразу вернуться. Долгую работу отдайте другому потоку и сообщайте SERVICE_STOP_PENDING.3

// Служба Win32: принимать PRESHUTDOWN и оставить работу остановки рабочему потоку
g_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN;

DWORD WINAPI ServiceHandlerEx(DWORD control, DWORD, LPVOID, LPVOID)
{
    switch (control)
    {
    case SERVICE_CONTROL_PRESHUTDOWN:
    case SERVICE_CONTROL_STOP:
        ReportStatus(SERVICE_STOP_PENDING, /*waitHintMs*/ 3000);
        SetEvent(g_stopEvent);   // Сигнализировать рабочему остановиться и сразу вернуться
        return NO_ERROR;
    }
    return ERROR_CALL_NOT_IMPLEMENTED;
}

// Сторона рабочего: если завершающие действия длятся дольше wait hint, продолжайте
// периодически сообщать SERVICE_STOP_PENDING, увеличивая dwCheckPoint. SCM судит
// по wait hint и продвигающемуся checkpoint, что служба «ещё жива и
// продвигается». Если сообщения прекратятся, её считают зависшей и завершение работы может
// идти дальше. Всегда сообщайте SERVICE_STOPPED по окончании

На стороне рабочего, если завершающие действия длятся дольше wait hint, продолжайте сообщать SERVICE_STOP_PENDING, продвигая dwCheckPoint. Если сообщения о прогрессе прекратятся, службу могут счесть зависшей. Сообщение SERVICE_STOPPED по окончании — часть обработки остановки.39

Разделение работы между обработчиком управления службы и рабочимОбработчик управления сообщает stop pending, сигнализирует рабочему и сразу возвращается; рабочий делает завершающие действия и сообщения о прогрессе и в конце сообщает stoppedОбработчик управленияСообщить STOP_PENDINGСигнализировать рабочему остановитьсяОбработчик сразу возвращаетсяРабочий делает завершающие действияСообщать о прогрессе, если длится долгоСообщить STOPPED по окончании

Рис. 12: Не блокируйте поток, который принимает уведомление, долгой обработкой остановки.

6.3. Умейте закончить быстро даже если зависимость остановилась первой

При завершении работы SCM по умолчанию шлёт уведомления без учёта зависимостей служб. Обработка остановки должна справляться со случаем, когда служба-зависимость уже недоступна, и не должна слишком долго ждать ответов сетевых партнёров. Вместо того чтобы тратить время на освобождение памяти и подобное, отдавайте приоритет быстрой фиксации нужных данных.3

Эта политика важна и в связи с ИБП. Чем дольше служба растягивает ожидание, тем труднее завершить остановку всей ОС до того, как сядет батарея. Вместо увеличения льготного срока уменьшайте работу при остановке, сохраняя на каждой вехе.3

Когда .NET Worker Service работает под UseWindowsService, STOP / SHUTDOWN превращаются в остановку Host, которая ведёт к BackgroundService.StopAsync. Стандартная реализация на момент написания оригинала не принимает PRESHUTDOWN, поэтому если он нужен, придётся расширить реализацию обработчика. Политика явно задавать HostOptions.ShutdownTimeout и укладывать сам StopAsync в несколько секунд та же. Общую реализацию см. в Как создать и эксплуатировать службу Windows.

7. Восстановление после перезагрузки — выровняйте регистрацию, данные восстановления и вход

7.1. RegisterApplicationRestart заранее регистрирует путь восстановления

На ПК оборудования и в необслуживаемой работе проект идёт дальше сохранения и выхода: он покрывает возобновление работы после перезагрузки. RegisterApplicationRestart — API, который регистрирует приложение на перезапуск в каждой из этих ситуаций: сбой (необработанное исключение), «Не отвечает», перезапуск приложения из‑за обновления и перезапуск ОС из‑за обновления.10

Можно указать аргументы командной строки для перезапуска, например открытые файлы или точку восстановления. Однако одна регистрация не значит автоматическое восстановление из любой ситуации.

Что проверить Условие или ограничение
Когда регистрировать До возникновения проблемы. В сценарии обновления обработка WM_QUERYENDSESSION — последний шанс
Предотвращение цикла перезапуска Процесс, который работал меньше 60 секунд, не перезапускается
Сбой или зависание Перезапускается после согласия пользователя. Перезапуск из‑за обновления автоматический
Через перезапуск ОС Инициирующая сторона должна вызвать API завершения с EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS
Повышенный процесс Не подходит для автоматического перезапуска, поэтому нужен отдельный явный путь запуска

Для приложения, которому нужно повышение, проектируйте путь восстановления, держа UI на обычных правах и вынося привилегированную работу в службу, либо через задачу Планировщика заданий с «Выполнять с наивысшими правами» и подобным.10

7.2. Обратный вызов восстановления и ARSO играют разные роли

Если ещё использовать RegisterApplicationRecoveryCallback, WER (Windows Error Reporting) вызывает обратный вызов восстановления при сбое, и можно сохранить данные, с которыми работали. Пока сохранение продолжается, вызывайте ApplicationRecoveryInProgress в пределах зарегистрированного интервала ping и уведомляйте ApplicationRecoveryFinished по окончании. Если сообщения о прогрессе прекратятся, обработку восстановления могут оборвать.

Сохранение и уведомление о прогрессе в обратном вызове восстановленияОбратный вызов восстановления, который вызывает WER, продолжает вызывать ApplicationRecoveryInProgress в пределах интервала ping, пока сохраняет, и уведомляет ApplicationRecoveryFinished по окончанииЕщё нетГотовоЗаранее зарегистрировать обратный вызов восстановленияСбой; WER вызывает егоСохранить данные, с которыми работалиСообщать о прогрессе в пределах интервала pingСохранение закончено?Уведомить, что восстановление законченоМогут оборвать, если сообщения прекратятся

Рис. 13: Не только сохраняйте; уведомляйте WER о прогрессе и завершении.

Замена файлов, которые используются во время обновления, и перезапуск — работа Restart Manager. Это разобрано в Как заменить EXE или DLL, которые используются.

Механизм, который возвращает сеанс пользователя после перезапуска ОС, напротив, — ARSO (автоматический вход Winlogon). Когда Windows Update начинает автоматический перезапуск, он безопасно сохраняет учётные данные последнего интерактивного пользователя и настраивает Autologon, и после перезапуска входит этим пользователем и блокирует экран.11

Регистрация перезапуска, вход и восстановление данныхНа фоне предварительной регистрации перезапуска сбой требует согласия пользователя, а приложениям пользователя после перезапуска ОС нужны восстановление сеанса через ARSO или подобное и восстановление сохранённого состоянияЗарегистрировать перезапуск до возникновения проблемыСбой или «Не отвечает»Получить согласие пользователяПерезапустить приложениеПерезапуск ОС с нужными флагамиВосстановить сеанс через ARSO или подобноеПрочитать сохранённую точку восстановленияПроверить политику и условия запуска

Рис. 14: Дайте не только регистрацию перезапуска приложения, но и путь, которым возвращаются сеанс и рабочее состояние.

shutdown /g — команда, которая запрашивает перезапуск плюс возобновление зарегистрированных приложений. ARSO может быть отключён организационной политикой вроде DisableAutomaticRestartSignOn, поэтому проверяйте его вместе с требованиями необслуживаемого восстановления. Фоновую обработку, которая нужна всегда, лучше запускать как службу Windows, чем делать зависимой от автоматического входа пользователя.11

8. Отключение питания без уведомления — проектируйте сохранение и загрузку парой

8.1. Временный файл, подмена, резервная копия и проверка при запуске как один комплект

Сработавший автомат, вышедший из строя блок питания или выдернутый шнур не несут ни WM_ENDSESSION, ни PRESHUTDOWN. Если перезаписывать исходный файл на месте, прерывание посередине может оставить файл со смешанным старым и новым содержимым.

База — полностью записать во временный файл на том же томе, сбросить и затем подменить. ReplaceFile собирает шаги, соответствующие сохранению в новый файл, откладыванию исходного, переименованию и удалению, и переносит атрибуты вроде времени создания, ACL и альтернативных потоков данных. Заменяемый файл, файл замены и резервная копия должны быть на одном томе. File.Replace в .NET вызывает этот API.12

Сохранение файла и восстановление при следующем запускеЗаписать во временный файл на том же томе, сбросить, подменить, сохранив резерв, затем при запуске проверить основной файл и при необходимости откатиться на резервДаНетВременный файл на том же томеЗаписать до конца и сброситьПодменить; старое содержимое уходит в .bakСледующий запускОсновной файл цел?Читать основной файлОткатиться на резерв

Рис. 15: Реализуйте не только безопасный способ писать, но и способ читать, когда файл сломан.

// Стандартный шаблон сохранения настроек и данных: записать во временный файл, затем подменить, сохранив старое содержимое
public static void SaveAtomically(string path, string content)
{
    string dir = Path.GetDirectoryName(path)!;
    string tmp = Path.Combine(dir, Path.GetRandomFileName());  // Создать на том же томе

    try
    {
        using (var fs = new FileStream(tmp, FileMode.CreateNew, FileAccess.Write))
        using (var writer = new StreamWriter(fs))
        {
            writer.Write(content);
            writer.Flush();
            fs.Flush(flushToDisk: true);   // Эквивалент FlushFileBuffers. Выводит буферы ОС
                                           // на диск (пределы кэша на стороне устройства — в разделе 8.2)
        }

        if (File.Exists(path))
            File.Replace(tmp, path, path + ".bak");  // Вызывает ReplaceFile. Держит старое содержимое как .bak
        else
            File.Move(tmp, path);
    }
    catch
    {
        // Если сбой посередине, не оставлять временный файл. Если периодические сохранения
        // продолжают сбоить, полные копии постепенно заполнят том
        try { File.Delete(tmp); } catch { /* Неудачное удаление уступает исходному исключению */ }
        throw;
    }
}

Хотя этот пример назван SaveAtomically, он не гарантирует атомарность через отключение питания. В обычной работе он оставляет читаемым либо полный старый файл, либо полный новый, но ReplaceFile — многошаговая операция пространства имён, и её атомарность против внезапного отключения питания спецификацией не гарантируется. Именно поэтому вы держите .bak и реализуете процедуру загрузки, которая при запуске проверяет основной файл и откатывается на резерв, если он сломан.12

catch в примере существует, чтобы временные файлы не копились при обычных сбоях и не заполняли том. Он не рассчитывает, что этот код выполнится в момент отключения питания. Для журналов только на дозапись и CSV не применяйте подход подмены целого файла как есть; используйте формат, который учитывает, как он ломается, например «писать одну строку на запись и при загрузке отбрасывать повреждённую последнюю строку».

8.2. Различайте успех WriteFile и достижение диска

Даже когда WriteFile успешен, данные могут ещё сидеть в кэше ОС. Windows обычно пишет в системные буферы и применяет данные к диску отложенной записью. На важных контрольных точках сбрасывайте через FlushFileBuffers или укажите FILE_FLAG_WRITE_THROUGH при CreateFile, чтобы запросить немедленную запись. Метаданные файловой системы тоже кэшируются, поэтому их фиксация включает сброс или write-through.13

Write-through, однако, не то же самое, что «не использовать кэш ОС». Частые вызовы FlushFileBuffers неэффективны, поэтому где нужно рассмотрите сочетание с FILE_FLAG_NO_BUFFERING. На практике реалистичный проект — сбрасывать в точках, которые важны для целостности, например на границах транзакций и непосредственно перед закрытием файла.13

Граница между успехом записи и сохранениемОбычная запись доходит до накопителя из кэша ОС с задержкой; сброс или write-through толкают к фиксации, но ограничения кэша на стороне устройства остаютсяWriteFile успешенМожет быть в кэше ОСОтложенная записьСброс на контрольной точкеПрименено к накопителюПределы кэша на стороне устройства

Рис. 16: Не считайте успех API, фиксацию кэша ОС и устойчивость к отключению питания одним и тем же.

У энергозависимого кэша на стороне устройства тоже есть пределы, и нельзя сказать «мы сбросили, значит это полностью переживает отключение питания на любом железе». Связь диспетчера кэша, отложенной записи и аппаратных кэшей подробно объяснена в Диспетчере кэша: когда WriteFile оказывается на диске?.

8.3. Используйте ИБП, чтобы превратить отключение питания в запланированное завершение работы

Роль ИБП — не устранить отключения, а превратить отключение питания без уведомления в запланированное завершение работы с уведомлением. Нужное условие — такое соотношение:

Время работы батареи ИБП > время обнаружения переключения + завершающие действия приложений и служб + завершение остановки ОС

Переключение между питанием от сети и батареей и низкий остаток заряда сообщаются через PBT_APMPOWERSTATUSCHANGE. GUI принимает это через WM_POWERBROADCAST; служба объявляет SERVICE_ACCEPT_POWEREVENT и затем принимает SERVICE_CONTROL_POWEREVENT в HandlerEx. WM_POWERBROADCAST в обработчик управления службы не доставляется.14

После приёма проверьте ACLineStatus и BatteryLifePercent через GetSystemPowerStatus и переходите к приостановке измерений, сохранению и запросу завершения работы.14

От обнаружения ИБП до завершения остановкиОбнаружить переход ИБП на батарею через соответствующий путь уведомления, проверить состояние питания, перейти к сохранению и завершению работы и спроектировать всю последовательность так, чтобы она укладывалась во время работы батареиОтключение; ИБП переходит на батареюУведомление о питании по подходящему путиПроверить состояние питания и остаток зарядаПриостановить измерения и сохранитьЗапросить завершение работы у ОСЗакончить завершающие действия и выход ОСУложить всю последовательность во время работы

Рис. 17: Укладывайте во время работы ИБП не только приложение, но и время до выхода ОС.

В конфигурациях, где Windows видит ИБП как батарею, например типичный ИБП с USB, стандартные API могут его мониторить. Если у управляющего ПО поставщика есть функция «завершить работу при N% остатка», также проверьте, что её порог согласован со временем завершающих действий. Возобновление после сна или гибернации — отдельная тема; см. Сон, гибернация, Modern Standby и долгоживущие приложения.

9. Проверка — подтвердите уведомления, время и результаты восстановления

9.1. Воспроизводите пути выхода на тестовой машине, а не в продакшене

Не пробуйте сначала на продакшен-ПК оборудования; используйте тестовую машину или виртуальную машину вроде Hyper-V. На виртуальной машине заранее сделайте контрольную точку и повторяйте тесты с тестовыми данными, которые можно потерять.

Операция для пробы Что подтвердить
Выход из системы WM_QUERYENDSESSION → WM_ENDSESSION GUI и обработка сохранения. Не замена проверки остановки службы
shutdown /s /t 0 Поведение при полном завершении работы
shutdown /s /hybrid /t 0 Гибридное поведение в конфигурации, которая использует быстрый запуск
shutdown /r /t 0 Перезагрузка с полной загрузкой и восстановление после
Выключение ВМ Восстанавливается ли следующий запуск даже когда гостевая ОС останавливается без уведомления
Отключение питания на железе, эквивалентном продакшену Устойчивость, включая физический накопитель и контроллер

Выход из системы отличается тем, что установлен бит ENDSESSION_LOGOFF, но это простой способ подтвердить путь уведомления GUI. Не путайте команды полного, гибридного и перезапуска; пробуйте их отдельно.15

Поэтапное расширение проверки завершения работыВ тестовой среде пробовать уведомления GUI и каждую операцию выхода, подтвердить восстановление после внезапной остановки на ВМ, затем проверить устойчивость к отключению питания, включая накопитель, на железе, эквивалентном продакшенуПодготовить тестовую машину и данныеПробовать пути уведомления и операции выходаИзмерить время завершающих действийПодтвердить восстановление после внезапной остановки ВМПроверить на реальном железе, включая накопительПодтвердить данные и восстановление при следующем запуске

Рис. 18: Проверяйте не только то, что приложение нормально выходит, но и что оно может восстановить после внезапной остановки.

Выключение ВМ воспроизводит только внезапную остановку гостя. Оно не воспроизводит потерю энергозависимого кэша физического диска или порчу, зависящую от контроллера, поэтому при поставке как ПК оборудования финальное подтверждение делайте на железе, эквивалентном продакшену.

Пишите метку времени в начале и конце функции завершающих действий и измеряйте, укладывается ли она примерно в 5 секунд для GUI и подобного или в льготный срок, настроенный для службы. Подтверждайте не только то, что выход удался, но и что было загружено при следующем запуске и откуда можно было возобновить работу.

9.2. Изолируйте, что случилось ночью, из журнала событий

В журнале «Система» Windows Event ID 1074 записывает процесс, который инициировал завершение работы, пользователя и причину. При неожиданном завершении при следующем запуске записываются 41 (Kernel-Power) или 6008.15

# Проверить недавнюю историю событий, связанных с завершением работы
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
    Select-Object TimeCreated, Id, ProviderName, Message |
    Format-List

Если 1074 показывает перезапуск Windows Update, а данные всё равно повреждены, сначала разбирайте обработку уведомления о выходе и путь сохранения. С другой стороны, не заключайте об отключении питания только из 41 или 6008. Они указывают на неожиданное завершение, и синий экран или принудительный сброс тоже кандидаты.15

Выбор того, что расследовать, по журналу событий завершения работыПодтвердить инициирующий процесс и причину из 1074 и считать 41 и 6008 подсказками к неожиданному завершению, чтобы сверить с окружающей информацией вроде кода bug check и дамповЖурнал событий системы1074: инициирующий процесс и причинаРазобрать обработку уведомления и путь сохранения41 и 6008: неожиданное завершениеПроверить BugcheckCode и дампыИзолировать сбой, отключение питания и прочее

Рис. 19: 41 и 6008 — не доказательство самого отключения питания; они вход в дальнейшее расследование.

Ненулевой BugcheckCode в событии 41 — подсказка к сбою. Если он 0 и дампа памяти нет, подозревают отключение питания, но решайте, сверяя с окружающей информацией. Если оказывается отключение питания, сосредоточьтесь на проекте сохранения и ИБП раздела 8; если это был запланированный выход — на уведомлениях и завершающих действиях разделов 3–6.

10. Итог — проектируйте до следующего запуска, а не только обработку выхода

Обработка завершения работы — не про обработчик события, который один раз выполняется на выходе. Важно сделать штатное сохранение → короткие завершающие действия → проверку и восстановление при следующем запуске одним непрерывным целым.

Где пересмотреть Точка проектирования
Штатная обработка Сохранять часто и уменьшать дельту, оставшуюся на выходе
Уведомления GUI о выходе На запрос сразу отвечать TRUE как правило. Отделить идемпотентное сохранение от завершающих действий после подтверждения
Консольные приложения и службы Использовать уведомление, которое соответствует модели приложения. Не опираться только на ProcessExit .NET или на растягивание льготного срока
Непрерываемая работа Совмещать регистрацию причины и отказ только пока нужно. Готовиться и к принудительному завершению
Сохранение и загрузка Помимо временного файла, сброса и подмены дать резервную копию и проверку при запуске
После перезагрузки Проверить регистрацию перезапуска, данные восстановления и путь входа или запуска службы

При завершении работы с включённым быстрым запуском ядро и драйверы могут вернуться из гибернации. При изоляции сбоев явно указывайте «перезагрузить» и делайте приложение работоспособным и при полном завершении, и при гибридном.5

Финальная проверка от обработки выхода до восстановленияДержать сохранённое состояние во время обычной обработки, делать маленькие завершающие действия при уведомлении о выходе и проверять данные, которые остаются даже без уведомления, чтобы восстановиться при следующем запускеДаНетДержать сохранённое состояние штатноЕсть уведомление о выходе?Выйти с короткими завершающими действиямиПоследнее сохранённое состояние — всё, что естьПроверить и восстановить при следующем запускеВернуться к работе при нужных условиях

Рис. 20: Не отделяйте реализацию события выхода от штатного сохранения и восстановления при следующем запуске.

Наконец, пробуйте пути уведомления и требуемое время на тестовой машине и подтверждайте восстановление после внезапной остановки тоже. В расследовании после факта используйте 1074 / 41 / 6008 как подсказки, чтобы изолировать запланированный выход от неожиданного завершения.

В следующий раз, когда добавляете функцию, спрашивайте: «если во время этой обработки придёт уведомление о выходе или выдернут питание, что останется при следующем запуске?» Включить этот ответ в проект — то, что защищает от обнаружения порчи данных только на следующее утро.

Похожие статьи

Смежные области консультирования

KomuraSoft LLC занимается проектированием и реализацией мер против завершения работы и отключения питания для ПК оборудования и долгоживущих приложений, расследованием причин порчи данных и сбоев «утром уже не работало», которые начинаются с перезапуска Windows Update или выхода из системы, и ревью проекта обработки остановки служб Windows и автоматического восстановления. Можно начать со стадии «кажется, каждый раз при завершении работы что-то ломается, но не знаю, с чего начать».

Справочные ссылки

  1. Microsoft Learn, WM_QUERYENDSESSION message. О том, что WM_QUERYENDSESSION отправляется при конце сеанса и приложения возвращают TRUE, чтобы уважать намерение пользователя (DefWindowProc тоже по умолчанию даёт TRUE); об откладывании завершающих действий до WM_ENDSESSION; о том, что система через 5 секунд показывает UI со списком приложений, которые препятствуют завершению работы, чтобы пользователь мог принудительно завершить; о смысле битов ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL в lParam; о том, что завершение работы и перезагрузку нельзя различить; и о частом сохранении данных, чтобы уменьшить объём, сохраняемый на выходе. ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. Microsoft Learn, HandlerRoutine callback function. О событиях CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN, которые принимает обработчик, зарегистрированный через SetConsoleCtrlHandler; о том, что тайм-аут CTRL_CLOSE_EVENT по умолчанию около 5000 миллисекунд, а CTRL_SHUTDOWN_EVENT для процессов служб около 20000 миллисекунд; о том, что CTRL_LOGOFF/SHUTDOWN_EVENT на практике принимают только службы, потому что интерактивные приложения завершаются при выходе из системы; и о том, что обработчик выполняется на отдельном потоке. ↩ ↩2 ↩3 ↩4

  3. Microsoft Learn, Service Control Handler Function. О том, что службы, объявившие SERVICE_ACCEPT_PRESHUTDOWN, получают SERVICE_CONTROL_PRESHUTDOWN первыми, затем службы SERVICE_ACCEPT_SHUTDOWN получают SERVICE_CONTROL_SHUTDOWN; о том, что льготный срок по умолчанию при завершении работы около 20 секунд, а WaitToKillServiceTimeout — верхний предел при перезапуске ОС; о том, чтобы не растягивать это значение; о том, что обработчик управления возвращается в течение 30 секунд, сообщает STOP_PENDING с wait hint и отдаёт долгую работу другому потоку; о том, чтобы заканчивать завершающие действия как можно быстрее с учётом работы ИБП; и о том, что SCM при завершении работы по умолчанию не учитывает зависимости. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  4. Microsoft Learn, .NET runtime no longer provides default termination signal handlers. О том, что начиная с .NET 10 среда больше не даёт обработчики по умолчанию для Windows CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT (эквиваленты SIGTERM/SIGHUP на Unix); о том, что обработка ОС по умолчанию сразу завершает приложение, поэтому AppDomain.ProcessExit и AssemblyLoadContext.Unloading больше не срабатывают; и о том, что обработку сигналов, соответствующую модели приложения, регистрируют библиотеки верхнего уровня или код приложения. ↩ ↩2 ↩3

  5. Microsoft Learn, Fast startup causes hibernation or shutdown to fail in Windows 10 or Windows 8.1. О том, что при быстром запуске сеанс ядра не закрывается, а уходит в гибернацию, причём состояние ядра и драйверов устройств сохраняется в hiberfil.sys; о том, что «Перезагрузка» всегда делает полную загрузку, потому что требуется полностью новое состояние Windows; о том, что быстрый запуск включён по умолчанию и отключать его не рекомендуется; и о том, что Shutdown.exe по умолчанию делает полное завершение, а параметр /hybrid даёт гибридное поведение. ↩ ↩2 ↩3 ↩4 ↩5

  6. Microsoft Learn, Shutdown Changes for Windows Vista. О том, что ответы на WM_QUERYENDSESSION/WM_ENDSESSION можно отложить на 5 секунд каждый, после чего пользователь выбирает продолжить или отменить; о том, что консольные приложения и приложения без видимого окна не могут отменить завершение работы и автоматически завершаются через 5 секунд без ответа или при ответе FALSE; о регистрации причины через ShutdownBlockReasonCreate, когда нужна блокировка; и о том, что приложения не должны зависеть от возможности блокировать завершение работы. ↩ ↩2 ↩3

  7. Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). О вызове в начале непрерываемой работы для регистрации строки причины и вызове ShutdownBlockReasonDestroy по окончании; о том, что вызывать можно только из потока, который создал окно; и о том, чтобы держать строку короткой и ясной, потому что пользователь читает причину лишь несколько секунд. ↩ ↩2 ↩3

  8. Microsoft Learn, SetConsoleCtrlHandler function. О том, что процесс, загрузивший gdi32.dll или user32.dll, считается приложением Windows, чьи обработчики CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT не вызываются; об обходе через создание скрытого окна и обработку WM_QUERYENDSESSION/WM_ENDSESSION; и о том, что консольные функции во время обработки сигнала могут работать ненормально. ↩

  9. Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). О том, что SCM после уведомления PRESHUTDOWN ждёт, пока служба остановится или истечёт тайм-аут; о том, что тайм-аут по умолчанию 10 секунд начиная с Windows 10 Creators Update (сборка 15063) и 3 минуты до того; о настройке через ChangeServiceConfig2; и о том, что обновления состояния продолжаются во время SERVICE_STOP_PENDING. ↩ ↩2

  10. Microsoft Learn, RegisterApplicationRestart function (winbase.h). О регистрации на перезапуск в сценариях сбоя, «Не отвечает», обновления и перезапуска компьютера из‑за обновления; об указании аргументов командной строки для перезапуска; о регистрации до возникновения проблемы, причём обработка WM_QUERYENDSESSION — последний шанс в сценарии обновления; о том, что процессы, работавшие меньше 60 секунд, не перезапускаются; о том, что перезапуск после сбоя или зависания требует согласия пользователя; и о том, что переход через перезапуск ОС требует завершения с EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS. ↩ ↩2

  11. Microsoft Learn, Winlogon automatic restart sign-on (ARSO). О том, что Windows Update сохраняет учётные данные последнего интерактивного пользователя и настраивает Autologon, когда начинает автоматический перезапуск; о том, что после перезапуска пользователь входит автоматически и сеанс блокируется; о том, что сохранённые учётные данные удаляются после успешного входа; и о настройке через групповую политику (DisableAutomaticRestartSignOn и другие). ↩ ↩2

  12. Microsoft Learn, ReplaceFileW function (winbase.h). О том, что ReplaceFile собирает в одну функцию несколько шагов, соответствующих сохранению в новый файл, временному переименованию исходного, переименованию нового и удалению исходного; о том, что он сохраняет атрибуты исходного файла вроде времени создания, DACL, шифрования, сжатия и именованных потоков; и о том, что резервная копия, заменяемый файл и файл замены должны быть на одном томе. ↩ ↩2

  13. Microsoft Learn, File Caching. О том, что записи по умолчанию идут в системный кэш и применяются к диску отложенной записью; о том, что FILE_FLAG_WRITE_THROUGH сразу пишет данные на диск; о явном сбросе через FlushFileBuffers; и о том, что метаданные файловой системы всегда кэшируются, поэтому фиксация метаданных требует сброса или write-through. ↩ ↩2

  14. Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. О том, что это событие доставляется через WM_POWERBROADCAST при переключении между батареей и питанием от сети или при падении остатка заряда; и о вызове GetSystemPowerStatus при приёме, чтобы проверить ACLineStatus, BatteryFlag, BatteryLifePercent и другие члены SYSTEM_POWER_STATUS. ↩ ↩2

  15. Microsoft Learn, Troubleshoot unexpected reboots using system event logs. О том, что нормальный перезапуск записывает Event ID 1074 (какой процесс инициировал завершение работы, от чьего имени и по какой причине); о том, что неожиданный перезапуск записывает Event ID 41 (Kernel-Power) и 6008 (предыдущее завершение работы было неожиданным); и о том, что эти идентификаторы используют, чтобы изолировать вид перезапуска. ↩ ↩2

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Сбой, который не ушёл после «Завершение работы», исчез после «Перезагрузка». Почему?
На клиентских ОС начиная с Windows 8, если включён быстрый запуск (по умолчанию на большинстве ПК, которые поддерживают гибернацию), «Завершение работы» идёт по схеме гибридного завершения (hybrid shutdown). Пользователь выходит из системы, но состояние ядра и драйверов сохраняется в файл гибернации и при следующей загрузке восстанавливается как есть. То есть ядро ОС не сбрасывается. «Перезагрузка», напротив, всегда делает полную загрузку, поэтому сбои драйверов и служб сбрасываются. В инструкции по диагностике пишите «Перезагрузить», а не «выключить и снова включить». Если из командной строки нужно полное завершение работы, можно использовать shutdown /s.
Можно ли задержать завершение работы, пока приложение не закончит сохранение?
Попросить подождать ненадолго можно, надёжно остановить — нельзя. Если зарегистрировать строку причины через ShutdownBlockReasonCreate только на время операции, которую нельзя прервать, эта причина появится на экране «Это приложение препятствует завершению работы», и пользователь решит, продолжить или отменить. Пользователь всё равно может выбрать принудительное продолжение, а принудительное завершение или перезагрузка из‑за обновления могут не ждать вовсе. Поэтому правильный путь — не «блокировать», а частое автосохранение, чтобы под риском было меньше данных, плюс завершающие действия, рассчитанные на несколько секунд с момента уведомления.
Остановка службы Windows занимает много времени. Можно ли увеличить отведённое на завершение время?
В конфигурации по умолчанию, которая принимает SERVICE_CONTROL_SHUTDOWN, отводится примерно 20 секунд, и это зависит от значения реестра WaitToKillServiceTimeout. Переписывать это значение со стороны приложения, чтобы растянуть срок, не рекомендуется. Если нужно больше времени, можно объявить SERVICE_ACCEPT_PRESHUTDOWN и принимать SERVICE_CONTROL_PRESHUTDOWN: уведомление приходит раньше остальных, а тайм-аут задаётся через ChangeServiceConfig2 (по умолчанию 10 секунд начиная с Windows 10 Creators Update и 3 минуты до того). PRESHUTDOWN, однако, на это время задерживает завершение работы всей системы, поэтому оставляйте его для случаев, когда он действительно нужен, а саму остановку проектируйте так, чтобы она укладывалась в несколько секунд.
Можно ли делать завершающие действия при выходе в AppDomain.ProcessExit .NET?
На это лучше не опираться. Раньше среда выполнения регистрировала обработчик сигналов по умолчанию, и ProcessExit срабатывал на CTRL_CLOSE_EVENT и CTRL_SHUTDOWN_EVENT, но начиная с .NET 10 среда больше не даёт обработчики сигналов завершения по умолчанию, и в этих ситуациях ProcessExit не возникает. Реализуйте завершающие действия на пути уведомления, который соответствует модели приложения: GUI-приложения — FormClosing или SessionEnding (это уведомления этапа запроса, поэтому ограничивайтесь идемпотентным сохранением; действия, которые допустимы только после подтверждения сеанса, делайте в хуке WM_ENDSESSION); Generic Host / Worker Service — IHostApplicationLifetime и StopAsync; консольные приложения — SetConsoleCtrlHandler или PosixSignalRegistration.
Как не дать файлам повредиться при внезапном отключении питания?
Отключение питания не несёт никакого уведомления, поэтому остаётся писать так, чтобы файл не ломался, когда бы питание ни пропало. Базовая схема — не перезаписывать исходный файл на месте: полностью записать во временный файл на том же томе, сбросить на диск и подменить через ReplaceFile (в .NET — File.Replace). В обычной работе это оставляет возможность прочитать либо полный старый файл, либо полный новый, но атомарность ReplaceFile через отключение питания спецификацией не гарантируется, поэтому держите резервную копию (третий аргумент) и при запуске проверяйте основной файл, а если он повреждён — откатывайтесь на резерв. Кроме того, успех WriteFile не значит, что данные дошли до диска, поэтому на важных контрольных точках подтверждайте запись через FlushFileBuffers или FILE_FLAG_WRITE_THROUGH. На ПК оборудования к этому обычно добавляют ИБП: обнаруживают переход на батарею и переводят систему в безопасное завершение работы.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог