Ограничение числа процессов толстого клиента 1С для пользователя Windows: Python, psutil и подмена ярлыка 1cestart
База данных - Администрирование СУБД
Ограничение числа одновременных процессов 1С для пользователей Windows сводится к подсчёту экземпляров 1cv8c.exe и 1cv8s.exe и к вызову штатного 1cestart.exe только пока лимит не исчерпан. Дальше разберём установку Python, счётчик через psutil и подмену цели ярлыка запуска толстого клиента 1С:Предприятие 8.3.
Когда одного пользователя и пятнадцати баз становится слишком много
На терминальном сервере или в офисе, где десятки информационных баз, картина повторяется: один сотрудник держит открытыми несколько клиентов сразу, иногда до пятнадцати окон. Каждый экземпляр толстого клиента забирает память на рабочем месте и добавляет нагрузку на сервер 1С и СУБД. Отклик сервера проседает, в консоли кластера виден всплеск одновременных сеансов и соединений, страдают те, кто работает в одной-двух базах.
Штатный ярлык «1C:Enterprise» или «1С:Предприятие» ведёт напрямую на 1cestart.exe. В этом ярлыке нет проверки числа процессов 1cv8c для текущей учётной записи Windows. Лимиты кластера, лицензирование и параметры безопасности процессы на рабочем ПК не считают.
Мы ограничиваем запуск на рабочей станции: перед каждым открытием из корпоративного ярлыка скрипт смотрит, сколько процессов толстого клиента и стартера уже принадлежит текущему пользователю, и только при соблюдении лимита вызывает привычное окно выбора базы через 1cestart.exe. Конфигурации 1С, расширения и регламентные задания на сервере не трогаем.
Под «процессами 1С» здесь имеются в виду 1cv8c.exe (работающий клиент) и 1cv8s.exe (окно стартера). «Ограничение в разрезе пользователей Windows» означает учётную запись, из-под которой запущен ярлык; чужие процессы на том же компьютере в лимит не входят. Тонкий клиент 1cv8.exe и веб-клиент в эту схему не входят.
Лимит на ярлыке срабатывает в момент попытки открыть ещё одно окно, то есть раньше, чем администратор успевает заметить пик нагрузки только по сеансам на сервере.
Схема решения: ярлык, проверка, 1cestart.exe
Ярлык для пользователя тот же по смыслу, но в свойствах вместо 1cestart.exe указан Limit1CStart.exe (или скрипт Python до сборки). Windows сначала запускает прослойку, та по полному пути вызывает 1cestart.exe. Выбор базы и параметры запуска остаются внутри стартера.
Скрипт отрабатывает только в момент вызова из ярлыка, поэтому стартер ограничивают отдельно. Если разрешить бесконечно открывать 1cv8s.exe, пользователь заранее поднимет несколько окон выбора базы и из каждого запустит клиент, обойдя проверку по 1cv8c. Правило «не более одного процесса 1cv8s.exe» закрывает эту лазейку в модели «контроль при каждом клике по ярлыку».
На 64-bit Windows 1cestart.exe чаще лежит в каталоге common установки платформы, например C:\Program Files (x86)\1cv8\common\1cestart.exe или C:\Program Files\1cv8\common\1cestart.exe. Встречается и путь через bin конкретной сборки 8.3.xx.xxxx: у релизов номер в каталоге меняется. Я сверяю фактический путь в свойствах штатного ярлыка «1C:Enterprise»; при необходимости смотрю ветки Uninstall в HKLM или HKCU для записи «1C:Enterprise 8.x» и поле DisplayIcon, откуда виден каталог bin и далее common. В константе ICESTART_PATH в коде должна стоять одна строка с вашим реальным путём, а не чужой пример из статьи.
EXE в сетевой папке имеет смысл, когда на всех рабочих местах одна версия платформы и один путь к common; в остальных случаях проще локальная копия с конфигом в ProgramData.
Ярлык параметры базы не передаёт: он лишь стартует прослойку проверки. Все параметры запуска пользователь задаёт внутри 1cestart.
Логика подсчёта процессов через psutil
Библиотека psutil обходит список процессов с атрибутами name и username без подключения к WMI. В исходных публикациях ту же роль играет процедура OpenCount1C(Num); в листинге ниже она названа open_count_1c, смысл тот же. Num - максимальное число одновременных клиентов 1cv8c.exe для текущего пользователя.
Алгоритм такой. Создаём два списка для процессов текущей учётной записи. В цикле psutil.process_iter с атрибутами username и name для каждой записи смотрим name и владельца. Сначала отбрасываем пустое username: у чужих процессов psutil часто не показывает владельца (в терминах API это NULL), и одного этого фильтра мало. Затем сравниваем имя владельца с текущим USERNAME и при необходимости с DOMAIN\user - так в лимит попадают только процессы этой учётной записи, даже если ярлык запускали «от имени администратора» и владелец чужих сеансов иногда виден.
Если name равен 1cv8c.exe и владелец совпал, имя попадает в список клиентов. То же для 1cv8s.exe - в список стартеров. CountList1cv8c и CountList1cv8s - длины списков. Запуск 1cestart разрешён, когда стартеров ещё нет и CountList1cv8c меньше Num; иначе показываем MessageBox с текстом от администратора, пишем строку в deny.log и завершаем процесс. Если 1cv8s уже один или больше, показываем отдельное сообщение про окно стартера, при желании фиксируем отказ в deny.log и выходим без нового 1cv8s.
Лимит «N баз одновременно» - это N процессов 1cv8c.exe. Попытка открыть клиент номер N плюс один блокируется, пока пользователь сам не закроет один из работающих экземпляров.
import ctypes
import json
import os
import sys
from datetime import datetime
from pathlib import Path
try:
import psutil
except ImportError:
ctypes.windll.user32.MessageBoxW(
0,
"Не установлен модуль psutil. Обратитесь к администратору.",
"Ошибка запуска 1С",
16,
)
sys.exit(1)
ICESTART_PATH = r"C:\Program Files (x86)\1cv8\common\1cestart.exe"
CONFIG_DIR = Path(os.environ.get("ProgramData", r"C:\ProgramData")) / "Limit1C"
CONFIG_PATH = CONFIG_DIR / "limits_1c.json"
LOG_PATH = CONFIG_DIR / "deny.log"
def load_limits():
default = {"default": 4, "users": {}, "exclude": []}
if CONFIG_PATH.is_file():
with CONFIG_PATH.open(encoding="utf-8") as fh:
data = json.load(fh)
default.update(data)
return default
def _username_variants():
short = os.environ.get("USERNAME", "")
domain = os.environ.get("USERDOMAIN", "")
variants = set()
if short:
variants.add(short.upper())
if domain:
variants.add(f"{domain}\\{short}".upper())
return variants
def _process_belongs_to_me(proc_username):
if not proc_username:
return False
proc = proc_username.upper()
if proc in _username_variants():
return True
short = os.environ.get("USERNAME", "")
if short and proc.split("\\")[-1] == short.upper():
return True
return False
def log_deny(username, count_c, limit, reason="1cv8c_limit"):
try:
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
with LOG_PATH.open("a", encoding="utf-8") as fh:
fh.write(
f"{datetime.now().isoformat(timespec='seconds')}"
f"\t{username}\t{reason}\t1cv8c={count_c}\tlimit={limit}\n"
)
except OSError:
pass
def open_1cestart():
if not os.path.isfile(ICESTART_PATH):
ctypes.windll.user32.MessageBoxW(
0,
f"Не найден 1cestart.exe:\n{ICESTART_PATH}",
"Ошибка конфигурации",
16,
)
sys.exit(1)
os.startfile(ICESTART_PATH)
def open_count_1c(num):
list_1cv8c = []
list_1cv8s = []
try:
for proc in psutil.process_iter(attrs=["username", "name"]):
info = proc.info
if not _process_belongs_to_me(info.get("username")):
continue
name = info.get("name")
if name == "1cv8c.exe":
list_1cv8c.append(name)
elif name == "1cv8s.exe":
list_1cv8s.append(name)
except (psutil.AccessDenied, psutil.NoSuchProcess):
ctypes.windll.user32.MessageBoxW(
0,
"Нет прав на просмотр списка процессов. Запуск без проверки отменён.",
"Ошибка",
16,
)
sys.exit(1)
count_1cv8c = len(list_1cv8c)
count_1cv8s = len(list_1cv8s)
username = os.environ.get("USERNAME", "")
if count_1cv8s >= 1:
log_deny(username, count_1cv8c, num, reason="1cv8s_already_running")
ctypes.windll.user32.MessageBoxW(
0,
"Уже открыто окно стартера 1С. Закройте лишнее окно выбора базы.",
"Уведомление",
48,
)
sys.exit(0)
if count_1cv8c < num:
open_1cestart()
else:
log_deny(username, count_1cv8c, num)
message = (
"Администратором запрещено запускать более "
+ str(num)
+ " процессов 1С одновременно. Закройте лишнее окно клиента."
)
ctypes.windll.user32.MessageBoxW(0, message, "Уведомление о блокировке", 48)
sys.exit(0)
def main():
cfg = load_limits()
username = os.environ.get("USERNAME", "")
if username in cfg.get("exclude", []):
open_1cestart()
return
limit = cfg.get("users", {}).get(username, cfg.get("default", 4))
open_count_1c(int(limit))
if __name__ == "__main__":
main()
Пример limits_1c.json в каталоге Limit1C под ProgramData (или рядом со скриптом на этапе отладки) заменяет длинные ветки elif по USERNAME:
{
"default": 4,
"exclude": ["Sobstvennik"],
"users": {
"Buhgalter1": 8,
"Buhgalter2": 12
}
}
Имена учётных записей в JSON должны совпадать с тем, что возвращает os.environ['USERNAME'] (часто латиница без домена). Для доменных профилей иногда удобнее сверять полное DOMAIN\user - в коде это учтено при сравнении владельца процесса.
На загруженном сервере или на сервере терминалов (Remote Desktop) с сотнями фоновых процессов пауза после двойного щелчка по ярлыку заметна. В типовых сравнениях из публикаций про такие скрипты обход через psutil обычно быстрее, чем через WMI: порядка десяти секунд против порядка тридцати восьми при около тысяче процессов. Это не обязательно ваш замер на площадке; если пользователи жалуются на задержку перед стартером, один раз сравните psutil и WMI у себя и зафиксируйте в регламенте.
Лимиты по пользователям и исключения
Персональные лимиты нужны, когда у бухгалтерии по регламенту открыто больше баз, чем у кладовщика, а у владельца или главного администратора счётчик вообще не должен срабатывать. В исходной схеме это ветки if USERNAME равен… с вызовом OpenCount1C(8) или прямым os.startfile для исключения.
Минус хардкода в коде: каждая смена состава требует правки скрипта и пересборки EXE. Файл limits_1c.json в каталоге, доступном администратору, позволяет менять цифры без компилятора. Конфиг отдают через групповые политики; пользователям выдают собранный EXE без исходников.
Исключение «без лимита» лучше явно перечислять в exclude. Исключения документируйте в конфиге и в регламенте, чтобы через полгода было ясно, почему у одной учётной записи снова много клиентов. Успешные запуски для exclude в deny.log не пишутся - только отказы по лимиту и по стартеру.
Дефолт для всех остальных, например четыре базы, задаётся одной строкой в конфиге. Новый сотрудник попадает под дефолт, пока администратор не добавит его в users. В main() после load_limits() проверяют exclude, затем берут limit из users.get(USERNAME, умолчанию) и передают в open_count_1c(limit).
Подготовка окружения: Python, pip, psutil
На Windows 10 и 11 я ставлю Python 3.10 или новее с python.org или через winget, с галочкой «Add python.exe to PATH». Лаунчер py вызывает интерпретатор без путаницы, когда установлено несколько версий: py -3 --version.
У свежего инсталлятора с python.org pip часто уже есть; если нет, скачивают официальный get-pip.py, открывают cmd в каталоге с файлом и выполняют py get-pip.py. Зависимость одной командой: py -m pip install --upgrade pip psutil.
На админском ПК для изоляции можно создать venv: py -m venv C:\Tools\limit1c-venv, активировать C:\Tools\limit1c-venv\Scripts\activate.bat, установить psutil внутри и гонять скрипт до сборки. На пользовательских машинах venv не нужен, если разворачиваем только EXE.
pip иногда попадает не в тот интерпретатор, и собранный EXE «не видит» psutil. Проверка сразу после установки: py -m pip show psutil.
Обычному пользователю хватает прав на чтение списка процессов. На ужесточённых политиках возможен AccessDenied для части записей; тогда в коде лучше не пускать в 1С молча, а показать сообщение, иначе создаётся ложное ощущение контроля.
Сборка EXE и настройка ярлыка
Пользователям проще ярлык на EXE, чем на.py и отдельный Python. Для админов удобнее auto-py-to-exe (py -m pip install auto-py-to-exe, затем py -m auto_py_to_exe); в CI или скриптах развёртывания - PyInstaller одной командой.
Пример onefile через PyInstaller после установки py -m pip install pyinstaller:
py -m PyInstaller --onefile --noconsole --name Limit1CStart limit_1c_start.py
Флаг --noconsole убирает мигающее чёрное окно при двойном щелчке. Файл limits_1c.json и deny.log для onefile кладут в C:\ProgramData\Limit1C\ (как в листинге CONFIG_PATH), а не рядом с временной распаковкой EXE, иначе после первого запуска конфиг «пропадёт». Установщик или GPO создаёт каталог и кладёт JSON.
Копируем существующий ярлык 1С, переименовываем, например «1С (лимит)». В свойствах в поле «Объект» указываем полный путь к Limit1CStart.exe. «Рабочая папка» может совпадать с каталогом EXE; конфиг читается из ProgramData. Иконку можно оставить от платформы, указав путь к 1cv8.exe или стандартному ico из bin.
Проверка вручную: при нуле клиентов ярлык открывает стартер. Из стартера поднимаем базы до лимита N. Следующий клик по ярлыку при N активных 1cv8c должен показать MessageBox и не открыть второй стартер, если 1cv8s уже есть. Закрыли один клиент - запуск снова разрешён. Два быстрых двойных щелчка подряд по ярлыку не должны оставить два 1cv8s (между обходом процессов и os.startfile теоретически возможна гонка; на тестовом ПК это стоит щёлкнуть дважды нарочно).
Через GPO кладут ярлык в общее меню «Пуск», старые ярлыки на прямой 1cestart убирают или перекрывают политикой запрета ярлыков на рабочем столе с прямым путём к bin. Иначе часть пользователей останется на обходе.
На тестовом ПК можно сравнить число процессов 1cv8c в диспетчере задач: с прямым ярлыком окна набираются без потолка, со скриптом-проверкой упираются в N.
Ограничения подхода и сочетание с администрированием 1С
Модель «только при запуске из ярлыка» обходят. Прямой запуск 1cestart.exe или 1cv8.exe из каталога bin, второй ярлык, автозапуск в профиле, копия exe на сетевом диске, ярлык из установщика платформы после очередного релиза 8.3 - всё это минует скрипт. Меры: один утверждённый ярлык, политики на рабочий стол, регламент, при необходимости AppLocker или WDAC на запуск 1cestart только из разрешённого каталога (отдельный проект по ИБ).
На сервере терминалов (Remote Desktop) лимит считается внутри сессии пользователя, где крутится скрипт. Два пользователя на одном хосте друг другу не мешают. Один пользователь с двумя сессиями, если политика это разрешает, получит два независимых счётчика - редкий, но возможный двойной лимит.
На сервере 1С процесс на ПК пользователя не тождественен одному сеансу кластера: один клиент - одно соединение, но зависшие сеансы, фоновые задания и внешние соединения через COM (V83.COMConnector) смотрим отдельно. Список сеансов и блокировки смотрите в консоли администрирования кластера и через rac/rac.exe к host:port агента кластера с учётными данными администратора кластера. Скрипт режет лишние окна толстого клиента на ПК; сеансы на сервере контролируются отдельно.
Ограничение процессов не подменяет лимиты сеансов и лицензий платформы: это локальный барьер от множества процессов клиента на рабочей станции, а не способ уменьшить число клиентских лицензий и перестать следить за кластером. Имеет смысл держать и лимит на ПК, и просмотр сеансов раз в смену по регламенту «один пользователь - N баз».
Тонкий и веб-клиент эту прослойку не затрагивают; при смешанном ландшафте лимит дублируют политиками там, где люди реально работают.
Перед раскаткой на группу рабочих мест проверьте три вещи: после смены версии платформы 8.3 путь ICESTART_PATH совпадает с фактическим 1cestart.exe; limits_1c.json читается из ProgramData; при искусственном превышении лимита появляется MessageBox и строка в deny.log. Если MessageBox не появляется, а клиенты множатся, первым делом смотрите диспетчер задач: путь к образу 1cv8c.exe покажет, стартовали ли в обход ярлыка.
Вступайте в нашу телеграмм-группу Инфостарт
Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.