Compare commits
4
Commits
main
..
5027a69f56
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5027a69f56 | ||
|
|
6be4e229d2 | ||
|
|
ca5fc684ae | ||
|
|
0aa3007c5e |
@@ -1,3 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# -*- mode: python ; coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
a = Analysis(
|
|
||||||
['Main_v3.py'],
|
|
||||||
pathex=[],
|
|
||||||
binaries=[],
|
|
||||||
datas=[],
|
|
||||||
# ad_sessions импортируется внутри try/except, а pywin32 подтягивает часть
|
|
||||||
# модулей динамически — перечисляем явно, иначе их не будет в сборке
|
|
||||||
hiddenimports=[
|
|
||||||
'ad_sessions',
|
|
||||||
'win32timezone',
|
|
||||||
'win32com.client',
|
|
||||||
'win32security',
|
|
||||||
],
|
|
||||||
hookspath=[],
|
|
||||||
hooksconfig={},
|
|
||||||
runtime_hooks=[],
|
|
||||||
excludes=[],
|
|
||||||
noarchive=False,
|
|
||||||
optimize=0,
|
|
||||||
)
|
|
||||||
pyz = PYZ(a.pure)
|
|
||||||
|
|
||||||
exe = EXE(
|
|
||||||
pyz,
|
|
||||||
a.scripts,
|
|
||||||
a.binaries,
|
|
||||||
a.datas,
|
|
||||||
[],
|
|
||||||
name='1C_Base_Adder',
|
|
||||||
debug=False,
|
|
||||||
bootloader_ignore_signals=False,
|
|
||||||
strip=False,
|
|
||||||
upx=True,
|
|
||||||
upx_exclude=[],
|
|
||||||
runtime_tmpdir=None,
|
|
||||||
console=False,
|
|
||||||
disable_windowed_traceback=False,
|
|
||||||
argv_emulation=False,
|
|
||||||
target_arch=None,
|
|
||||||
codesign_identity=None,
|
|
||||||
entitlements_file=None,
|
|
||||||
icon=['download.ico'],
|
|
||||||
)
|
|
||||||
+164
-822
File diff suppressed because it is too large
Load Diff
-531
@@ -1,531 +0,0 @@
|
|||||||
# -*- 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
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 264 KiB |
@@ -1,482 +0,0 @@
|
|||||||
<#
|
|
||||||
Пробник для 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 "Готово."
|
|
||||||
Reference in New Issue
Block a user