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

TL;DR: две стадии и один важный флаг
- Стадия 1. Перечисление сессий через
quserиlogoff.exeпо идентификатору — с пятью секундами на сохранение данных. - Стадия 2. Через паузу — скан
Get-Process -IncludeUserNameиStop-Process -Forceдля остатков. - Безопасность.
#Requires -RunAsAdministrator, поддержка-WhatIf/-Confirm, пропуск своей сессии. - Результат. Объект с
SummaryиDetails— не текст в консоль, а данные для конвейера. - Грабля. Если
$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 |
Главная находка прогона — поведение по умолчанию. Параметр -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 явно.
Цитата: Сессия закрывается одной командой, а пользователь уходит из системы — только если за ним прибрать.
Связанные материалы
- RDP Diagnostic Tool 5.0.1: аудит RDP одной командой — диагностика того же стека удалённого доступа.
- sr-search-replace: замена по дереву с журналом — про
--dry-runи коды выхода в CLI. - MD.Scribe 3.5.0: Mermaid 12 и починенный ремонт — ещё один инструмент с самопроверкой после обновления.
- BAS-IP 2.0.2: система управления шлагбаумом — учёт доступа и журнал событий.
- Релиз v1.0.0 — заметки к первому выпуску.
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 без ошибок, профиль освобождён, повторный вход работает, запись в журнале есть.