В 2011 году я описал в блоге решение с запуском задания планировщика по событию в журнале. А спустя 10+ лет читатель Денис озадачил меня условиями, при которых планировщик задействовать не выйдет.
Задача немного этюдная, хотя и основана на реальных событиях. В любом случае приемы из статьи можно применять и для других задач.
[+] Сегодня в программе
Условия задачи на практическом примере
С помощью планировщика заданий Денис настроил на ноутбуках организации подключение к корпоративному VPN при запуске ОС и выходе из сна. Однако запланированное задание переставало работать, когда система переходила в режим экономии энергии.
Оказывается, есть вполне легитимные условия, при сочетании которых запланированные задания не выполняются. И они даже описаны в документации!
- Система перешла в режим экономии энергии (battery saver).
- Задание настроено выполняться для всех пользователей, нежели для конкретного пользователя.
Логика разработчиков Windows простая – раз мы экономим заряд батареи, надо максимально придушить фоновые системные задачи, включая автоматическое обслуживание. Задание для всех пользователей фактически попадает в разряд системных, поэтому оно тоже игнорируется.
Я, конечно, посоветовал сделать ревизию таких костыльных решений и по возможности уходить от них. Ноутбук – явно не многопользовательское устройство, такая задача может работать в контексте пользователя.
Однако из академического интереса я задумался над костылем собственного производства для обхода этого ограничения ОС :)
Идея – мониторинг журнала событий
Известно, что при выходе из сна в журнал Система (System) пишется событие с кодом 1 от источника Microsoft-Windows-Power-Troubleshooter.
Раз невозможно поручить планировщику отслеживание журнала событий, придется реализовать его самостоятельно. С PowerShell, конечно!
Однако тут важно учитывать тип журнала.
О различных типах журналов событий в Windows
В ОС Windows работает два типа журналов.

На картинке я выделил:
- Журналы Windows. Это старые журналы: Приложения, Безопасность, Установка, Система и что-то еще по мелочи.
- Журналы приложений и служб. Десятки новых журналов появились в Windows Vista. Они работают на основе ETW и событий .NET.
Поэтому реализация мониторинга журналов в PowerShell слегка отличается в зависимости от их типа. В частности, задействуются разные классы .NET.
Мониторинг журналов Windows
Решая задачу в лоб, можно запустить в цикле регулярный опрос новых событий с помощью командлета Get-WinEvent. Но интуитивно это выглядит не самым эффективным способом. Как насчет подписки на события? Выход из сна регистрируется в журнале Система. Для журналов Windows быстро нашлись два родственных способа:
- создание фонового задания PowerShell для мониторинга журнала с помощью класса EventLog
- регистрация события WMI (временная или постоянная) для отслеживания системных событий
Мне приглянулся первый вариант, потому что в примере нужно был лишь поменять название журнала, ИД и источник события. Я лишь слегка заточил его под выход из сна.
Скрипт
Командлет Register-ObjectEvent по сути создает фоновое задание (PowerShell job).
# Запускать от администратора
# Журнал (Get-EventLog -AsString)
$log = [System.Diagnostics.EventLog]'System'
$log.EnableRaisingEvents = $true
# Имя фонового задания
$jobname = 'ResumeFromSleep'
$action = {
# Путь к файлу с отловленными событиями
$logFile = "C:\temp\ResumeFromSleep.txt"
$entry = $Event.SourceEventArgs.Entry
# Искомое событие: ID 1 от 'Microsoft-Windows-Power-Troubleshooter'
if ($entry.EventId -eq 1 -and $entry.Source -eq 'Microsoft-Windows-Power-Troubleshooter') {
# Сообщение
$msg = "Resumed from sleep: event $($entry.EventId) from $($entry.Source) is: $($entry.Message)"
$msg | Out-File -Append -FilePath $logFile -Encoding Unicode
Write-Host $msg
}
}
# Отменяем регистрацию предыдущих фоновых заданий с таким же именем
Unregister-Event -SourceIdentifier $jobname -ErrorAction SilentlyContinue
# Регистрируем и запускаем фоновую задачу
$job = Register-ObjectEvent -InputObject $log -EventName EntryWritten -SourceIdentifier $jobname -Action $action
Receive-Job $job
Write-Host "Monitoring started for Event ID 1 (Power-Troubleshooter)."
# Необязательно: блокируем появление приглашения на ввод следующей команды
# Имеет смысл только для выполнения в консоли
while ($true) { Start-Sleep -Seconds 1 }
<# остановка мониторинга и полное удаление фонового задания
Get-Job -Name 'ResumeFromSleep' | Stop-Job -PassThru | Remove-Job
#>
При наступлении события (выходе из сна) его свойства выводятся на экран и выполняется запись в текстовый файл, что вы можете легко изменить под свои нужды.
Демо
Тестовое событие пишется в журнал утилитой eventcreate, но смысл тот же. Каждое событие одновременно выводится в консоль и текстовый файл, изменения в котором отслеживает командлет Get-Content.
Организация запуска скрипта
У работы фоновых заданий есть неочевидные тонкости.
Тестовый запуск
Для теста просто вызовите скрипт из консоли: .\ResumeFromSleep.ps1. Теперь можно отправить систему в сон, а после выхода проверить наличие записей в тестовом файле на диске.
Для отмены мониторинга фоновое задание нужно остановить. Заодно можно и удалить.
Get-Job -Name ResumeFromSleep | Stop-Job -PassThru | Remove-Job
Учтите, что фоновая работа зависит от наличия родительского процесса. Закрыв текущую сессию консоли, вы также прекратите отслеживание.
Регулярный запуск
В общем случае скрипт нужно запускать отдельным процессом. Для разовых запусков подходит такой вариант:
Start-Process -Verb RunAs -FilePath powershell -ArgumentList "-WindowStyle hidden -ex bypass -File C:\Path\ResumeFromSleep.ps1"
Для регулярной работы имеет смысл создать для запуска… запланированное задание на однократный запуск от имени SYSTEM или для всех пользователей! Да, по условиям задачи планировщик не выполняет такие задания автоматически. Однако на запуск по требованию это не распространяется. Поэтому можно воспользоваться разделом реестра Run для выполнения при старте системы!
Код ниже создает задание ResumeFromSleep для запуска нашего скрипта таким образом, чтобы оно выполнялось при работе от батареи и не завершалось автоматически. Этот модуль PowerShell я разбирал в статье про выполнение заданий на закате и восходе солнца.
#Переменные
#путь к скрипту и УЗ системы
$path = "C:\Program Files\Scripts"
$system = "NT AUTHORITY\SYSTEM"
#Создание задания
$taskname = "ResumeFromSleep"
#Общие: выполнять с наивысшими правами от имени системы вне зависимости от входа
$principal = New-ScheduledTaskPrincipal -UserId $system -LogonType ServiceAccount
#Триггер
$trigger = New-ScheduledTaskTrigger -Once -At 00:01
#Параметры: #запускать при работе от батареи; немедленно если пропущено; не останавливать
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -ExecutionTimeLimit 0
#Команда...
$execute = "powershell"
#... и ее параметры командной строки
$argument = "-ExecutionPolicy Bypass -WindowStyle Hidden -file $path\ResumeFromSleep.ps1"
#Действие: "команда + параметры командной строки"
$action = New-ScheduledTaskAction -Execute $execute -Argument $argument
#Создать задание в планировщике
Register-ScheduledTask -TaskName $taskname -Action $action -Trigger $trigger -Settings $settings -Principal $principal
<#
$trigger = @(
#Запускать ежедневно сразу после полуночи (не пробуждая ПК)
$(New-ScheduledTaskTrigger -Daily -At 00:01),
#Запускать при входе пользователя в систему
$(New-ScheduledTaskTrigger -AtLogon)
)
#>
Запуск задания проще всего добавить в HKLM старой доброй reg add:
reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run /v ResumeFromSleep /t REG_SZ /d "powershell -ex bypass -noprofile -command Start-ScheduledTask -Taskname ResumeFromSleep"
Теперь запланированное задание будет форсироваться при старте ОС и запускать «невидимую» фоновую задачу PowerShell в сеансе 0 для мониторинга событий в журнале.
Чтобы остановить отслеживание, просто завершите запланированное задание:
Stop-ScheduledTask -Taskname ResumeFromSleep

Мониторинг журналов приложений и служб
Допустим, мы хотим выполнять задачу при переходе системы в режим энергосбережения. Для начала надо выяснить, какое событие при этом пишется в журналы.
Поиск журнала и события
Это несложно организовать с помощью Get-WinEvent. Включив режим экономии батареи, вы помимо прочих найдете такое событие:
- журнал:
Microsoft-Windows-PushNotification-Platform/Operational - ИД:
1025 - сообщение:
A Power event was fired: BatterySaverStateChange [PowerEventType] true [Enabled]. - пользователь:
система, S-1-5-18
В данном случае следует конкретизировать пользователя. Дело в том, что одновременно регистрируется такое же событие от имени интерактивного пользователя. Если опираться только на ИД события, скрипт отработает дважды.

Итак, у нас есть критерии для мониторинга.
Скрипт
Для отслеживания журналов приложений и служб предусмотрен класс EventLogWatcher. Спасибо за подсказки, Вадимс Поданс.
# Запускать от администратора
# Журнал
$log = "Microsoft-Windows-PushNotification-Platform/Operational"
$watcher = New-Object System.Diagnostics.Eventing.Reader.EventLogWatcher $log
$watcher.Enabled = $true
# Имя фонового задания
$jobname = 'BatterySaver'
# действия при появлении события
$action = {
# Путь к файлу с отловленными событиями
$logFile = "C:\temp\BatterySaver.txt"
$entry = $Event.SourceEventArgs.Entry
# Искомое событие: ID 1025
if ($eventArgs.EventRecord.Id -eq 1025 -and
$eventArgs.EventRecord.UserID -eq 'S-1-5-18' -and
$eventArgs.EventRecord.FormatDescription() -eq 'A Power event was fired: BatterySaverStateChange [PowerEventType] true [Enabled].'
) {
# Сообщение
$msg = "Triggered: event $($eventArgs.EventRecord.Id) from $($eventArgs.EventRecord.UserID) with message: $($eventArgs.EventRecord.FormatDescription())"
$msg | Out-File -Append -FilePath $logFile -Encoding Unicode
# Необязательно: форсируем вывод в консоль (при использовании Receive-Job)
Write-Host $msg
}
}
# Отменяем регистрацию предыдущих фоновых заданий с таким же именем
Unregister-Event -SourceIdentifier $jobname -ErrorAction SilentlyContinue
$job = Register-ObjectEvent -InputObject $watcher -EventName 'EventRecordWritten' -SourceIdentifier $jobname -Action $action
Receive-Job $job
Write-Host "Monitoring started for Event ID 1025"
# Необязательно: блокируем появление приглашения на ввод следующей команды
# Имеет смысл только для выполнения в консоли
while ($true) { Start-Sleep -Seconds 1 }
<# остановка мониторинга и полное удаление фонового задания
Get-Job -Name 'BatterySaver' | Stop-Job -PassThru | Remove-Job
$watcher.Dispose()
#>
Организация запуска скрипта такая же, как и в случае с мониторингом журнала события Windows.
Протестировать действие при конкретном событии на компьютере без батареи не получится, но вы легко можете скорректировать скрипт под любой журнал и событие.
Заключение
Если ограничиться задачей читателя, то костыли слишком сложные и хрупкие, особенно для внедрения в производственной среде. Да и кейс очень специфический. Однако приемы мониторинга полное имеют право на жизнь и практическое применение.
Например, может потребоваться запуск задачи при регистрации нескольких событий в журналах, нежели одного. Либо нужно отловить событие журнала в сочетании с другими условиями. С другой стороны, мониторинг вовсе не ограничен журналом событий. Так, Василий Гусев доставлял мне из своей практики вполне боевой пример отслеживания запуска процессов путем регистрации события WMI.
Добавить толковый комментарий
Для отправки комментария вам необходимо авторизоваться.