Завершение работы 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.
flowchart TB
accTitle: Проект сохранения, общий для уведомлений о выходе и отключения питания
accDescr: Штатное сохранение держит несохранённую дельту маленькой; при уведомлении следуют короткие завершающие действия, а без него сохранённые данные проверяют и восстанавливают до возобновления работы
daily["Сохранять на каждой вехе"] --> event{"Есть уведомление при выходе?"}
event -->|"Да"| close["Сохранить только оставшуюся дельту и выйти"]
event -->|"Нет"| lost["Завершающие действия на месте невозможны"]
close --> boot["Проверить и восстановить при следующем запуске"]
lost --> boot
boot --> resume["Возобновить работу"]
Рис. 1: Вместо того чтобы напрягаться только при уведомлении, сделайте штатное сохранение до следующего запуска одним непрерывным процессом.
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 14, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Сначала различите — выход из системы, завершение работы, перезагрузка и отключение питания
2.1. Смотрите на сеанс пользователя и на ядро отдельно
Если разделить «что заканчивается» на две части, обработку проще организовать. Сеанс пользователя — область, в которой работают приложения этого пользователя. Ядро и драйверы, напротив, принадлежат стороне ОС и продолжают работать после выхода пользователя.
| Операция | Сеанс пользователя | Ядро и драйверы | Уведомления |
|---|---|---|---|
| Выход из системы | Заканчивается | Продолжают работать | GUI получает запрос на выход и подтверждённое уведомление. Службы при выходе из системы не останавливаются |
| Завершение работы при включённом быстром запуске | Заканчивается | Сохраняет состояние гибернации в hiberfil.sys |
Уведомления GUI о конце сеанса и уведомление службам о завершении работы |
| Перезагрузка | Заканчивается | Заканчивается полностью и в следующий раз делает полную загрузку | То же, что выше |
| Внезапное отключение питания | Теряется мгновенно | Теряется мгновенно | Уведомления нет |
В GUI WM_QUERYENDSESSION бит ENDSESSION_LOGOFF в lParam указывает на выход из системы. Если lParam равен 0, это завершение работы или перезагрузка, и их нельзя различить. Считайте lParam битовой маской.1
Для несохранённых данных приложения выход из системы и завершение работы требуют одной и той же подготовки. Не разделяйте их на «при выходе из системы сохранять не нужно»; направляйте оба в общую процедуру сохранения. Это, однако, не делает условия уведомления консольных приложений и служб одинаковыми; проверяйте пути в разделах 5 и 6 для каждого.
flowchart TB
accTitle: Думайте о выходе из системы и выходе системы отдельно
accDescr: Выход из системы заканчивает приложения пользователя, а службы продолжают работать; завершение работы или перезагрузка системы останавливает и службы; отключение питания не уведомляет никого
signout["Выход из системы"] --> user["Приложения пользователя выходят"]
signout -.-> alive["Службы продолжают работать"]
system["Завершение работы или перезагрузка"] --> user
system --> svc["Службы тоже останавливаются"]
power["Отключение питания"] --> none["Уведомления нет никому"]
Рис. 2: Выход из системы позволяет прогнать обработку выхода GUI, но это не считается проверкой остановки службы.
2.2. Почему «завершение работы не чинит, а перезагрузка чинит»
На клиентских ОС начиная с Windows 8 быстрый запуск по умолчанию включён на многих ПК, которые поддерживают гибернацию. В этой конфигурации завершение работы выводит пользователя, но состояние ядра и драйверов устройств сохраняется в файл гибернации и восстанавливается при следующей загрузке. Отключение питания не обязательно значит, что всё состояние ОС было сброшено.5
Это поведение условно. Там, где гибернация отключена (powercfg /hibernate off), где быстрый запуск выключен политикой или в параметрах электропитания, и на Windows Server получается традиционное полное завершение. Проверяйте параметры электропитания и через powercfg /a смотрите, доступен ли быстрый запуск.
«Перезагрузка», напротив, всегда выполняет полный цикл загрузки. В процедуре изоляции сбоев драйверов пишите явно «перезагрузить», а не «выключить и включить».5
flowchart TB
accTitle: Быстрый запуск против полной загрузки
accDescr: Завершение работы при включённом быстром запуске сохраняет и восстанавливает состояние ядра и драйверов, а полное завершение или перезагрузка инициализируют всё полной загрузкой
s["Завершение работы"] --> q{"Использовать быстрый запуск?"}
q -->|"Да"| save["Увести ядро и драйверы в гибернацию"]
save --> restore["Восстановить состояние при следующем запуске"]
q -->|"Нет"| full["Полное завершение работы"]
full --> boot["Инициализировать полной загрузкой в следующий раз"]
r["Перезагрузка"] --> boot
Рис. 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
flowchart TB
accTitle: Запрос GUI и результат выхода
accDescr: Даже после возврата TRUE на WM_QUERYENDSESSION другое приложение может отменить выход, поэтому завершающие действия после подтверждения выполняются только когда wParam у WM_ENDSESSION равен TRUE
query["WM_QUERYENDSESSION"] --> reply["Сразу вернуть TRUE как правило"]
reply --> result["WM_ENDSESSION"]
result --> yes{"wParam равен TRUE?"}
yes -->|"Да"| cleanup["Завершающие действия после подтверждения"]
yes -->|"Нет"| running["Выход отменён; продолжать работу"]
Рис. 4: Не путайте ответ, который разрешает выход, с уведомлением, что выход подтверждён.
Есть случаи, когда можно вернуть FALSE и отказать в выходе, но правило — уважать намерение пользователя выйти. Приложение, которое отказывает, показывают как препятствующее завершению работы. У консольных приложений и приложений без видимого окна тоже есть ограничения: в обычной конфигурации приложение, которое не отвечает в течение 5 секунд, может быть завершено автоматически. Считайте блокировку исключительной обработкой раздела 4 и не используйте её для обычного сохранения.6
3.2. Около 5 секунд — это не гарантия, что успеете сохранить
Если задержать ответ примерно на 5 секунд на этапе WM_QUERYENDSESSION или WM_ENDSESSION, система показывает экран со списком приложений, которые препятствуют завершению работы, и пользователь может выбрать принудительное завершение. После принудительного завершения возможности дописать сохранение уже нет.6
Мера — сохранять штатно и уменьшать дельту на выходе. Несохранённое рабочее состояние кладите во временное место и восстанавливайте при следующем запуске. Не проектируйте приложение так, чтобы при завершении работы показывать диалог подтверждения и ждать. Обычное подтверждение выхода и запрос на выход от ОС — разные вещи.1
flowchart TB
accTitle: Держите работу на выходе маленькой
accDescr: Проект, который сохраняет штатно, оставляет на выходе маленькую дельту, а проект, который копит в памяти до выхода, не укладывается в короткий льготный срок и рискует потерей через принудительное завершение
good["Сохранять штатно"] --> small["Дельта, оставшаяся на выходе, маленькая"]
small --> fast["Выйти с короткими завершающими действиями"]
bad["Копить в памяти до выхода"] --> large["Сохранить всё на выходе"]
large --> risk["Слишком мало льготного срока; риск принудительного завершения"]
Рис. 5: Подготовка к выходу за несколько секунд идёт до уведомления о выходе.
3.3. В WinForms и WPF отделите сохранение от завершающих действий, которые нельзя отменить
В WinForms соответствие — FormClosing с CloseReason.WindowsShutDown; в WPF — Application.SessionEnding. WPF можно зарегистрировать и через атрибут SessionEnding в XAML, и обработать, переопределив OnSessionEnding.
Оба, однако, — события этапа запроса. Здесь допустимо самое большее «идемпотентное сохранение снимка»: безвредно, если выход отменят, и даёт тот же результат, сколько бы раз ни выполнилось. Работу, которая возможна только после подтверждения выхода, например отключение, делают, принимая WM_ENDSESSION с wParam=TRUE в WndProc WinForms или в хуке WPF.
flowchart TB
accTitle: Разделение обработки выхода в WinForms и WPF
accDescr: FormClosing и SessionEnding сохраняют снимок, который безопасен даже если выход отменят, а хук на WM_ENDSESSION с TRUE выполняет завершающие действия, которые нельзя отменить
notify["Этап запроса"] --> forms["FormClosing"]
notify --> wpf["SessionEnding"]
forms --> snapshot["Идемпотентное сохранение снимка"]
wpf --> snapshot
final["WM_ENDSESSION с TRUE"] --> hook["Принять в WndProc или хуке"]
hook --> cleanup["Работа после подтверждения, например отключение"]
Рис. 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. Когда работа заканчивается, снимите и причину, и флаг защиты.
flowchart TB
accTitle: Разделение ролей во временной блокировке выхода
accDescr: Только пока идёт непрерываемая работа, совмещайте регистрацию причины с отказом на запрос и снимайте оба по завершении; принудительное завершение пользователем всё равно нельзя предотвратить
start["Начинается непрерываемая работа"] --> reason["Зарегистрировать причину и поставить флаг защиты"]
reason --> work["Обрабатывать на рабочем потоке"]
work --> done["Закончить; снять причину и флаг"]
work -.-> request["Тем временем приходит запрос на выход"]
request --> refuse["Вернуть FALSE и показать причину"]
refuse --> choice{"Решение пользователя"}
choice -->|"Отменить"| keep["Продолжать работу"]
choice -->|"Принудительно"| terminate["Может быть завершено"]
Рис. 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
flowchart TB
accTitle: Условия приёма уведомлений о выходе консоли
accDescr: Интерактивная консоль может принять уведомление о закрытии, но не может рассчитывать на уведомления о выходе из системы или завершении работы, а даже службе нужен другой путь уведомления, как только она загружает GUI-DLL
console["Консольный процесс"] --> close["Закрытие идёт в обработчик управления"]
console --> q{"Ждать LOGOFF или SHUTDOWN?"}
q -->|"Интерактивный сеанс"| no["На это уведомление опираться нельзя"]
q -->|"Служба"| dll{"Загружены GUI-DLL?"}
dll -->|"Нет"| signal["Обработать соответствующий управляющий сигнал"]
dll -->|"Да"| window["Принимать через скрытое окно"]
Рис. 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.
flowchart TB
accTitle: Снятие зависимости от обработчика завершения .NET по умолчанию
accDescr: Старая среда связывала соответствующие сигналы с ProcessExit, но начиная с .NET 10 обработчик по умолчанию не предоставляется, поэтому используйте путь уведомления модели приложения
signal["Сигнал CLOSE или SHUTDOWN"] --> old["Обработка старой среды по умолчанию"]
old --> event["Поднимает ProcessExit и подобные"]
signal --> modern["Нет обработки по умолчанию начиная с .NET 10"]
modern --> own{"Приложение обрабатывает?"}
own -->|"Нет"| os["Завершено обработкой ОС по умолчанию"]
own -->|"Да"| handle["Завершающие действия, которые соответствуют модели"]
Рис. 9: Различайте обычный выход и завершение внешним сигналом и дайте обработчик, который нужна модели приложения.
Сосредоточьте завершающие действия там, где это соответствует модели приложения. Для GUI это события и подтверждённое уведомление раздела 3; для Generic Host — IHostApplicationLifetime и BackgroundService.StopAsync; для обычного консольного приложения — SetConsoleCtrlHandler или PosixSignalRegistration. Когда используете последнее, выбирайте сигналы, которые соответствуют целевым путям завершения, например SIGINT, SIGTERM и SIGHUP.4
В Generic Host явно задайте HostOptions.ShutdownTimeout. Однако одна настройка на стороне Host не растягивает внешний льготный срок GUI, консоли или SCM. Даже хотя у Ctrl+C нет явного тайм-аута, всё равно нужно готовиться к отключению питания и другим путям завершения. Во всех моделях политика одна: держите состояние сохранённым и держите работу после уведомления маленькой.
flowchart TB
accTitle: Остановка Host и внешний льготный срок выхода
accDescr: Остановка Generic Host сосредоточена в StopAsync, но задание тайм-аута HostOptions не растягивает льготный срок выхода ОС или SCM, поэтому проверяйте оба
outer["Запрос на выход от ОС или SCM"] --> host["Путь остановки Host"]
host --> stop["Завершающие действия в StopAsync"]
stop --> inner["Задать время остановки на стороне Host"]
outer -.-> limit["Внешний льготный срок существует отдельно"]
inner --> check["Измерить, укладывается ли быстро"]
limit --> check
Рис. 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 ждёт, пока служба остановится или истечёт настроенный тайм-аут |
flowchart TB
accTitle: Этапы уведомления службам о выходе
accDescr: SCM сначала уведомляет службы, которые принимают PRESHUTDOWN, ждёт их остановки или тайм-аута, затем переходит к обычному уведомлению SHUTDOWN
start["Начинается завершение работы системы"] --> pre["Уведомить службы, принимающие PRESHUTDOWN"]
pre --> wait["Ждать остановки или настроенного срока"]
wait --> shut["Уведомить службы, принимающие SHUTDOWN"]
shut --> proceed["После льготного срока перейти к выходу системы"]
Рис. 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
flowchart TB
accTitle: Разделение работы между обработчиком управления службы и рабочим
accDescr: Обработчик управления сообщает stop pending, сигнализирует рабочему и сразу возвращается; рабочий делает завершающие действия и сообщения о прогрессе и в конце сообщает stopped
handler["Обработчик управления"] --> pending["Сообщить STOP_PENDING"]
pending --> signal["Сигнализировать рабочему остановиться"]
signal --> back["Обработчик сразу возвращается"]
signal --> worker["Рабочий делает завершающие действия"]
worker --> report["Сообщать о прогрессе, если длится долго"]
report --> stopped["Сообщить 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 по окончании. Если сообщения о прогрессе прекратятся, обработку восстановления могут оборвать.
flowchart TB
accTitle: Сохранение и уведомление о прогрессе в обратном вызове восстановления
accDescr: Обратный вызов восстановления, который вызывает WER, продолжает вызывать ApplicationRecoveryInProgress в пределах интервала ping, пока сохраняет, и уведомляет ApplicationRecoveryFinished по окончании
reg["Заранее зарегистрировать обратный вызов восстановления"] --> crash["Сбой; WER вызывает его"]
crash --> save["Сохранить данные, с которыми работали"]
save --> progress["Сообщать о прогрессе в пределах интервала ping"]
progress --> completed{"Сохранение закончено?"}
completed -->|"Ещё нет"| save
completed -->|"Готово"| done["Уведомить, что восстановление закончено"]
progress -.-> timeout["Могут оборвать, если сообщения прекратятся"]
Рис. 13: Не только сохраняйте; уведомляйте WER о прогрессе и завершении.
Замена файлов, которые используются во время обновления, и перезапуск — работа Restart Manager. Это разобрано в Как заменить EXE или DLL, которые используются.
Механизм, который возвращает сеанс пользователя после перезапуска ОС, напротив, — ARSO (автоматический вход Winlogon). Когда Windows Update начинает автоматический перезапуск, он безопасно сохраняет учётные данные последнего интерактивного пользователя и настраивает Autologon, и после перезапуска входит этим пользователем и блокирует экран.11
flowchart TB
accTitle: Регистрация перезапуска, вход и восстановление данных
accDescr: На фоне предварительной регистрации перезапуска сбой требует согласия пользователя, а приложениям пользователя после перезапуска ОС нужны восстановление сеанса через ARSO или подобное и восстановление сохранённого состояния
reg["Зарегистрировать перезапуск до возникновения проблемы"] --> crash["Сбой или «Не отвечает»"]
crash --> consent["Получить согласие пользователя"]
consent --> app["Перезапустить приложение"]
reg --> update["Перезапуск ОС с нужными флагами"]
update --> session["Восстановить сеанс через ARSO или подобное"]
session --> app
app --> data["Прочитать сохранённую точку восстановления"]
session -.-> policy["Проверить политику и условия запуска"]
Рис. 14: Дайте не только регистрацию перезапуска приложения, но и путь, которым возвращаются сеанс и рабочее состояние.
shutdown /g — команда, которая запрашивает перезапуск плюс возобновление зарегистрированных приложений. ARSO может быть отключён организационной политикой вроде DisableAutomaticRestartSignOn, поэтому проверяйте его вместе с требованиями необслуживаемого восстановления. Фоновую обработку, которая нужна всегда, лучше запускать как службу Windows, чем делать зависимой от автоматического входа пользователя.11
8. Отключение питания без уведомления — проектируйте сохранение и загрузку парой
8.1. Временный файл, подмена, резервная копия и проверка при запуске как один комплект
Сработавший автомат, вышедший из строя блок питания или выдернутый шнур не несут ни WM_ENDSESSION, ни PRESHUTDOWN. Если перезаписывать исходный файл на месте, прерывание посередине может оставить файл со смешанным старым и новым содержимым.
База — полностью записать во временный файл на том же томе, сбросить и затем подменить. ReplaceFile собирает шаги, соответствующие сохранению в новый файл, откладыванию исходного, переименованию и удалению, и переносит атрибуты вроде времени создания, ACL и альтернативных потоков данных. Заменяемый файл, файл замены и резервная копия должны быть на одном томе. File.Replace в .NET вызывает этот API.12
flowchart TB
accTitle: Сохранение файла и восстановление при следующем запуске
accDescr: Записать во временный файл на том же томе, сбросить, подменить, сохранив резерв, затем при запуске проверить основной файл и при необходимости откатиться на резерв
tmp["Временный файл на том же томе"] --> write["Записать до конца и сбросить"]
write --> replace["Подменить; старое содержимое уходит в .bak"]
replace -.-> boot["Следующий запуск"]
boot --> valid{"Основной файл цел?"}
valid -->|"Да"| main["Читать основной файл"]
valid -->|"Нет"| backup["Откатиться на резерв"]
Рис. 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
flowchart TB
accTitle: Граница между успехом записи и сохранением
accDescr: Обычная запись доходит до накопителя из кэша ОС с задержкой; сброс или write-through толкают к фиксации, но ограничения кэша на стороне устройства остаются
write["WriteFile успешен"] --> cache["Может быть в кэше ОС"]
cache --> delayed["Отложенная запись"]
cache --> flush["Сброс на контрольной точке"]
delayed --> device["Применено к накопителю"]
flush --> device
device -.-> limit["Пределы кэша на стороне устройства"]
Рис. 16: Не считайте успех API, фиксацию кэша ОС и устойчивость к отключению питания одним и тем же.
У энергозависимого кэша на стороне устройства тоже есть пределы, и нельзя сказать «мы сбросили, значит это полностью переживает отключение питания на любом железе». Связь диспетчера кэша, отложенной записи и аппаратных кэшей подробно объяснена в Диспетчере кэша: когда WriteFile оказывается на диске?.
8.3. Используйте ИБП, чтобы превратить отключение питания в запланированное завершение работы
Роль ИБП — не устранить отключения, а превратить отключение питания без уведомления в запланированное завершение работы с уведомлением. Нужное условие — такое соотношение:
Время работы батареи ИБП > время обнаружения переключения + завершающие действия приложений и служб + завершение остановки ОС
Переключение между питанием от сети и батареей и низкий остаток заряда сообщаются через PBT_APMPOWERSTATUSCHANGE. GUI принимает это через WM_POWERBROADCAST; служба объявляет SERVICE_ACCEPT_POWEREVENT и затем принимает SERVICE_CONTROL_POWEREVENT в HandlerEx. WM_POWERBROADCAST в обработчик управления службы не доставляется.14
После приёма проверьте ACLineStatus и BatteryLifePercent через GetSystemPowerStatus и переходите к приостановке измерений, сохранению и запросу завершения работы.14
flowchart TB
accTitle: От обнаружения ИБП до завершения остановки
accDescr: Обнаружить переход ИБП на батарею через соответствующий путь уведомления, проверить состояние питания, перейти к сохранению и завершению работы и спроектировать всю последовательность так, чтобы она укладывалась во время работы батареи
outage["Отключение; ИБП переходит на батарею"] --> notice["Уведомление о питании по подходящему пути"]
notice --> check["Проверить состояние питания и остаток заряда"]
check --> save["Приостановить измерения и сохранить"]
save --> req["Запросить завершение работы у ОС"]
req --> done["Закончить завершающие действия и выход ОС"]
done -.-> time["Уложить всю последовательность во время работы"]
Рис. 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
flowchart TB
accTitle: Поэтапное расширение проверки завершения работы
accDescr: В тестовой среде пробовать уведомления GUI и каждую операцию выхода, подтвердить восстановление после внезапной остановки на ВМ, затем проверить устойчивость к отключению питания, включая накопитель, на железе, эквивалентном продакшену
prep["Подготовить тестовую машину и данные"] --> notify["Пробовать пути уведомления и операции выхода"]
notify --> time["Измерить время завершающих действий"]
time --> vm["Подтвердить восстановление после внезапной остановки ВМ"]
vm --> real["Проверить на реальном железе, включая накопитель"]
real --> check["Подтвердить данные и восстановление при следующем запуске"]
Рис. 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
flowchart TB
accTitle: Выбор того, что расследовать, по журналу событий завершения работы
accDescr: Подтвердить инициирующий процесс и причину из 1074 и считать 41 и 6008 подсказками к неожиданному завершению, чтобы сверить с окружающей информацией вроде кода bug check и дампов
log["Журнал событий системы"] --> normal["1074: инициирующий процесс и причина"]
normal --> cleanup["Разобрать обработку уведомления и путь сохранения"]
log --> unexpected["41 и 6008: неожиданное завершение"]
unexpected --> evidence["Проверить BugcheckCode и дампы"]
evidence --> classify["Изолировать сбой, отключение питания и прочее"]
Рис. 19: 41 и 6008 — не доказательство самого отключения питания; они вход в дальнейшее расследование.
Ненулевой BugcheckCode в событии 41 — подсказка к сбою. Если он 0 и дампа памяти нет, подозревают отключение питания, но решайте, сверяя с окружающей информацией. Если оказывается отключение питания, сосредоточьтесь на проекте сохранения и ИБП раздела 8; если это был запланированный выход — на уведомлениях и завершающих действиях разделов 3–6.
10. Итог — проектируйте до следующего запуска, а не только обработку выхода
Обработка завершения работы — не про обработчик события, который один раз выполняется на выходе. Важно сделать штатное сохранение → короткие завершающие действия → проверку и восстановление при следующем запуске одним непрерывным целым.
| Где пересмотреть | Точка проектирования |
|---|---|
| Штатная обработка | Сохранять часто и уменьшать дельту, оставшуюся на выходе |
| Уведомления GUI о выходе | На запрос сразу отвечать TRUE как правило. Отделить идемпотентное сохранение от завершающих действий после подтверждения |
| Консольные приложения и службы | Использовать уведомление, которое соответствует модели приложения. Не опираться только на ProcessExit .NET или на растягивание льготного срока |
| Непрерываемая работа | Совмещать регистрацию причины и отказ только пока нужно. Готовиться и к принудительному завершению |
| Сохранение и загрузка | Помимо временного файла, сброса и подмены дать резервную копию и проверку при запуске |
| После перезагрузки | Проверить регистрацию перезапуска, данные восстановления и путь входа или запуска службы |
При завершении работы с включённым быстрым запуском ядро и драйверы могут вернуться из гибернации. При изоляции сбоев явно указывайте «перезагрузить» и делайте приложение работоспособным и при полном завершении, и при гибридном.5
flowchart TB
accTitle: Финальная проверка от обработки выхода до восстановления
accDescr: Держать сохранённое состояние во время обычной обработки, делать маленькие завершающие действия при уведомлении о выходе и проверять данные, которые остаются даже без уведомления, чтобы восстановиться при следующем запуске
daily["Держать сохранённое состояние штатно"] --> endq{"Есть уведомление о выходе?"}
endq -->|"Да"| short["Выйти с короткими завершающими действиями"]
endq -->|"Нет"| prior["Последнее сохранённое состояние — всё, что есть"]
short --> nextboot["Проверить и восстановить при следующем запуске"]
prior --> nextboot
nextboot --> restart["Вернуться к работе при нужных условиях"]
Рис. 20: Не отделяйте реализацию события выхода от штатного сохранения и восстановления при следующем запуске.
Наконец, пробуйте пути уведомления и требуемое время на тестовой машине и подтверждайте восстановление после внезапной остановки тоже. В расследовании после факта используйте 1074 / 41 / 6008 как подсказки, чтобы изолировать запланированный выход от неожиданного завершения.
В следующий раз, когда добавляете функцию, спрашивайте: «если во время этой обработки придёт уведомление о выходе или выдернут питание, что останется при следующем запуске?» Включить этот ответ в проект — то, что защищает от обнаружения порчи данных только на следующее утро.
Похожие статьи
- Как заменить EXE или DLL, которые используются — Restart Manager и проблема «файл используется» при автоматическом обновлении
- Как создать и эксплуатировать службу Windows — от выбора между Планировщиком заданий и службой до превращения BackgroundService в службу
- Спящий режим, гибернация и Modern Standby: как не дать долгоживущему приложению остановиться ночью
- Глубины ввода-вывода Windows (часть 4) — диспетчер кэша: когда WriteFile оказывается на диске
- Как проектировать логи и дампы при сбое Windows-приложения
- Чек-лист безопасной работы с дочерними процессами в Windows-приложении
Смежные области консультирования
KomuraSoft LLC занимается проектированием и реализацией мер против завершения работы и отключения питания для ПК оборудования и долгоживущих приложений, расследованием причин порчи данных и сбоев «утром уже не работало», которые начинаются с перезапуска Windows Update или выхода из системы, и ревью проекта обработки остановки служб Windows и автоматического восстановления. Можно начать со стадии «кажется, каждый раз при завершении работы что-то ломается, но не знаю, с чего начать».
- Разработка Windows-приложений
- Расследование сбоев и анализ причин
- Техническая консультация и ревью проекта
- Связаться с нами
Справочные ссылки
-
Microsoft Learn, WM_QUERYENDSESSION message. О том, что WM_QUERYENDSESSION отправляется при конце сеанса и приложения возвращают TRUE, чтобы уважать намерение пользователя (DefWindowProc тоже по умолчанию даёт TRUE); об откладывании завершающих действий до WM_ENDSESSION; о том, что система через 5 секунд показывает UI со списком приложений, которые препятствуют завершению работы, чтобы пользователь мог принудительно завершить; о смысле битов ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL в lParam; о том, что завершение работы и перезагрузку нельзя различить; и о частом сохранении данных, чтобы уменьшить объём, сохраняемый на выходе. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
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
-
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
-
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
-
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
-
Microsoft Learn, Shutdown Changes for Windows Vista. О том, что ответы на WM_QUERYENDSESSION/WM_ENDSESSION можно отложить на 5 секунд каждый, после чего пользователь выбирает продолжить или отменить; о том, что консольные приложения и приложения без видимого окна не могут отменить завершение работы и автоматически завершаются через 5 секунд без ответа или при ответе FALSE; о регистрации причины через ShutdownBlockReasonCreate, когда нужна блокировка; и о том, что приложения не должны зависеть от возможности блокировать завершение работы. ↩ ↩2 ↩3
-
Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). О вызове в начале непрерываемой работы для регистрации строки причины и вызове ShutdownBlockReasonDestroy по окончании; о том, что вызывать можно только из потока, который создал окно; и о том, чтобы держать строку короткой и ясной, потому что пользователь читает причину лишь несколько секунд. ↩ ↩2 ↩3
-
Microsoft Learn, SetConsoleCtrlHandler function. О том, что процесс, загрузивший gdi32.dll или user32.dll, считается приложением Windows, чьи обработчики CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT не вызываются; об обходе через создание скрытого окна и обработку WM_QUERYENDSESSION/WM_ENDSESSION; и о том, что консольные функции во время обработки сигнала могут работать ненормально. ↩
-
Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). О том, что SCM после уведомления PRESHUTDOWN ждёт, пока служба остановится или истечёт тайм-аут; о том, что тайм-аут по умолчанию 10 секунд начиная с Windows 10 Creators Update (сборка 15063) и 3 минуты до того; о настройке через ChangeServiceConfig2; и о том, что обновления состояния продолжаются во время SERVICE_STOP_PENDING. ↩ ↩2
-
Microsoft Learn, RegisterApplicationRestart function (winbase.h). О регистрации на перезапуск в сценариях сбоя, «Не отвечает», обновления и перезапуска компьютера из‑за обновления; об указании аргументов командной строки для перезапуска; о регистрации до возникновения проблемы, причём обработка WM_QUERYENDSESSION — последний шанс в сценарии обновления; о том, что процессы, работавшие меньше 60 секунд, не перезапускаются; о том, что перезапуск после сбоя или зависания требует согласия пользователя; и о том, что переход через перезапуск ОС требует завершения с EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS. ↩ ↩2
-
Microsoft Learn, Winlogon automatic restart sign-on (ARSO). О том, что Windows Update сохраняет учётные данные последнего интерактивного пользователя и настраивает Autologon, когда начинает автоматический перезапуск; о том, что после перезапуска пользователь входит автоматически и сеанс блокируется; о том, что сохранённые учётные данные удаляются после успешного входа; и о настройке через групповую политику (DisableAutomaticRestartSignOn и другие). ↩ ↩2
-
Microsoft Learn, ReplaceFileW function (winbase.h). О том, что ReplaceFile собирает в одну функцию несколько шагов, соответствующих сохранению в новый файл, временному переименованию исходного, переименованию нового и удалению исходного; о том, что он сохраняет атрибуты исходного файла вроде времени создания, DACL, шифрования, сжатия и именованных потоков; и о том, что резервная копия, заменяемый файл и файл замены должны быть на одном томе. ↩ ↩2
-
Microsoft Learn, File Caching. О том, что записи по умолчанию идут в системный кэш и применяются к диску отложенной записью; о том, что FILE_FLAG_WRITE_THROUGH сразу пишет данные на диск; о явном сбросе через FlushFileBuffers; и о том, что метаданные файловой системы всегда кэшируются, поэтому фиксация метаданных требует сброса или write-through. ↩ ↩2
-
Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. О том, что это событие доставляется через WM_POWERBROADCAST при переключении между батареей и питанием от сети или при падении остатка заряда; и о вызове GetSystemPowerStatus при приёме, чтобы проверить ACLineStatus, BatteryFlag, BatteryLifePercent и другие члены SYSTEM_POWER_STATUS. ↩ ↩2
-
Microsoft Learn, Troubleshoot unexpected reboots using system event logs. О том, что нормальный перезапуск записывает Event ID 1074 (какой процесс инициировал завершение работы, от чьего имени и по какой причине); о том, что неожиданный перезапуск записывает Event ID 41 (Kernel-Power) и 6008 (предыдущее завершение работы было неожиданным); и о том, что эти идентификаторы используют, чтобы изолировать вид перезапуска. ↩ ↩2
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Что на самом деле делает быстрый запуск — почему «Завершение работы» в Windows не то же самое, что перезагрузка
Завершение работы Windows по умолчанию — гибридное: ядро и драйверы сохраняются в hiberfil.sys. Почему только перезагрузка их сбрасывает ...
Сеть работает, а Windows пишет «Нет доступа к интернету» — разбираем NCSI, DNS, прокси и VPN
Почему Windows сообщает «Нет доступа к интернету», хотя сеть работает: разбираем решение NCSI о связности. Отделяем DNS, прокси, VPN и ca...
Time Travel Debugging — записывать и перематывать ошибки, которые не воспроизводятся в долгоживущих приложениях
Ошибка раз в месяц оставляет в дампе только результат. Записывайте и перематывайте исполнение через WinDbg Time Travel Debugging (TTD): T...
Пул потоков Win32 — параллелизм через CreateThreadpoolWork без своих потоков
Не плодите ли вы CreateThread по всему нативному коду? Разбираем API пула потоков Win32, переработанный в Vista: четыре объекта work, tim...
Именованные каналы на практике — от проектирования до безопасности IPC в Windows
Практический разбор именованных каналов — стандартного IPC в Windows. По первоисточникам: выбор байтового режима и режима сообщений, серв...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Сбой, который не ушёл после «Завершение работы», исчез после «Перезагрузка». Почему?
- На клиентских ОС начиная с 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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.