Compare commits

..
4 Commits
Author SHA1 Message Date
saturnandClaude 72ce3541f5 Поиск сеансов пользователя по домену и добавление баз прямо в них
При перемещаемых профилях базу нужно писать в профиль на той машине, где у
пользователя открыт сеанс, иначе правку затрёт при выходе. Раньше, чтобы
узнать, где человек сидит, приходилось отдельно лезть в Dameware.

Что добавлено:
- ad_sessions.py — обнаружение сеансов: список компьютеров из AD через ADSI
  (RSAT не нужен), отсев выключенных по порту 445, опрос WTS, разрешение
  реальной папки профиля через SID и реестр ProfileList, с фоллбэком на
  перебор имён папок, если служба RemoteRegistry отключена.
- Панель «Сеансы пользователя в домене»: поиск по имени, выбор сеансов
  галочками и отдельная кнопка добавления баз в отмеченные сеансы. Старый
  режим записи по профилям остался для пользователей без активных сеансов.
- Отключённые сеансы учитываются наравне с активными: они тоже держат
  профиль загруженным, и запись в серверную копию так же затрётся.
- Фильтр по паре домен + пользователь, чтобы локальные учётки с тем же
  именем не смешивались с доменными.
- Enter в полях ввода запускает соответствующее действие.

Скорость (на парке в 316 машин опрос занимал около 90 секунд):
- WTS вызывается через ctypes, а не pywin32: pywin32 не отпускает GIL на
  время нативного вызова, из-за чего опрос шёл строго последовательно и
  никакие потоки не помогали. На живом домене 21,4 с -> 1,3 с.
- Проверка порта и опрос WTS слиты в один проход, потоков 256.
- Машины, зависшие на RPC, не ждём дольше 4 секунд после того, как очередь
  разобрана: прервать вызов нельзя, но можно перестать его ждать.
- Снимок домена кэшируется на 2 минуты, поэтому поиск следующего
  пользователя мгновенный; для принудительного обновления есть кнопка
  «Пересканировать».
- COM инициализируется явно в каждом потоке: без этого повторное
  сканирование из нового потока падало с MK_E_SYNTAX.

Безопасность записи: перед добавлением сеансы перепроверяются на живость, а
если сеансов не нашлось и при этом часть машин не ответила — выводится
предупреждение, чтобы базу не записали в перемещаемый профиль вслепую.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 17:14:32 +03:00
saturnandClaude 9331eff420 Диагностический пробник обнаружения сеансов в домене
PowerShell-скрипт для проверки, как поиск сеансов поведёт себя в незнакомом
домене, до того как разворачивать там программу: список компьютеров из AD,
отсев выключенных машин по порту 445, перечисление сеансов через WTS API и
разрешение папки профиля через SID + удалённый реестр ProfileList.

Сеансы читаются через P/Invoke к wtsapi32, а не разбором вывода quser —
тот зависит от локали Windows и ломается на русской версии.

Python на целевой машине не нужен, скрипт только читает данные и ничего
не меняет. Показывает тайминги по шагам и группирует причины отказов —
по ним видно, где закрыт RPC и где отключена служба RemoteRegistry.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-29 17:14:32 +03:00
saturnandClaude Opus 4.8 c3345a94e8 Удаление баз, поиск, дублирование в ZIP-профиль + фикс скролла
- Панель «Подключённые базы»: клик по пользователю показывает его базы,
  удаление отмеченных с подтверждением
- Поиск по пользователям и базам (галочки сохраняются при фильтрации)
- Опциональное дублирование add/delete в ZIP-профиль (автопоиск папки <user>.*)
- Устойчивое чтение ibases.v8i (UTF-8 BOM/без/cp1251)
- Интерфейс переведён на русский, окно резиновое
- Фикс: сброс прокрутки списка наверх при поиске
- Новая иконка download.ico, spec теперь версионируется

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 12:11:24 +03:00
saturnandClaude Opus 4.8 fdfd79346d Initial commit: рабочая версия Main_v3 (add + фильтр дублей)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:50:56 +03:00
22 changed files with 1810 additions and 16350 deletions
+3
View File
@@ -0,0 +1,3 @@
__pycache__/
build/
dist/
+11 -3
View File
@@ -6,7 +6,14 @@ a = Analysis(
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[], datas=[],
hiddenimports=[], # ad_sessions импортируется внутри try/except, а pywin32 подтягивает часть
# модулей динамически — перечисляем явно, иначе их не будет в сборке
hiddenimports=[
'ad_sessions',
'win32timezone',
'win32com.client',
'win32security',
],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},
runtime_hooks=[], runtime_hooks=[],
@@ -22,17 +29,18 @@ exe = EXE(
a.binaries, a.binaries,
a.datas, a.datas,
[], [],
name='Main_v3', name='1C_Base_Adder',
debug=False, debug=False,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, strip=False,
upx=True, upx=True,
upx_exclude=[], upx_exclude=[],
runtime_tmpdir=None, runtime_tmpdir=None,
console=True, console=False,
disable_windowed_traceback=False, disable_windowed_traceback=False,
argv_emulation=False, argv_emulation=False,
target_arch=None, target_arch=None,
codesign_identity=None, codesign_identity=None,
entitlements_file=None, entitlements_file=None,
icon=['download.ico'],
) )
Submodule 1C_Bases deleted from 0aa3007c5e
+780 -213
View File
File diff suppressed because it is too large Load Diff
+531
View File
@@ -0,0 +1,531 @@
# -*- coding: utf-8 -*-
"""Обнаружение сеансов пользователей по домену.
Логика повторяет то, как это делает Dameware NT Utilities:
1. список компьютеров берём из AD (ADSI/LDAP, RSAT не нужен);
2. быстро отсеиваем выключенные машины проверкой порта 445 (асинхронно);
3. живые опрашиваем через WTS API — тот же интерфейс, что у диспетчера задач,
без разбора текстового вывода quser (он зависит от локали Windows).
Модуль не содержит GUI и может использоваться отдельно.
"""
import ctypes
import os
import socket
import threading
import time
from collections import deque
from ctypes import wintypes
import win32api
import win32con
import win32security
# WTS дёргаем через ctypes, а не через pywin32, сознательно: pywin32 не отпускает
# GIL на время нативного вызова, из-за чего опрос машин выполняется строго
# последовательно и никакие потоки не помогают. ctypes.WinDLL GIL освобождает,
# и параллельный опрос домена начинает работать по-настоящему.
_wts = ctypes.WinDLL("wtsapi32.dll", use_last_error=True)
WTS_CURRENT_SERVER_HANDLE = 0
# Классы информации о сеансе (WTS_INFO_CLASS)
_WTS_USER_NAME = 5
_WTS_DOMAIN_NAME = 7
_WTS_CLIENT_NAME = 10
class _WTS_SESSION_INFOW(ctypes.Structure):
_fields_ = [
("SessionId", wintypes.DWORD),
("pWinStationName", wintypes.LPWSTR),
("State", ctypes.c_int),
]
_wts.WTSOpenServerW.argtypes = [wintypes.LPWSTR]
_wts.WTSOpenServerW.restype = wintypes.HANDLE
_wts.WTSCloseServer.argtypes = [wintypes.HANDLE]
_wts.WTSCloseServer.restype = None
_wts.WTSEnumerateSessionsW.argtypes = [
wintypes.HANDLE, wintypes.DWORD, wintypes.DWORD,
ctypes.POINTER(ctypes.POINTER(_WTS_SESSION_INFOW)),
ctypes.POINTER(wintypes.DWORD),
]
_wts.WTSEnumerateSessionsW.restype = wintypes.BOOL
_wts.WTSQuerySessionInformationW.argtypes = [
wintypes.HANDLE, wintypes.DWORD, ctypes.c_int,
ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(wintypes.DWORD),
]
_wts.WTSQuerySessionInformationW.restype = wintypes.BOOL
_wts.WTSFreeMemory.argtypes = [ctypes.c_void_p]
_wts.WTSFreeMemory.restype = None
# Значения по умолчанию для сканирования домена.
# Опрос упирается не в процессор, а в ожидание сети, поэтому потоков берём много.
DEFAULT_WORKERS = 256
# Жёсткий предел на весь опрос. Машины, у которых открыт порт 445, но закрыт RPC,
# вешают WTSOpenServer на 20-45 секунд — без общего дедлайна они растягивают скан.
DEFAULT_DEADLINE = 30.0
DEFAULT_PORT_TIMEOUT = 0.4
# Сколько ждать отставшие машины после того, как очередь разобрана.
# Зависшие на RPC не дождутся никогда, а живые обычно отвечают за доли секунды.
DEFAULT_GRACE = 4.0
# Состояния сеанса WTS
WTS_ACTIVE = 0
WTS_DISCONNECTED = 4
STATE_NAMES = {
0: "Активен",
1: "Подключается",
2: "Запрос подключения",
3: "Теневой",
4: "Отключён",
5: "Простой",
6: "Ожидание",
7: "Сброс",
8: "Отключение",
9: "Инициализация",
}
# Сеансы, которые держат профиль пользователя загруженным. Отключённый (Disconnected)
# сеанс профиль НЕ выгружает, поэтому при перемещаемых профилях писать в него нужно
# так же, как в активный — иначе правку затрёт при выходе пользователя.
PROFILE_HELD_STATES = (WTS_ACTIVE, WTS_DISCONNECTED)
PROFILE_LIST_KEY = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"
# ------------------------------------------------------------------ AD
def list_domain_computers():
"""Список компьютеров домена (активные учётки) через ADSI.
Возвращает список имён (FQDN, если заполнен dNSHostName).
Используется только pywin32 — дополнительных зависимостей и RSAT не требуется.
"""
import pythoncom
import win32com.client
# COM инициализируется отдельно в КАЖДОМ потоке. Ленивый импорт win32com делает
# это неявно только для того потока, где импорт случился первым, поэтому повторный
# поиск (кнопка «Пересканировать» создаёт новый поток) падал с MK_E_SYNTAX
# «Синтаксическая ошибка». Инициализируем явно и симметрично освобождаем.
try:
pythoncom.CoInitialize()
com_ready = True
except Exception:
com_ready = False # поток уже в другой модели — работаем как есть
root = conn = cmd = rs = None
names = []
try:
root = win32com.client.GetObject("LDAP://RootDSE")
base_dn = root.Get("defaultNamingContext")
conn = win32com.client.Dispatch("ADODB.Connection")
conn.Provider = "ADsDSOObject"
conn.Open("Active Directory Provider")
cmd = win32com.client.Dispatch("ADODB.Command")
cmd.ActiveConnection = conn
# Без Page Size AD вернёт максимум 1000 записей и молча обрежет остальные
cmd.Properties("Page Size").Value = 1000
cmd.Properties("Timeout").Value = 30
cmd.CommandText = (
f"<LDAP://{base_dn}>;"
# (!userAccountControl:...:=2) — отбрасываем отключённые учётки компьютеров
"(&(objectCategory=computer)(!userAccountControl:1.2.840.113556.1.4.803:=2));"
"dNSHostName,name;subtree"
)
rs = cmd.Execute()
if isinstance(rs, tuple): # позднее связывание отдаёт (recordset, records_affected)
rs = rs[0]
while not rs.EOF:
dns = rs.Fields.Item("dNSHostName").Value
name = rs.Fields.Item("name").Value
host = dns or name
if host:
names.append(str(host))
rs.MoveNext()
rs.Close()
finally:
if conn is not None:
try:
conn.Close()
except Exception:
pass
# Отпускаем COM-объекты ДО CoUninitialize: иначе их деструкторы сработают,
# когда COM в потоке уже деинициализирован, и посыплется
# "Win32 exception occurred releasing IUnknown".
rs = cmd = conn = root = None
if com_ready:
try:
pythoncom.CoUninitialize()
except Exception:
pass
return names
def current_domain():
"""NetBIOS-имя домена текущего пользователя (пустая строка, если не в домене)."""
return os.environ.get("USERDOMAIN", "")
# ------------------------------------------------------- Проверка доступности
def is_alive(host, port=445, timeout=0.4):
"""Быстрая проверка, что машина включена и отвечает по SMB."""
try:
with socket.create_connection((host, port), timeout=timeout):
return True
except OSError:
return False
def _parallel(func, items, workers, deadline, grace=DEFAULT_GRACE,
progress=None, stage=""):
"""Выполняем func по всем items в несколько потоков, не ожидая зависших.
Своя реализация вместо ThreadPoolExecutor по трём причинам:
* потоки демонические — зависший в нативном вызове WTSOpenServer поток
не мешает закрыть программу (ThreadPoolExecutor ждёт свои потоки на выходе);
* прервать зависший вызов нельзя, но можно перестать его ждать: как только
очередь машин разобрана, даём отставшим ровно `grace` секунд и уходим.
Именно это отличает 5 секунд от полутора минут — машины с закрытым RPC
висят по 20-45 с каждая, и ждать их бессмысленно;
* `deadline` остаётся страховкой на случай очень большого парка.
Возвращает (results, unfinished), где results — список (item, value, error).
"""
items = list(items)
total = len(items)
if not total:
return [], []
pending = deque(items)
pending_lock = threading.Lock()
results = []
results_lock = threading.Lock()
counter = [0]
in_flight = [0]
end_at = time.monotonic() + deadline
def worker():
while True:
if time.monotonic() >= end_at:
return
with pending_lock:
if not pending:
return
item = pending.popleft()
in_flight[0] += 1
try:
value, error = func(item), None
except Exception as exc:
value, error = None, f"{type(exc).__name__}: {exc}"
with results_lock:
results.append((item, value, error))
counter[0] += 1
done = counter[0]
with pending_lock:
in_flight[0] -= 1
# Прогресс обновляем пачками, чтобы не забивать очередь событий Tk
if progress and (done % 5 == 0 or done == total):
progress(stage, done, total)
threads = [threading.Thread(target=worker, daemon=True)
for _ in range(min(workers, total))]
for t in threads:
t.start()
drain_started = None
while True:
now = time.monotonic()
if now >= end_at:
break
with pending_lock:
queue_empty = not pending
running = in_flight[0]
if queue_empty:
if running == 0:
break # все машины честно опрошены
if drain_started is None:
drain_started = now
elif now - drain_started >= grace:
break # остальные зависли — дальше не ждём
time.sleep(0.05)
with results_lock:
handled = {item for item, _, _ in results}
snapshot = list(results)
unfinished = [i for i in items if i not in handled]
return snapshot, unfinished
def filter_alive(hosts, timeout=DEFAULT_PORT_TIMEOUT, workers=DEFAULT_WORKERS):
"""Оставляем только машины, ответившие на порт 445."""
results, _ = _parallel(lambda h: is_alive(h, timeout=timeout), hosts,
workers, DEFAULT_DEADLINE)
return [host for host, alive, err in results if alive and not err]
# ------------------------------------------------------------------ WTS
def _query_session(handle, session_id, info_class):
"""Строковое свойство сеанса; пустая строка, если недоступно."""
buffer = ctypes.c_void_p()
returned = wintypes.DWORD()
ok = _wts.WTSQuerySessionInformationW(
handle, session_id, info_class, ctypes.byref(buffer), ctypes.byref(returned))
if not ok or not buffer:
return ""
try:
return ctypes.cast(buffer, ctypes.c_wchar_p).value or ""
finally:
_wts.WTSFreeMemory(buffer)
def enum_sessions(machine=None):
"""Сеансы одной машины. machine=None — локальная.
Возвращает список словарей. Сеансы без пользователя (службы, listener) пропускаем.
"""
remote = bool(machine)
if remote:
handle = _wts.WTSOpenServerW(machine)
if not handle:
raise OSError(ctypes.WinError(ctypes.get_last_error()))
else:
handle = WTS_CURRENT_SERVER_HANDLE
info_ptr = ctypes.POINTER(_WTS_SESSION_INFOW)()
count = wintypes.DWORD()
result = []
try:
ok = _wts.WTSEnumerateSessionsW(
handle, 0, 1, ctypes.byref(info_ptr), ctypes.byref(count))
if not ok:
raise OSError(ctypes.WinError(ctypes.get_last_error()))
try:
for i in range(count.value):
entry = info_ptr[i]
session_id = entry.SessionId
user = _query_session(handle, session_id, _WTS_USER_NAME)
if not user:
continue
result.append({
"machine": machine or os.environ.get("COMPUTERNAME", ""),
"session_id": session_id,
"station": entry.pWinStationName or "",
"state": entry.State,
"state_name": STATE_NAMES.get(entry.State, str(entry.State)),
"user": user,
"domain": _query_session(handle, session_id, _WTS_DOMAIN_NAME),
"client": _query_session(handle, session_id, _WTS_CLIENT_NAME),
})
finally:
_wts.WTSFreeMemory(ctypes.cast(info_ptr, ctypes.c_void_p))
finally:
if remote:
_wts.WTSCloseServer(handle)
return result
_OFFLINE = object() # маркер: машина не ответила на порт 445
def scan_domain(computers=None, workers=DEFAULT_WORKERS, port_timeout=DEFAULT_PORT_TIMEOUT,
deadline=DEFAULT_DEADLINE, grace=DEFAULT_GRACE, progress=None):
"""Снимаем сеансы со всех машин домена одним проходом.
Проверка порта и опрос WTS выполняются в одной задаче: поток, освободившись,
сразу берёт следующую машину. Раньше это были две последовательные фазы,
и вторая простаивала, пока первая доделывала самую медленную машину.
Возвращает (sessions, errors, stats) — сеансы всех пользователей, без фильтра.
"""
t_start = time.monotonic()
if progress:
progress("Получаем список компьютеров из AD...", 0, 0)
t0 = time.monotonic()
hosts = list(computers) if computers else list_domain_computers()
ad_seconds = time.monotonic() - t0
stats = {"total": len(hosts), "alive": 0, "offline": 0, "failed": 0,
"unfinished": 0, "sessions_total": 0,
"ad_seconds": round(ad_seconds, 2), "scan_seconds": 0.0,
"total_seconds": 0.0}
if not hosts:
return [], [], stats
def probe(host):
# Быстрый отсев выключенных машин: без него WTSOpenServer будет ждать
# RPC-таймаут в десятки секунд на каждой недоступной машине.
if not is_alive(host, timeout=port_timeout):
return _OFFLINE
return enum_sessions(host)
t0 = time.monotonic()
results, unfinished = _parallel(probe, hosts, workers, deadline, grace,
progress, "Опрашиваем машины...")
scan_seconds = time.monotonic() - t0
sessions, errors = [], []
for host, value, error in results:
if error:
errors.append((host, error))
elif value is _OFFLINE:
stats["offline"] += 1
else:
stats["alive"] += 1
sessions.extend(value)
stats["failed"] = len(errors)
stats["unfinished"] = len(unfinished)
stats["sessions_total"] = len(sessions)
stats["scan_seconds"] = round(scan_seconds, 2)
stats["total_seconds"] = round(time.monotonic() - t_start, 2)
return sessions, errors, stats
def filter_sessions(sessions, user=None, domain=None, include_disconnected=True):
"""Отбираем из готового списка сеансы нужного пользователя.
Вынесено отдельно, чтобы поиск другого пользователя по уже собранным
данным происходил мгновенно, без повторного опроса домена.
"""
allowed = PROFILE_HELD_STATES if include_disconnected else (WTS_ACTIVE,)
out = [s for s in sessions if s["state"] in allowed]
if user:
target = user.strip().lower()
out = [s for s in out if s["user"].lower() == target]
if domain:
dom = domain.strip().lower()
# У локальных учёток в domain стоит имя машины — такие отсеиваем
out = [s for s in out if s["domain"].lower() == dom]
out.sort(key=lambda s: (s["machine"].lower(), s["session_id"]))
return out
def find_sessions(user=None, computers=None, domain=None, include_disconnected=True,
port_timeout=DEFAULT_PORT_TIMEOUT, workers=DEFAULT_WORKERS,
deadline=DEFAULT_DEADLINE, progress=None):
"""Опрашиваем домен и сразу отбираем сеансы одного пользователя."""
sessions, errors, stats = scan_domain(
computers=computers, workers=workers, port_timeout=port_timeout,
deadline=deadline, progress=progress)
matched = filter_sessions(sessions, user=user, domain=domain,
include_disconnected=include_disconnected)
stats["matched"] = len(matched)
return matched, errors, stats
def verify_sessions(sessions):
"""Перепроверяем перед записью, что сеансы ещё живы.
Данные скана могут быть слегка устаревшими — пользователь мог выйти.
Машин здесь единицы, так что проверка почти мгновенная.
"""
still_there, gone = [], []
checked = {}
for sess in sessions:
machine = sess["machine"]
if machine not in checked:
try:
checked[machine] = enum_sessions(machine)
except Exception:
checked[machine] = None # не смогли проверить — не мешаем записи
current = checked[machine]
if current is None:
still_there.append(sess)
continue
match = any(c["user"].lower() == sess["user"].lower()
and c["domain"].lower() == sess["domain"].lower()
and c["state"] in PROFILE_HELD_STATES
for c in current)
(still_there if match else gone).append(sess)
return still_there, gone
# -------------------------------------------------------------- Путь профиля
def _to_unc(machine, local_path):
"""C:\\Users\\Ivanov на машине PC1 -> \\\\PC1\\C$\\Users\\Ivanov"""
drive, rest = os.path.splitdrive(local_path)
if not drive:
return local_path
return f"\\\\{machine}\\{drive[0]}$" + rest
def resolve_profile_dir(machine, domain, user):
"""Каталог профиля пользователя на машине, в виде UNC-пути.
Основной способ — SID + реестр ProfileList: корректно разрешает случаи
вида Ivanov.CORP или Ivanov.000, которые не угадать по имени.
Если удалённый реестр недоступен (служба RemoteRegistry остановлена),
перебираем типовые варианты имени папки.
"""
account = f"{domain}\\{user}" if domain else user
sid_str = None
for lookup_host in (machine, None):
try:
sid_obj, _, _ = win32security.LookupAccountName(lookup_host, account)
sid_str = win32security.ConvertSidToStringSid(sid_obj)
break
except Exception:
continue
if sid_str:
try:
root = win32api.RegConnectRegistry(f"\\\\{machine}", win32con.HKEY_LOCAL_MACHINE)
key = win32api.RegOpenKeyEx(root, PROFILE_LIST_KEY + "\\" + sid_str,
0, win32con.KEY_READ)
path, _ = win32api.RegQueryValueEx(key, "ProfileImagePath")
win32api.RegCloseKey(key)
path = win32api.ExpandEnvironmentStrings(path)
if path:
return _to_unc(machine, path)
except Exception:
pass
# Фоллбэк: перебираем типовые имена папок профиля
candidates = [user]
if domain:
candidates.append(f"{user}.{domain}")
candidates.append(f"{user}.000")
for name in candidates:
unc = f"\\\\{machine}\\C$\\Users\\{name}"
try:
if os.path.isdir(unc):
return unc
except Exception:
continue
raise RuntimeError(
"не удалось определить папку профиля "
"(нет доступа к удалённому реестру и папка не найдена перебором)"
)
def ibases_path_for_session(session):
"""Путь к ibases.v8i внутри профиля пользователя из сеанса."""
profile = resolve_profile_dir(session["machine"], session["domain"], session["user"])
return os.path.join(profile, "AppData", "Roaming", "1C", "1CEStart", "ibases.v8i")
def short_name(machine):
"""SERVERTS.CORP.local -> SERVERTS (для компактного отображения в списке)."""
return machine.split(".")[0] if machine else machine
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
-289
View File
@@ -1,289 +0,0 @@
('C:\\Project\\build\\Main_v3\\PYZ-00.pyz',
[('_compat_pickle',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\_compat_pickle.py',
'PYMODULE'),
('_compression',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\_compression.py',
'PYMODULE'),
('_py_abc',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\_py_abc.py',
'PYMODULE'),
('_pydecimal',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\_pydecimal.py',
'PYMODULE'),
('_strptime',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\_strptime.py',
'PYMODULE'),
('_threading_local',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\_threading_local.py',
'PYMODULE'),
('argparse',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\argparse.py',
'PYMODULE'),
('ast',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\ast.py',
'PYMODULE'),
('base64',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\base64.py',
'PYMODULE'),
('bisect',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\bisect.py',
'PYMODULE'),
('bz2',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\bz2.py',
'PYMODULE'),
('calendar',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\calendar.py',
'PYMODULE'),
('contextlib',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\contextlib.py',
'PYMODULE'),
('contextvars',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\contextvars.py',
'PYMODULE'),
('copy',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\copy.py',
'PYMODULE'),
('csv',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\csv.py',
'PYMODULE'),
('dataclasses',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\dataclasses.py',
'PYMODULE'),
('datetime',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\datetime.py',
'PYMODULE'),
('decimal',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\decimal.py',
'PYMODULE'),
('dis',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\dis.py',
'PYMODULE'),
('email',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\__init__.py',
'PYMODULE'),
('email._encoded_words',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\_encoded_words.py',
'PYMODULE'),
('email._header_value_parser',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\_header_value_parser.py',
'PYMODULE'),
('email._parseaddr',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\_parseaddr.py',
'PYMODULE'),
('email._policybase',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\_policybase.py',
'PYMODULE'),
('email.base64mime',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\base64mime.py',
'PYMODULE'),
('email.charset',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\charset.py',
'PYMODULE'),
('email.contentmanager',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\contentmanager.py',
'PYMODULE'),
('email.encoders',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\encoders.py',
'PYMODULE'),
('email.errors',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\errors.py',
'PYMODULE'),
('email.feedparser',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\feedparser.py',
'PYMODULE'),
('email.generator',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\generator.py',
'PYMODULE'),
('email.header',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\header.py',
'PYMODULE'),
('email.headerregistry',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\headerregistry.py',
'PYMODULE'),
('email.iterators',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\iterators.py',
'PYMODULE'),
('email.message',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\message.py',
'PYMODULE'),
('email.parser',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\parser.py',
'PYMODULE'),
('email.policy',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\policy.py',
'PYMODULE'),
('email.quoprimime',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\quoprimime.py',
'PYMODULE'),
('email.utils',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\email\\utils.py',
'PYMODULE'),
('fnmatch',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\fnmatch.py',
'PYMODULE'),
('fractions',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\fractions.py',
'PYMODULE'),
('getopt',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\getopt.py',
'PYMODULE'),
('gettext',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\gettext.py',
'PYMODULE'),
('gzip',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\gzip.py',
'PYMODULE'),
('hashlib',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\hashlib.py',
'PYMODULE'),
('importlib',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\__init__.py',
'PYMODULE'),
('importlib._abc',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\_abc.py',
'PYMODULE'),
('importlib._bootstrap',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\_bootstrap.py',
'PYMODULE'),
('importlib._bootstrap_external',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\_bootstrap_external.py',
'PYMODULE'),
('importlib.abc',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\abc.py',
'PYMODULE'),
('importlib.machinery',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\machinery.py',
'PYMODULE'),
('importlib.metadata',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\metadata\\__init__.py',
'PYMODULE'),
('importlib.metadata._adapters',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\metadata\\_adapters.py',
'PYMODULE'),
('importlib.metadata._collections',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\metadata\\_collections.py',
'PYMODULE'),
('importlib.metadata._functools',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\metadata\\_functools.py',
'PYMODULE'),
('importlib.metadata._itertools',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\metadata\\_itertools.py',
'PYMODULE'),
('importlib.metadata._meta',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\metadata\\_meta.py',
'PYMODULE'),
('importlib.metadata._text',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\metadata\\_text.py',
'PYMODULE'),
('importlib.readers',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\readers.py',
'PYMODULE'),
('importlib.util',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\importlib\\util.py',
'PYMODULE'),
('inspect',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\inspect.py',
'PYMODULE'),
('logging',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\logging\\__init__.py',
'PYMODULE'),
('lzma',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\lzma.py',
'PYMODULE'),
('numbers',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\numbers.py',
'PYMODULE'),
('opcode',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\opcode.py',
'PYMODULE'),
('optparse',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\optparse.py',
'PYMODULE'),
('pathlib',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\pathlib.py',
'PYMODULE'),
('pickle',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\pickle.py',
'PYMODULE'),
('pprint',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\pprint.py',
'PYMODULE'),
('py_compile',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\py_compile.py',
'PYMODULE'),
('quopri',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\quopri.py',
'PYMODULE'),
('random',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\random.py',
'PYMODULE'),
('selectors',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\selectors.py',
'PYMODULE'),
('shutil',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\shutil.py',
'PYMODULE'),
('signal',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\signal.py',
'PYMODULE'),
('socket',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\socket.py',
'PYMODULE'),
('statistics',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\statistics.py',
'PYMODULE'),
('string',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\string.py',
'PYMODULE'),
('stringprep',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\stringprep.py',
'PYMODULE'),
('subprocess',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\subprocess.py',
'PYMODULE'),
('tarfile',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\tarfile.py',
'PYMODULE'),
('textwrap',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\textwrap.py',
'PYMODULE'),
('threading',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\threading.py',
'PYMODULE'),
('tkinter',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\tkinter\\__init__.py',
'PYMODULE'),
('tkinter.commondialog',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\tkinter\\commondialog.py',
'PYMODULE'),
('tkinter.constants',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\tkinter\\constants.py',
'PYMODULE'),
('tkinter.messagebox',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\tkinter\\messagebox.py',
'PYMODULE'),
('token',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\token.py',
'PYMODULE'),
('tokenize',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\tokenize.py',
'PYMODULE'),
('tracemalloc',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\tracemalloc.py',
'PYMODULE'),
('typing',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\typing.py',
'PYMODULE'),
('urllib',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\urllib\\__init__.py',
'PYMODULE'),
('urllib.parse',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\urllib\\parse.py',
'PYMODULE'),
('uu',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\uu.py',
'PYMODULE'),
('zipfile',
'C:\\Users\\Saturn\\AppData\\Local\\Programs\\Python\\Python310\\lib\\zipfile.py',
'PYMODULE')])
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-27
View File
@@ -1,27 +0,0 @@
This file lists modules PyInstaller was not able to find. This does not
necessarily mean this module is required for running your program. Python and
Python 3rd-party packages include a lot of conditional or optional modules. For
example the module 'ntpath' only exists on Windows, whereas the module
'posixpath' only exists on Posix systems.
Types if import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!
missing module named pep517 - imported by importlib.metadata (delayed)
missing module named 'org.python' - imported by copy (optional)
missing module named org - imported by pickle (optional)
missing module named pwd - imported by posixpath (delayed, conditional), subprocess (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional)
missing module named grp - imported by subprocess (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib (delayed, optional)
missing module named posix - imported by os (conditional, optional), shutil (conditional), importlib._bootstrap_external (conditional)
missing module named resource - imported by posix (top-level)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional)
missing module named _posixsubprocess - imported by subprocess (optional)
missing module named fcntl - imported by subprocess (optional)
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

+482
View File
@@ -0,0 +1,482 @@
<#
Пробник для v2.0 "Добавление баз 1С" — проверка обнаружения сеансов по домену.
Что проверяет:
1. Окружение (домен, права, версия ОС/PS).
2. Получение списка компьютеров из AD (через [adsisearcher], RSAT не нужен).
3. Быстрый отсев мёртвых машин по порту 445 (асинхронно, как это делает Dameware).
4. Перечисление сеансов через WTS API (P/Invoke, без парсинга quser — не зависит от локали).
5. Разрешение реальной папки профиля пользователя (SID -> удалённый реестр ProfileList).
Запуск (на терминальном сервере, под доменным админом):
powershell -ExecutionPolicy Bypass -File .\probe_sessions.ps1 -User Saturn | Tee-Object probe_report.txt
Быстрый прогон только по двум машинам, без опроса всего домена:
powershell -ExecutionPolicy Bypass -File .\probe_sessions.ps1 -User Saturn -Computers "SERVERTERM,SERVERSQL"
Скрипт только читает данные: ничего не пишет на другие машины и не меняет настройки.
#>
[CmdletBinding()]
param(
# Имя пользователя, чьи сеансы ищем (без домена).
[string]$User = "Saturn",
# Явный список машин через запятую. Если задан — AD не опрашивается.
[string]$Computers = "",
# Ограничить число машин из AD (0 = без ограничения). Полезно для первого прогона.
[int]$Limit = 0,
# Таймаут проверки порта 445, мс. Меньше = быстрее, но можно потерять медленные машины.
[int]$PortTimeoutMs = 400,
# Число параллельных потоков опроса WTS.
[int]$Threads = 64,
# Общий таймаут этапа опроса сеансов, секунд.
[int]$WtsTimeoutSec = 90
)
$ErrorActionPreference = "Stop"
$swTotal = [Diagnostics.Stopwatch]::StartNew()
function Section($title) {
Write-Output ""
Write-Output ("=" * 78)
Write-Output " $title"
Write-Output ("=" * 78)
}
function TableOut($objects, $props) {
if (-not $objects -or $objects.Count -eq 0) { Write-Output " (пусто)"; return }
$t = $objects | Format-Table -Property $props -AutoSize | Out-String
Write-Output $t.TrimEnd()
}
# ---------------------------------------------------------------- ШАГ 0: окружение
Section "ШАГ 0. Окружение"
$os = Get-CimInstance Win32_OperatingSystem
$cs = Get-CimInstance Win32_ComputerSystem
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($id)
$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Write-Output " Компьютер : $($env:COMPUTERNAME)"
Write-Output " ОС : $($os.Caption) ($($os.Version))"
Write-Output " PowerShell : $($PSVersionTable.PSVersion)"
Write-Output " Домен машины : $($cs.Domain) (в домене: $($cs.PartOfDomain))"
Write-Output " Запущен от : $($id.Name)"
Write-Output " Права локал. админа: $isAdmin"
Write-Output " Ищем сеансы юзера : '$User'"
if (-not $cs.PartOfDomain) {
Write-Output ""
Write-Output " !! Машина не в домене — поиск по AD работать не будет."
}
if (-not $isAdmin) {
Write-Output ""
Write-Output " !! Нет прав локального администратора. Опрос WTS на удалённых машинах"
Write-Output " почти наверняка вернёт 'Отказано в доступе'. Запусти из-под доменного админа."
}
# ------------------------------------------------- Компиляция WTS-обёртки (P/Invoke)
$cs_code = @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
public class WtsProbe
{
[DllImport("wtsapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr WTSOpenServerW(string pServerName);
[DllImport("wtsapi32.dll")]
private static extern void WTSCloseServer(IntPtr hServer);
[DllImport("wtsapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern int WTSEnumerateSessionsW(IntPtr hServer, int Reserved, int Version,
ref IntPtr ppSessionInfo, ref int pCount);
[DllImport("wtsapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern int WTSQuerySessionInformationW(IntPtr hServer, int sessionId, int wtsInfoClass,
out IntPtr ppBuffer, out int pBytesReturned);
[DllImport("wtsapi32.dll")]
private static extern void WTSFreeMemory(IntPtr pMemory);
[StructLayout(LayoutKind.Sequential)]
private struct WTS_SESSION_INFO
{
public int SessionId;
[MarshalAs(UnmanagedType.LPWStr)] public string pWinStationName;
public int State;
}
private const int WTSUserName = 5;
private const int WTSDomainName = 7;
private const int WTSClientName = 10;
private static string Query(IntPtr h, int sid, int infoClass)
{
IntPtr buf = IntPtr.Zero;
int bytes = 0;
if (WTSQuerySessionInformationW(h, sid, infoClass, out buf, out bytes) == 0) return "";
try { string s = Marshal.PtrToStringUni(buf); return s == null ? "" : s; }
finally { if (buf != IntPtr.Zero) WTSFreeMemory(buf); }
}
// Возвращает строки "SessionId|Station|State|User|Domain|Client".
// server = null/"" -> локальная машина.
public static string[] Enumerate(string server)
{
bool remote = !string.IsNullOrEmpty(server);
IntPtr h = IntPtr.Zero; // WTS_CURRENT_SERVER_HANDLE
if (remote)
{
h = WTSOpenServerW(server);
if (h == IntPtr.Zero)
throw new Exception("WTSOpenServer: win32 error " + Marshal.GetLastWin32Error());
}
IntPtr pInfo = IntPtr.Zero;
int count = 0;
List<string> res = new List<string>();
try
{
if (WTSEnumerateSessionsW(h, 0, 1, ref pInfo, ref count) == 0)
throw new Exception("WTSEnumerateSessions: win32 error " + Marshal.GetLastWin32Error());
int sz = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
for (int i = 0; i < count; i++)
{
IntPtr cur = new IntPtr(pInfo.ToInt64() + (long)i * sz);
WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure(cur, typeof(WTS_SESSION_INFO));
string user = Query(h, si.SessionId, WTSUserName);
if (string.IsNullOrEmpty(user)) continue; // служебные сеансы пропускаем
res.Add(si.SessionId + "|" + si.pWinStationName + "|" + si.State + "|" +
user + "|" + Query(h, si.SessionId, WTSDomainName) + "|" +
Query(h, si.SessionId, WTSClientName));
}
}
finally
{
if (pInfo != IntPtr.Zero) WTSFreeMemory(pInfo);
if (remote && h != IntPtr.Zero) WTSCloseServer(h);
}
return res.ToArray();
}
}
'@
# Компилируем в DLL: так её можно быстро загрузить в каждый параллельный runspace
# (повторный Add-Type в каждом потоке стоил бы 1-2 секунды на компиляцию).
$dllPath = Join-Path $env:TEMP ("WtsProbe_{0}.dll" -f $PID)
try {
Add-Type -TypeDefinition $cs_code -OutputAssembly $dllPath -OutputType Library
Add-Type -Path $dllPath
} catch {
Write-Output ""
Write-Output " !! Не удалось скомпилировать WTS-обёртку: $($_.Exception.Message)"
Write-Output " Проверь, что доступен .NET-компилятор (csc) и %TEMP% доступен на запись."
exit 1
}
$STATES = @{
0 = "Active"; 1 = "Connected"; 2 = "ConnectQuery"; 3 = "Shadow"; 4 = "Disconnected";
5 = "Idle"; 6 = "Listen"; 7 = "Reset"; 8 = "Down"; 9 = "Init"
}
# --------------------------------------------------------- ШАГ 1: список компьютеров
Section "ШАГ 1. Список компьютеров"
$machines = @()
$adInfo = @{}
if ($Computers.Trim()) {
$machines = $Computers.Split(",") | ForEach-Object { $_.Trim() } | Where-Object { $_ }
Write-Output " Источник: задан вручную (-Computers)"
Write-Output " Машин: $($machines.Count)"
} else {
$sw = [Diagnostics.Stopwatch]::StartNew()
try {
# (!userAccountControl:...:=2) — отсекаем отключённые учётки компьютеров
$searcher = [adsisearcher]"(&(objectCategory=computer)(!userAccountControl:1.2.840.113556.1.4.803:=2))"
$searcher.PageSize = 1000
[void]$searcher.PropertiesToLoad.AddRange(@('name', 'dnshostname', 'operatingsystem', 'lastlogontimestamp'))
$found = $searcher.FindAll()
foreach ($r in $found) {
$p = $r.Properties
$nm = if ($p.dnshostname) { [string]$p.dnshostname[0] } else { [string]$p.name[0] }
if (-not $nm) { continue }
$llt = $null
if ($p.lastlogontimestamp) {
try { $llt = [DateTime]::FromFileTimeUtc([int64]$p.lastlogontimestamp[0]).ToLocalTime() } catch {}
}
$machines += $nm
$adInfo[$nm] = [pscustomobject]@{
OS = if ($p.operatingsystem) { [string]$p.operatingsystem[0] } else { "" }
LastLogon = $llt
}
}
$found.Dispose()
$sw.Stop()
Write-Output " Источник: Active Directory (LDAP)"
Write-Output " Найдено компьютеров: $($machines.Count) (за $([math]::Round($sw.Elapsed.TotalSeconds,2)) c)"
$stale = @($adInfo.Values | Where-Object { $_.LastLogon -and $_.LastLogon -lt (Get-Date).AddDays(-30) }).Count
Write-Output " Из них не логинились >30 дней: $stale (кандидаты на отсев)"
} catch {
Write-Output " !! Ошибка запроса к AD: $($_.Exception.Message)"
exit 1
}
if ($Limit -gt 0 -and $machines.Count -gt $Limit) {
$machines = $machines[0..($Limit - 1)]
Write-Output " Ограничено параметром -Limit: $($machines.Count)"
}
}
if ($machines.Count -eq 0) { Write-Output " Нет машин для опроса."; exit 1 }
# ------------------------------------------------- ШАГ 2: отсев мёртвых машин (порт 445)
Section "ШАГ 2. Проверка живости (TCP 445, асинхронно)"
$sw = [Diagnostics.Stopwatch]::StartNew()
$pending = @()
foreach ($m in $machines) {
$client = New-Object Net.Sockets.TcpClient
$ar = $null
try { $ar = $client.BeginConnect($m, 445, $null, $null) } catch { }
$pending += [pscustomobject]@{ Machine = $m; Client = $client; Async = $ar }
}
Start-Sleep -Milliseconds $PortTimeoutMs
$alive = @()
foreach ($p in $pending) {
$ok = $false
try { if ($p.Client.Connected) { $ok = $true } } catch { }
if ($ok) { $alive += $p.Machine }
try { $p.Client.Close() } catch { }
}
$sw.Stop()
Write-Output " Живых машин: $($alive.Count) из $($machines.Count) (за $([math]::Round($sw.Elapsed.TotalSeconds,2)) c)"
Write-Output " Недоступны : $($machines.Count - $alive.Count)"
if ($alive.Count -eq 0) {
Write-Output " !! Ни одна машина не ответила. Проверь таймаут (-PortTimeoutMs) и сетевую доступность."
exit 1
}
# ------------------------------------------------------ ШАГ 3: опрос сеансов через WTS
Section "ШАГ 3. Опрос сеансов (WTS API, параллельно)"
$sw = [Diagnostics.Stopwatch]::StartNew()
$pool = [runspacefactory]::CreateRunspacePool(1, [Math]::Max(1, $Threads))
$pool.Open()
$worker = {
param($dll, $machine)
try {
[void][Reflection.Assembly]::LoadFrom($dll)
$rows = [WtsProbe]::Enumerate($machine)
[pscustomobject]@{ Machine = $machine; Ok = $true; Rows = $rows; Err = $null }
} catch {
[pscustomobject]@{ Machine = $machine; Ok = $false; Rows = @(); Err = $_.Exception.Message }
}
}
$jobs = @()
foreach ($m in $alive) {
$ps = [powershell]::Create()
$ps.RunspacePool = $pool
[void]$ps.AddScript($worker).AddArgument($dllPath).AddArgument($m)
$jobs += [pscustomobject]@{ Machine = $m; PS = $ps; Handle = $ps.BeginInvoke() }
}
$deadline = (Get-Date).AddSeconds($WtsTimeoutSec)
while (@($jobs | Where-Object { -not $_.Handle.IsCompleted }).Count -gt 0 -and (Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 100
}
$sessions = @()
$failures = @()
$timedOut = @()
foreach ($j in $jobs) {
if (-not $j.Handle.IsCompleted) {
$timedOut += $j.Machine
try { $j.PS.Stop() } catch { }
try { $j.PS.Dispose() } catch { }
continue
}
try {
$res = $j.PS.EndInvoke($j.Handle)
foreach ($r in $res) {
if (-not $r.Ok) { $failures += [pscustomobject]@{ Machine = $r.Machine; Err = $r.Err }; continue }
foreach ($row in $r.Rows) {
$f = $row -split "\|"
$sessions += [pscustomobject]@{
Machine = $r.Machine
SessionId = [int]$f[0]
Station = $f[1]
State = $STATES[[int]$f[2]]
User = $f[3]
Domain = $f[4]
Client = $f[5]
}
}
}
} catch {
$failures += [pscustomobject]@{ Machine = $j.Machine; Err = $_.Exception.Message }
}
try { $j.PS.Dispose() } catch { }
}
$pool.Close()
$pool.Dispose()
$sw.Stop()
$wtsSec = [math]::Round($sw.Elapsed.TotalSeconds, 2)
Write-Output " Опрошено машин : $($alive.Count) (за $wtsSec c, потоков: $Threads)"
Write-Output " Успешно : $($alive.Count - $failures.Count - $timedOut.Count)"
Write-Output " С ошибкой : $($failures.Count)"
Write-Output " Не уложились в таймаут: $($timedOut.Count)"
Write-Output " Всего сеансов с пользователями: $($sessions.Count)"
if ($failures.Count -gt 0) {
Write-Output ""
Write-Output " Ошибки, сгруппированные по причине:"
$failures | Group-Object Err | Sort-Object Count -Descending | ForEach-Object {
$sample = ($_.Group | Select-Object -First 3 | ForEach-Object { $_.Machine }) -join ", "
Write-Output (" {0,4} шт. {1}" -f $_.Count, $_.Name)
Write-Output (" напр.: {0}" -f $sample)
}
}
if ($timedOut.Count -gt 0) {
Write-Output ""
Write-Output (" Зависли: " + (($timedOut | Select-Object -First 10) -join ", "))
}
# ------------------------------------------------------------ ШАГ 4: результат по юзеру
Section "ШАГ 4. Сеансы пользователя '$User'"
$mine = @($sessions | Where-Object { $_.User -like $User })
if ($mine.Count -eq 0) {
Write-Output " Сеансов не найдено."
Write-Output " (Проверь имя. Ниже — сводка всех найденных сеансов.)"
} else {
TableOut $mine @("Machine", "SessionId", "Station", "State", "Domain", "User", "Client")
}
Section "Все найденные сеансы (топ-40)"
TableOut @($sessions | Sort-Object Machine, SessionId | Select-Object -First 40) `
@("Machine", "SessionId", "Station", "State", "Domain", "User", "Client")
# ------------------------------------------- ШАГ 5: разрешение папки профиля
Section "ШАГ 5. Разрешение папки профиля (SID -> реестр ProfileList)"
if ($mine.Count -eq 0) {
Write-Output " Пропущено: не найдено сеансов искомого пользователя."
} else {
foreach ($s in ($mine | Sort-Object Machine -Unique)) {
Write-Output ""
Write-Output " --- $($s.Machine) ---"
# 1) имя -> SID
$sid = $null
try {
$acct = New-Object Security.Principal.NTAccount($s.Domain, $s.User)
$sid = $acct.Translate([Security.Principal.SecurityIdentifier]).Value
Write-Output " SID : $sid"
} catch {
Write-Output " SID : ОШИБКА — $($_.Exception.Message)"
}
# 2) SID -> ProfileImagePath (удалённый реестр; нужна служба RemoteRegistry)
$profilePath = $null
if ($sid) {
try {
$rk = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $s.Machine)
$sub = $rk.OpenSubKey("SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid")
if ($sub) {
$profilePath = $sub.GetValue("ProfileImagePath")
Write-Output " ProfileImagePath : $profilePath"
$sub.Close()
} else {
Write-Output " ProfileImagePath : ключ для SID не найден (профиль ещё не создан?)"
}
$rk.Close()
} catch {
Write-Output " ProfileImagePath : ОШИБКА — $($_.Exception.Message)"
Write-Output " (вероятно, служба RemoteRegistry остановлена)"
}
}
# 3) Фоллбэк: угадываем папку перебором и проверяем доступ по административной шаре
if (-not $profilePath) {
Write-Output " Фоллбэк (перебор папок):"
$shortDom = $s.Domain
foreach ($cand in @($s.User, "$($s.User).$shortDom", "$($s.User).000")) {
$p = "\\$($s.Machine)\C`$\Users\$cand"
$exists = $false
try { $exists = Test-Path -LiteralPath $p } catch { }
Write-Output (" {0,-45} {1}" -f $p, $(if ($exists) { "НАЙДЕНА" } else { "нет" }))
if ($exists -and -not $profilePath) { $profilePath = "C:\Users\$cand" }
}
}
# 4) Проверяем доступность самого ibases.v8i по админской шаре
if ($profilePath) {
$unc = "\\$($s.Machine)\" + ($profilePath -replace '^([A-Za-z]):', '$1$')
$v8i = Join-Path $unc "AppData\Roaming\1C\1CEStart\ibases.v8i"
Write-Output " UNC до профиля : $unc"
try {
if (Test-Path -LiteralPath $v8i) {
$fi = Get-Item -LiteralPath $v8i
$cnt = @(Select-String -LiteralPath $v8i -Pattern '^\[' -ErrorAction SilentlyContinue).Count
Write-Output " ibases.v8i : ЕСТЬ, $($fi.Length) байт, баз в файле: $cnt"
Write-Output " Запись в профиль : доступна (файл читается)"
} else {
$dir = Split-Path $v8i -Parent
$dirOk = Test-Path -LiteralPath $dir
Write-Output " ibases.v8i : файла нет (каталог 1CEStart существует: $dirOk)"
}
} catch {
Write-Output " ibases.v8i : ОШИБКА доступа — $($_.Exception.Message)"
}
}
}
}
# ------------------------------------------------------------------ Итоги
Section "ИТОГИ"
$swTotal.Stop()
Write-Output " Машин из источника : $($machines.Count)"
Write-Output " Живых (порт 445) : $($alive.Count)"
Write-Output " Сеансов найдено : $($sessions.Count)"
Write-Output " Сеансов юзера '$User' : $($mine.Count)"
Write-Output " Время опроса сеансов : $wtsSec c"
Write-Output " Общее время работы : $([math]::Round($swTotal.Elapsed.TotalSeconds,2)) c"
Write-Output ""
Write-Output " Уникальных машин с сеансами:"
$byMachine = $sessions | Group-Object Machine | Sort-Object Count -Descending | Select-Object -First 10
foreach ($g in $byMachine) { Write-Output (" {0,-30} сеансов: {1}" -f $g.Name, $g.Count) }
try { Remove-Item -LiteralPath $dllPath -Force -ErrorAction SilentlyContinue } catch { }
Write-Output ""
Write-Output "Готово."