Планировщик задач Windows часто воспринимают как будильник для скриптов, но его сильная сторона не в расписании, а в триггерах. Триггер это условие, при котором система сама поднимает задание: наступило время, в журнал упало событие с нужным EventID, пользователь вошёл, машина простаивает, служба остановилась. Администратор, который умеет связывать события журнала с действиями, получает реактивную автоматизацию без сторонних агентов: сервер сам замечает ошибку и сам о ней сообщает. Ниже разбирается модель триггеров, действия и условия, практический рецепт уведомления об ошибке через XPath-фильтр, регистрация задания из XML и вопросы безопасности, потому что Планировщик это ещё и излюбленное место закрепления вредоносного кода.

Модель триггеров и на что реально можно подписаться

У задания может быть несколько триггеров, и срабатывание любого из них запускает действие. Основные типы:

  • По времени: одноразово, ежедневно, еженедельно, ежемесячно, с повторением внутри окна (например, каждые 15 минут в течение дня).
  • По событию журнала: задание срабатывает, когда в указанный журнал записывается событие, попадающее под XML-фильтр. Это самый мощный тип для реактивных сценариев.
  • При запуске системы: отрабатывает после загрузки, до входа любого пользователя.
  • При входе пользователя: при интерактивном логоне конкретной учётной записи или любой.
  • При простое: система считается простаивающей по набору критериев (нет ввода, низкая загрузка процессора и диска).
  • При подключении и отключении пользовательской сессии, при блокировке и разблокировке рабочей станции.

Триггер по событию строится на стандартном механизме запросов журнала событий. В простейшем виде указываются журнал, источник и EventID, но точная настройка делается через XPath. Фильтр вида *[System[(EventID=1001)]] ограничивает срабатывание конкретным событием, а можно сузить выборку по уровню, провайдеру и даже по данным внутри события через EventData. Такой фильтр встраивается прямо в XML-описание задания, поэтому шаблон удобно версионировать и раскатывать на парк машин.

Практический совет по событийным триггерам: не стоит подписываться на слишком частые события. Если журнал получает тысячи записей в минуту и фильтр широкий, задание будет стартовать непрерывно, и Планировщик начнёт отбрасывать совпадающие экземпляры. Сузить выборку помогает комбинация уровня (Level=2 для ошибок), идентификатора и источника. Отдельная история это триггер при простое: критерии простоя фиксированы системой и в XML не настраиваются напрямую, поэтому этот тип годится для фоновой обслуживающей работы, но не для точечной реакции. А триггер при запуске системы стоит сочетать с небольшой отложенной задержкой в полторы-две минуты, чтобы сетевой стек и зависимые службы успели подняться до старта действия.

Действия и условия выполнения

Действие это то, что делает задание после срабатывания. Классических вариантов три: запуск программы (исполняемый файл или скрипт через интерпретатор), отправка электронной почты и вывод сообщения. Последние два исторические и помечены как устаревшие: сервер SMTP из Планировщика давно не развивается, поэтому почту сегодня отправляют из скрипта, например через Send-MailMessage или вызов API почтового сервиса. На практике почти каждое серьёзное задание сводится к одному действию: запуск powershell.exe или pwsh.exe с параметром -File и путём к скрипту. Скриптом же можно сделать всё остальное: старт и остановку служб, сбор данных, отправку уведомлений.

Условия сидят на соседней вкладке и решают, разрешено ли заданию стартовать в текущей обстановке:

  • Запускать только при простое и останавливать при выходе из простоя.
  • Запускать только при питании от сети, не будить ноутбук на батарее; отдельная опция будит компьютер для выполнения.
  • Запускать только при доступности указанного сетевого подключения.

Условия не заменяют триггер, а фильтруют его. Типичная ошибка новичка: серверное задание унаследовано с настройки по умолчанию "не запускать на батарее", и на машине с ИБП или на ноутбуке-тестовом стенде оно молча пропускается. Пропуск нигде не выглядит как ошибка: в истории появляется нейтральная запись, и по ней приходится догадываться, что виновато условие. Поэтому серверные задания принято создавать с явно снятыми галочками питания и простоя, а условие сети включать осознанно: если действие ходит на файловую шару, условие "сеть доступна" избавит скрипт от старта в момент, когда интерфейс ещё не поднялся. Скрипт при этом всё равно должен уметь ждать: первая попытка соединения с повтором через десяток секунд страхует от гонки при загрузке.

Рецепт уведомления об ошибке в журнале

Свяжем всё вместе на задаче, которая встречается постоянно: приложение пишет ошибку в журнал Application, а дежурный хочет узнать об этом сразу, а не на утреннем обходе.

  1. Найти в Event Viewer нужное событие и записать журнал, источник и EventID. На вкладке Details в режиме XML видны поля, по которым удобно уточнить фильтр.
  2. Собрать XPath-фильтр. Минимальный вариант: *[System[Provider[@Name='MyApp'] and (EventID=1010)]]. Если одно и то же событие бывает штатным и аварийным, уточнить по EventData, например по тексту внутри данных события.
  3. Написать скрипт notify-error.ps1: он читает последнее событие через Get-WinEvent с тем же фильтром, формирует краткое тело и отправляет уведомление (почта через Send-MailMessage, вебхук в корпоративный чат через Invoke-RestMethod). Скрипт логирует факт срабатывания в свой файл, чтобы отличать "не пришло письмо" от "не сработал триггер".
  4. Создать XML-описание задания: триггер типа EventTrigger с подпиской на журнал Application и нашим XPath, действие powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\notify-error.ps1", учётная запись SYSTEM, запуск независимо от входа пользователя.
  5. Зарегистрировать задание командой schtasks /create /tn "Notify-MyApp-Error" /xml notify-myapp-error.xml или через Register-ScheduledTask с импортом того же XML.

Тонкость четвёртого шага: триггер передаёт в действие не само событие, а факт срабатывания. Скрипт должен сам дочитать событие из журнала, поэтому запрашивает записи за последнюю минуту и берёт самую свежую. Если срабатываний несколько подряд, помогает задержка в пару секунд в начале скрипта и чтение окном с запасом.

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

Регистрация из XML и поведение при ошибках

Формат XML единый для schtasks и PowerShell, и это путь к воспроизводимости: файл хранится в репозитории, ревьюится как код и ставится одной командой. Проверка после регистрации: schtasks /query /tn "Notify-MyApp-Error" /v /fo list и Get-ScheduledTask с Get-ScheduledTaskInfo, где видно время последнего запуска и код последнего результата. Код 0x0 означает успех; 0x1 и 0x2 обычно говорят о проблеме внутри скрипта, а не в триггере.

Отдельно настраиваются параметры повтора: число попыток при сбое и интервал между ними. Для реактивных уведомлений разумны два-три повтора с минутным интервалом, чтобы кратковременная недоступность почтового шлюза не съедала оповещение. Плюс настройки "перезапускать через каждые N минут" внутри триггера и "останавливать, если выполняется дольше". Ещё одна пара параметров, которую недооценивают: "не запускать новый экземпляр, если задание уже выполняется" против параллельного запуска. Для уведомлений параллельность безопасна, для архивации категорически нет.

Выбор учётной записи определяет и права, и доступность. SYSTEM видит всё локально, но не имеет сетевой личности, поэтому Send-MailMessage с авторизацией под SYSTEM не пройдёт; выручает ретранслятор, принимающий почту с адреса сервера, или выделенная сервисная учётка. Вариант "выполнять вне зависимости от входа пользователя" (Run whether user is logged on or not) критичен для серверов: сеанс администратора на сервере это исключение, а не норма, и задание, привязанное к интерактивному входу, просто не стартует после перезагрузки. Пароль такой учётки хранится в системе, и смену пароля в домене нужно синхронизировать, иначе задания начнут массово падать ровно в ночь после ротации. В домене эту боль закрывают групповыми управляемыми сервисными учётными записями, где пароль меняется автоматически и задания переживают ротацию без вмешательства.

Отдельного слова заслуживает приоритет и рабочий каталог. Действие, запущенное Планировщиком, наследует рабочий каталог system32, если поле "рабочая папка" не задано явно, и скрипты с относительными путями ломаются именно на этом. В XML у действия стоит прописывать и путь к интерпретатору, и аргументы, и WorkingDirectory. Права на сам скрипт тоже должны быть выставлены заранее: если задание работает под SYSTEM, а скрипт лежит в профиле администратора с обрезанными ACL, запуск завершится ошибкой доступа, которая на первый взгляд выглядит загадочно. Правило простое: скрипты для системных заданий живут в выделенном каталоге вроде C:\Scripts с явными разрешениями, а не на чьём-то рабочем столе.

История заданий и типовые сценарии администратора

Диагностику сильно упрощает журнал Microsoft-Windows-TaskScheduler/Operational. Если он включён, каждая регистрация (EventID 106), запуск (EventID 200), завершение и ошибка фиксируются. Когда "задание не работает", разбор начинается оттуда: нет события 106, значит триггер не выстрелил; есть 200 без кода результата, значит действие стартовало и упало внутри. Включить журнал при необходимости: wevtutil set-log Microsoft-Windows-TaskScheduler/Operational /enabled:true.

Набор сценариев, которые закрываются шаблонно:

  • Архивация логов: раз в сутки сжать и уложить файлы старше семи дней, заодно контролируя свободное место.
  • Чистка временных папок: ночной запуск с условием простоя, чтобы не трогать файлы активных процессов.
  • Инвентаризация железа в полночь: сбор WMI-данных (память, диски, S.M.A.R.T., версии BIOS) и складывание результата в общую папку или базу.
  • Реакция на событие: остановка критичной службы порождает событие журнала, триггер ловит его, действие пытается поднять службу обратно и шлёт уведомление, если не вышло.

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

Безопасность, аудит и отличие от автозагрузки

Планировщик давно входит в список проверяемых мест при реагировании на инциденты: задание под SYSTEM стартует от самой привилегированной локальной учётки, переживает перезагрузку и не требует сеанса пользователя. Легитимное и вредоносное задание в списке выглядят одинаково, поэтому аудит ведут регулярно. Полный обзор дают schtasks /query /v /fo csv (удобно скармливать в таблицу и сравнивать с эталоном) и Get-ScheduledTask с разворотом Actions и Triggers. Смотрят на незнакомые имена, действия из профилей пользователей и временных каталогов, обфусцированные командные строки и триггеры при входе. Событие 106 в журнале Планировщика позволяет собрать историю появления заданий, а пересылка этого журнала на сборщик даёт сигнал почти в реальном времени.

Важно не путать Планировщик с автозагрузкой. Папка Startup и ключи Run стартуют только при входе конкретного пользователя, работают в его контексте и умирают вместе с сеансом. Задание Планировщика живёт отдельно: стартует при загрузке машины, может работать под SYSTEM без всякого входа, имеет условия, повторы, историю и журнал. Автозагрузка это удобство пользователя; Планировщик это инструмент администрирования, и автоматическую реакцию на события системы правильно строить именно на нём: событие попадает в журнал, XPath-фильтр выхватывает нужное, скрипт превращает факт в уведомление или действие, а EventID 200 в собственном журнале подтверждает, что цепочка отработала.