На терминальном сервере «пользователь вышел, а процесс висит» — обычное дело: сессия закрыта, а фоновая задача продолжает держать файл. Windows-LogOff 1.0.0 делает принудительный выход пользователя двухстадийно: сначала штатный logoff, затем добивание осиротевших процессов. Ниже — как устроен скрипт, что показал прогон на живой машине и какая ловушка ждёт там, где переменная окружения с именем компьютера пустая.

windows logoff cover

TL;DR: две стадии и один важный флаг

  1. Стадия 1. Перечисление сессий через quser и logoff.exe по идентификатору — с пятью секундами на сохранение данных.
  2. Стадия 2. Через паузу — скан Get-Process -IncludeUserName и Stop-Process -Force для остатков.
  3. Безопасность. #Requires -RunAsAdministrator, поддержка -WhatIf/-Confirm, пропуск своей сессии.
  4. Результат. Объект с Summary и Details — не текст в консоль, а данные для конвейера.
  5. Грабля. Если $env:COMPUTERNAME пуст, значение по умолчанию не проходит проверку — нужен явный -UserContext.

Проблема, которую это решает

logoff.exe завершает сессию, но не гарантирует, что пользователь исчез из системы. Часть процессов переживает выход: отвязанные фоновые задачи, службы, работающие от имени пользователя, процессы в нулевой сессии. Они держат файлы, порты и лицензии — и следующий вход того же пользователя получает «профиль занят».

Второй сценарий: сессия зависла. Окна не отвечают, WM_QUERYENDSESSION уходит в никуда, и штатный выход не срабатывает вовсе.

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

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

Что такое Windows-LogOff

flowchart TB
  Q["quser: список сессий"] --> S1["стадия 1: logoff.exe по идентификатору"]
  S1 --> WAIT["пауза 3 секунды"]
  WAIT --> SCAN["Get-Process -IncludeUserName"]
  SCAN --> S2["стадия 2: Stop-Process -Force"]
  S2 --> OUT["объект результата: Summary и Details"]
Свойство Значение
Версия в разборе 1.0.0
Тип один PowerShell-скрипт logoff.ps1
Размер 23 399 байт, 534 строки
Функций 5
Требования PowerShell 7.0+, права администратора
Платформы Windows 10, 11, Server 2016–2025
Зависимости только встроенные средства Windows
Лицензия MIT

Утилита сознательно не тащит модулей: quser, logoff.exe, Get-Process и Stop-Process есть в любой Windows. Это важно для сценария «пришёл на чужой сервер, где ничего нельзя ставить».

Возможности

Возможность Что даёт
Две стадии штатный выход плюс очистка остатков
-SkipLogoff пропустить первую стадию
-SkipProcessTermination пропустить вторую
-AllowCurrentSession разрешить снять свою же сессию
-PostLogoffDelaySeconds пауза 0–120 секунд между стадиями
-UserContext префикс для строгого сопоставления владельца процесса
-WhatIf показать план без изменений
-Confirm подтверждать каждый шаг
-Help встроенная справка
Структурный вывод Summary и Details для конвейера

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

Как это работает

flowchart LR
  PARAM["параметры и проверка прав"] --> FIND["поиск сессий пользователя"]
  FIND --> GUARD{"это текущая сессия?"}
  GUARD -- да --> SKIP["пропуск без -AllowCurrentSession"]
  GUARD -- нет --> OFF["logoff и очистка остатков"]
  SKIP --> OUT2["итог с числами"]
  OFF --> OUT2

Стадия 1: скрипт перечисляет сессии через quser (или query user) и разбирает вывод регуляркой, а не по колонкам. Это существенно: формат вывода quser зависит от локали, и разбор по позициям ломается на другой языковой версии Windows.

Дальше для каждой подходящей сессии вызывается logoff.exe <SessionId> /SERVER:localhost. Windows рассылает приложениям WM_QUERYENDSESSION и даёт около пяти секунд на сохранение — этого хватает почте и редакторам, но не хватает базам.

Стадия 2: после паузы (по умолчанию три секунды) выполняется Get-Process -IncludeUserName, отбираются процессы с нужным владельцем, и к ним применяется Stop-Process -Force. Это уже TerminateProcess — без предупреждения и без сохранения.

Обе стадии обёрнуты в SupportsShouldProcess, поэтому -WhatIf действительно ничего не делает, а -Confirm спрашивает по каждому действию.

Замеры и цифры

Проверено 14.11.2026, двумя проверками.

Что Значение
Архив тега v1.0.0 12 056 байт, SHA-256 9d03fe62…172978
Файлов в дереве 3 (logoff.ps1, README.md, LICENSE)
logoff.ps1 23 399 байт, 534 строки, 5 функций
Проверка синтаксиса парсером PowerShell 0 ошибок
Ассетов в релизе нет — установка из тега
-Help справка отдаётся, синтаксис с -WhatIf/-Confirm виден
Прогон -WhatIf с -UserContext длительность 3 852 мс, TotalRecords=2, все счётчики нулевые, код выхода 0
Прогон без -UserContext ошибка проверки параметра: пустое значение не подходит под шаблон
Живая проверка репозитория и релиза 200 / 200
Windows-LogOff 1.0.0: скрипт, прогон и границы

Главная находка прогона — поведение по умолчанию. Параметр -UserContext по умолчанию равен $env:COMPUTERNAME, и его значение проверяется шаблоном. В окружении, где эта переменная пуста (служба, часть CI-контейнеров, урезанная сессия), скрипт падает на собственном значении по умолчанию ещё до первой проверки сессий. Лечится одной строкой — явным -UserContext. В обычной интерактивной сессии проблемы нет: переменная заполнена.

Второе наблюдение: -WhatIf отработал примерно за 3,9 секунды и вернул объект с TotalRecords=2 при нулевых счётчиках успехов и ошибок. То есть перечисление реальное, действия — нет. Ровно то поведение, которого ждёшь от предпросмотра.

Сильные стороны продукта

  • Две стадии вместо одной. Штатный выход не всегда убирает пользователя из системы, а скрипт это учитывает.
  • Защита от самоубийства. Своя сессия пропускается, если не указан -AllowCurrentSession.
  • Локально-независимый разбор quser. Регулярка вместо индексов колонок.
  • -WhatIf и -Confirm. Прогон без изменений — штатная возможность, а не хак.
  • Структурный вывод. Summary и Details вместо текста: результат годится для журнала и конвейера.
  • Один файл без зависимостей. Работает там, где нельзя ставить модули.
  • Настраиваемая пауза. Между стадиями 0–120 секунд — под медленные приложения.
  • Встроенная справка. -Help и Get-Help без отдельных документов.

Установка

# из тега: ассетов в релизе нет
git clone --branch v1.0.0 --depth 1 https://github.com/paulmann/Windows-LogOff.git
cd Windows-LogOff

# справка
.\logoff.ps1 -Help

# предпросмотр без изменений
.\logoff.ps1 -UserName 'targetuser' -UserContext $env:COMPUTERNAME -WhatIf
Требование Значение
PowerShell 7.0 или новее
Права администратор (#Requires -RunAsAdministrator)
Политика запуска для локального файла нужен обход политики
Целевая система Windows 10, 11, Server 2016–2025
Зависимости нет

Настройка

flowchart TD
  A["требование администратора"] --> S["безопасный прогон"]
  B["-WhatIf и -Confirm"] --> S
  C["пропуск текущей сессии"] --> S
  D["разбор quser регуляркой"] --> S
  S --> E["результат: без случайных отключений"]
Параметр Зачем
-UserName кого снимать; без домена
-UserContext префикс владельца процессов; задавать явно, если COMPUTERNAME пуст
-PostLogoffDelaySeconds пауза перед очисткой: медленным приложениям нужно больше
-SkipLogoff только убрать остатки процессов
-SkipProcessTermination только штатный выход
-AllowCurrentSession разрешить снять себя
-WhatIf, -Confirm безопасный прогон

Отдельно про паузу: три секунды — разумное умолчание для офисных приложений, но если в сессии работает клиент базы данных или компилятор, стоит поднять до 10–15. Иначе вторая стадия убьёт процесс, который ещё сохранял файл.

Почему одного logoff недостаточно

Разница между «сессия закрыта» и «пользователь ушёл» на терминальном сервере видна на второй день эксплуатации.

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

Отсюда типичная жалоба: «я вышел, а система говорит, что я всё ещё вхожу». Причина не в Windows, а в остатке, который пережил выход. logoff.exe про него не знает и не сообщает.

Скрипт делает то, чего не делает logoff: после выхода сканирует процессы по владельцу и завершает оставшиеся. Это неприятная, но необходимая операция — и именно поэтому в нём есть -WhatIf, -Confirm и структурный результат: чтобы администратор видел, кого и что он снимает.

Как выбрать паузу между стадиями

Пауза — единственный параметр, который стоит подкручивать под конкретную машину.

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

Первая: клиенты баз данных. Клиент может держать соединение и незавершённую транзакцию; обрыв приводит к откату и, в худшем случае, к потере журнала. Здесь пауза 10–15 секунд и предупреждение пользователя.

Вторая: компиляция и сборки. Процесс msbuild или компилятор посреди работы не сохраняет промежуточные результаты — то, что успело записаться, останется, остальное пропадёт.

Третья: файловые операции. Копирование большого файла через сеть будет оборвано, и на приёмной стороне останется недописанный файл.

Практическое правило: паузу увеличивают на тех серверах, где работают «тяжёлые» приложения, и оставляют три секунды на обычных офисных.

Что видно в результате прогона

Вывод скрипта — объект, а не текст. В нём два свойства: Summary с общей картиной и Details с записями по каждой сессии и процессу.

В Summary есть имя компьютера, имя пользователя, длительность операции и три счётчика: успешные, неуспешные и пропущенные. В нашем предпросмотре длительность составила около 3,9 секунды, записей — две, счётчики успехов и ошибок нулевые: действия не выполнялись, шло только перечисление.

Поле с именем компьютера в этом прогоне оказалось пустым — следствие пустой переменной окружения. Это и есть причина ошибки по умолчанию: значение подставляется пустым и не проходит проверку. Явное указание -UserContext решает вопрос, и в Summary появляется заполненное имя.

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

Что происходит с профилем после выхода

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

Windows хранит профиль пользователя загруженным, пока есть хотя бы один процесс с его токеном. После закрытия сессии профиль должен выгрузиться, но если остался процесс — остаётся и профиль, а с ним занятые файлы: NTUSER.DAT, кэши, временные каталоги.

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

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

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

Подводные камни

  • Пустой COMPUTERNAME ломает умолчание. Ошибка проверки параметра до всякой работы; задавайте -UserContext явно.
  • Вторая стадия не сохраняет ничего. Stop-Process -Force — это TerminateProcess, данные не сохраняются.
  • Пять секунд мало для баз. Для приложений с длинным сохранением увеличивайте паузу и предупреждайте пользователей.
  • Нужны права администратора. Без них скрипт не стартует вовсе — это защита, а не недостаток.
  • Своя сессия пропускается по умолчанию. Отключить можно -AllowCurrentSession, но тогда админ рискует выкинуть сам себя посреди работы.
  • Сессия 0 и службы. Остатки от служб, запущенных от имени пользователя, попадут под очистку — проверяйте -WhatIf перед боевым прогоном.
  • quser локализован. Разбор регуляркой устойчивее, но экзотические локали всё равно стоит проверить.
  • Ассетов в релизе нет. Ставить надо из тега; на машине без git понадобится скачать архив.
  • Политика выполнения. В домене запуск локального .ps1 может быть запрещён — согласуйте способ заранее.
  • Возврат объекта, а не текста. В конвейере код выхода и Summary надо читать отдельно.
  • -WhatIf не заменяет проверку на тестовой машине. Он показывает план, но не поведение приложений.
  • Нет отката. Отключённую сессию не восстановить: сначала предупредите пользователя.

Как встроить в обслуживание сервера

Утилита полезна не только как ручной инструмент, но и как шаг регламента.

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

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

Третий: разбор инцидента. Профиль занят, пользователь не входит — прогон с -SkipLogoff убирает только остатки процессов и не трогает сессии. Это точечный вариант, который экономит время.

Четвёртый: смена подрядчика или увольнение. Снять все сессии учётной записи и убедиться, что процессов не осталось, — часть процедуры отзыва доступа.

Общее для всех сценариев: сначала предпросмотр, потом предупреждение, потом действие. Скрипт даёт техническую возможность, а порядок остаётся на администраторе.

Где утилита не нужна

Границы простые.

Не нужна на рабочей станции с одной сессией: там достаточно штатного выхода.

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

Не нужна как замена контролю доступа: она снимает сессии, но не управляет правами и не ведёт аудит входов.

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

Зато нужна на серверах, где сессии копятся: терминальные хосты, машины с удалённым доступом, тестовые стенды, где забытые сессии держат память и лицензии. И нужна в тех процедурах, где важно не «примерно», а точно: снять всех, убедиться, что никого не осталось, и записать числа в журнал.

Чек-лист

  • [ ] Проверить версию PowerShell: 7.0 или новее.
  • [ ] Убедиться, что запуск идёт от администратора.
  • [ ] Задать -UserContext явно, если COMPUTERNAME пуст.
  • [ ] Сделать прогон с -WhatIf и прочитать список сессий.
  • [ ] Сверить, что своя сессия не попадёт под действие.
  • [ ] Проверить, не держит ли пользователь файлы и базы.
  • [ ] Предупредить пользователя о выходе.
  • [ ] Выполнить боевой прогон, при необходимости увеличив паузу.
  • [ ] Проверить Summary: сколько сессий снято, сколько процессов завершено.
  • [ ] Убедиться, что профиль освобождён и повторный вход проходит.
  • [ ] Записать время и список снятых сессий в журнал работ.

Что было в заметках к релизу и что добавилось здесь

Заметки первого выпуска — 9 192 знака: подробное описание двух стадий, список возможностей, быстрый старт. Для обзора продукта этого достаточно по смыслу, но не хватало проверки: всё сказанное было заявлено, а не измерено.

Было в заметках Добавлено в статье
описание стадий разбор, почему одной стадии мало на практике
«проверка локали регуляркой» объяснение, что именно ломается при разборе по колонкам
список параметров рекомендации по паузе под тип приложений
«работает на Windows Server» прогон на живой машине с числами и найденной ловушкой

Главное добавление — ловушка с пустым COMPUTERNAME. В заметках она не упоминается, потому что в обычной интерактивной сессии переменная заполнена; обнаруживается она там, где скрипт запускают из службы или урезанного окружения. Именно такие детали и определяют, сработает ли утилита в чужой инфраструктуре.

FAQ

Чем это отличается от logoff.exe?

logoff.exe закрывает сессию. Скрипт добавляет вторую стадию — завершение процессов, которые пережили выход, — и возвращает структурный результат.

Можно ли запустить без изменений?

Да: -WhatIf показывает план и ничего не делает. Перечисление сессий при этом реальное.

Почему падает сразу после запуска?

Скорее всего, пустой $env:COMPUTERNAME: значение по умолчанию не проходит проверку шаблона. Укажите -UserContext явно.

Сохранятся ли данные в открытых приложениях?

На первой стадии — да, у приложений есть около пяти секунд. На второй — нет: Stop-Process -Force не сохраняет ничего.

Снимет ли он мою собственную сессию?

Нет, по умолчанию текущая сессия пропускается. Для обратного нужен -AllowCurrentSession.

Нужны ли модули PowerShell?

Нет: только встроенные средства Windows и PowerShell 7.

Что делать, если профиль остался занятым?

Проверьте Summary и Details: скорее всего, часть процессов не завершилась из-за прав. Тогда помогает повторный прогон с -SkipLogoff.

Почему структура кода имеет значение для такого скрипта

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

Первое: 534 строки и пять функций — это немного для задачи. Основная работа в одной функции Invoke-WindowsUserForceLogoff, остальные три вспомогательные: текущая сессия, список сессий, список процессов. Такая раскладка означает, что читать и проверять код можно целиком, а не по частям.

Второе: #Requires -RunAsAdministrator в начале файла — правильное решение. Она превращает «надо бы от админа» в жёсткое требование, которое срабатывает до любого действия. Альтернатива — проверка внутри скрипта — оставляет окно, в котором часть работы уже сделана.

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

Четвёртое: объект вместо текста. SupportsShouldProcess и возвращаемый PSCustomObject — это два признака, по которым администраторские скрипты отличаются от «однострочников на скорую руку». С таким результатом можно работать в конвейере: считать, логировать, сравнивать между прогонами.

И честная оговорка: структура не отменяет риска. Вторая стадия — это TerminateProcess, и никакая аккуратность кода не сделает её безопасной для несохранённых данных.

Итог

Windows-LogOff 1.0.0 решает узкую, но регулярную задачу: снять пользователя с машины полностью, а не «в основном». Две стадии, защита от снятия собственной сессии, -WhatIf и структурный результат — ровно тот набор, который отличает администраторскую утилиту от однострочника. Прогон подтвердил: 23 399 байт, 534 строки, 5 функций, ноль ошибок синтаксиса, предпросмотр отрабатывает за 3,9 секунды без изменений. И он же показал ловушку: на машинах с пустым COMPUTERNAME умолчание параметра не проходит проверку — задавайте -UserContext явно.

Цитата: Сессия закрывается одной командой, а пользователь уходит из системы — только если за ним прибрать.

Связанные материалы

LLM Summary

  • Тема: Windows-LogOff 1.0.0 — PowerShell 7-утилита для принудительного завершения пользовательских сессий и очистки остаточных процессов на Windows.
  • Две стадии: (1) перечисление сессий через quser/query user и logoff.exe <SessionId> /SERVER:localhost — около 5 секунд на сохранение; (2) через паузу (по умолчанию 3 с, диапазон 0–120) скан Get-Process -IncludeUserName и Stop-Process -Force.
  • Параметры: -UserName, -UserContext (по умолчанию $env:COMPUTERNAME), -SkipLogoff, -SkipProcessTermination, -AllowCurrentSession, -PostLogoffDelaySeconds, -Help, плюс -WhatIf/-Confirm.
  • Требования: PowerShell 7.0+, администратор (#Requires -RunAsAdministrator), Windows 10/11/Server 2016–2025, зависимостей нет.
  • Проверено 14.11.2026: архив тега — 12 056 байт, SHA-256 9d03fe62…172978, 3 файла; logoff.ps1 — 23 399 байт, 534 строки, 5 функций, парсер PowerShell даёт 0 ошибок; ассетов в релизе нет.
  • Прогон: -Help отдаёт справку с -WhatIf/-Confirm; -WhatIf -UserContext TESTHOST — 3 852 мс, TotalRecords=2, счётчики успехов и ошибок нулевые, код выхода 0; без -UserContext — ошибка проверки параметра, так как $env:COMPUTERNAME пуст.
  • Живая проверка: страницы репозитория и релиза — 200 / 200.
  • Границы: вторая стадия не сохраняет данные, прав администратора требует, откат невозможен, quser локализован, сессия 0 и службы попадают под очистку.
  • Не проверялось: реальное снятие чужой сессии и поведение на терминальном сервере под нагрузкой.
  • Ссылки: https://github.com/paulmann/Windows-LogOff, https://github.com/paulmann/Windows-LogOff/releases/tag/v1.0.0

Промт для агента (установка и настройка)

Готовый промт: скопируйте блок целиком. Источник шагов — README.md репозитория и встроенная справка -Help (файла AGENTS.md нет — проверено 14.11.2026), сверено с прогоном на живой машине.

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

Материалы:
- репозиторий: https://github.com/paulmann/Windows-LogOff
- README: https://github.com/paulmann/Windows-LogOff/blob/main/README.md
- релиз: https://github.com/paulmann/Windows-LogOff/releases/tag/v1.0.0

Шаг 1. Клонировать тег v1.0.0 (ассетов в релизе нет) или скачать архив.
Шаг 2. Проверить PowerShell: 7.0+, запуск от администратора.
Шаг 3. Задать -UserContext явно, если $env:COMPUTERNAME пуст.
Шаг 4. Предпросмотр: .\logoff.ps1 -UserName '<user>' -UserContext $env:COMPUTERNAME -WhatIf -Verbose
Шаг 5. Проверить в списке, что своя сессия не попадёт под действие.
Шаг 6. Предупредить пользователя; при работе с базами поднять -PostLogoffDelaySeconds до 10–15.
Шаг 7. Боевой прогон: то же без -WhatIf.
Шаг 8. Прочитать Summary: сессии сняты, процессы завершены, ошибок нет.
Шаг 9. Проверить, что повторный вход проходит и профиль не занят.
Шаг 10. Записать в журнал время, пользователя и число снятых сессий.

Запрещено:
- запускать боевой прогон без -WhatIf на первом шаге;
- использовать -AllowCurrentSession без понимания, что можно выкинуть себя;
- ставить паузу 0 при работе с приложениями, которые сохраняют файлы;
- полагаться на то, что данные в открытых документах сохранятся после второй стадии;
- запускать без прав администратора и менять политику выполнения «навсегда»;
- снимать сессии без предупреждения пользователя.

Что считать успехом: предпросмотр показывает ожидаемые сессии, боевой прогон завершён, Summary без ошибок, профиль освобождён, повторный вход работает, запись в журнале есть.