В 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.
Когда сталкиваюсь с условиями задачи, в первую очередь представляю, как бы хотел реализацию. Здесь прям напрашивается опция «Выполнять в режиме энергосбережения». Кому как не пользователю знать, что он хочет. То есть сама задача — триггеры, действия, условия, параметры, — остаются без изменений!!!
Но раз такой опции нет, ее надо эмулировать. А вот дальше, знаний администрирования и PS не хватает. Но подумайте в такую сторону. Имена нужных задач вносятся в определенный реестр — файл, ресурсы скрипта, системный реестр. Создается задача с триггером «Перешли в режим сохранения энергии», которая обслуживает этот список и каким-то обходным образом (каким?) форсирует их сработку. Возможно это будет создании копии задачи с правами запуска залогиненного пользователя. Не забыть откатить… Возможно как-то еще. Но работа именно со списком абсолютно стандартных задач.
В приложенном решении, как понял, модифицируется сама задача.
Так второй скрипт именно это и делает. Так-то наверное есть API, но скриптом событие ловится нормально.
Не совсем. Поймать не проблема. Но, ключевое «…которая обслуживает этот список». Нет связи, между перехватили событие и форсировали заданиЯ (любое из заданий, с любым штатным триггером). Либо я ее не увидел.
Еще раз повторю, как я вижу удобную работу. Создаем обычную задачу, ни на секунду не задумываясь в каком режиме энергосбережение. Узнаем что она не срабатывает или вспоминаем о таком поведении ОС. Помещаем имя задачи в список «Эти должны срабатывать». Всё!
Можно ли такое сделать программно? Например создавая копию задачи, устранив одно из условий возникновения проблемы — «Задание настроено выполняться для всех пользователей, нежели для конкретного пользователя»?
В скрипте действия при перехвате события — это запись события в текстовый файл и вывод в консоль. Но это же просто примеры.
Прописывайте какие угодно задачи в том же блоке и выполняйте их. Конечно, это могут быть те же самые запланированные задания. Вызываем их из скрипта.
Тут главное не забывать, что задания планировщика сами по себе умеют запускаться по событию в журнале. Поэтому смысл городить с ними огород имеет только в описанном особом случае, когда задания не отрабатывают в режиме энергосбережения.
С одной стороны, здесь исходная задача диктовала работу для всех пользователей. Фоновая задача не видна (не мешает), а без прав админа её и не остановить.
С другой стороны, даже если отбросить джобы Powershell и методы .NET, а просто периодически опрашивать журналы с помощью Get-winevent, не во все журналы есть доступ без прав администратора.
Емнип, как раз в Operational нет (в такой пишется запись о переходе в режим энергосбережения). Тут можно пристегнуть ещё один набор костылей https://www.outsidethebox.ms/20970/ Тут главное не запутаться в костылях:)
Не понимаю как. Статья сильно завязана на триггер «выход из сна», для конкретной задачи Дениса. И как-то плохо сочетаются рядом стоящие фразы «Прописывайте какие угодно задачи» и «главное не запутаться в костылях». :))
В целом, если говорить языком разработчиков: слишком сильная связность задания и метода форсирования. Они нужны абстрагированными. Задание отдельно. Самостоятельное, НИЧЕГО не знающее про его форсирование. Создаваемое как из GUI планировщика, так и программно. Форсирование отдельно. НИЧЕГО не знающее про задания, их триггеры и другие параметры. И простейший биндинг между ними.
Может при таком архитектурном подходе, если он конечно возможен, и конкретная задача Дениса будет выглядеть проще.
Это первый скрипт, есть ещё второй в разделе
Мониторинг журналов приложений и служб
И что в моей постановке задачи может этот мониторинг?
У нас есть задания успешно работающие при питании от розетки. Некоторые из них, перестали срабатывать при переключении на батарею. Именно так, во множественном числе! Допустим 5, от TaskExample1 до TaskExample5.
Допустим перехватили мы переход в сбережение и знаем, что с этого момента с задачами проблема. Что дальше? Как без их модификации и не изобретая новый способ форсирования под триггеры конкретного TaskExample, вернуть как было́?
P.S. на соседний пост про абстрагирование отвечать не буду. Темы плотно пересекаются. Поймать переход не проблема. Главный вопрос, что дальше?
Попробую перефразировать еще так.
Вы решаете задачу выполнить действие X в ситуации Y, если питание от батареи. Решение успешно.
Мне хочется видеть, обеспечить срабатывание задания TaskExample в ситуации Y, совершающего действие X. Когда ни X, ни Y, мы не знаем и знать не хотим. Они приватно инкапсурированы внутрь задачи. А известно нам только ее имя TaskExample.
Смотрите, для решения задачи сделана альтернатива запуску задания планировщика по событию в журнале, штатной функции. Но у подхода много применений.
Фоновое задание powershell — это нечто вроде простенькой службы. Через планировщик же запускаем его при старте системы. Потому что так проще всего запустить от системы. Но можно иначе это реализовать.
А дальше подписка на события в журнале это тоже лишь пример. В статье упоминается регистрация событий WMI (запущен процесс, например).
Я понимаю что это может всё не так очевидно, потому что много всего сразу. Надо просто сесть и попробовать сделать, и окажется что это достаточно легко. Один скрипт, пара команд.