diff --git a/probe_sessions.ps1 b/probe_sessions.ps1 new file mode 100644 index 0000000..b71ec8c --- /dev/null +++ b/probe_sessions.ps1 @@ -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 res = new List(); + 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 "Готово."