From 0019f2b1065cd98f9eb12e1385ad7edd68747c24 Mon Sep 17 00:00:00 2001 From: ened Date: Mon, 6 Oct 2025 09:37:03 +0900 Subject: [PATCH] Update script for file handles --- Find-FileHandles.ps1 | 327 +++++++++++++++++++++++++++++++++---------- 1 file changed, 252 insertions(+), 75 deletions(-) diff --git a/Find-FileHandles.ps1 b/Find-FileHandles.ps1 index 4cdefe8..300c957 100644 --- a/Find-FileHandles.ps1 +++ b/Find-FileHandles.ps1 @@ -1,122 +1,299 @@ <# .SYNOPSIS - Specifies a directory and lists all processes that have open file handles within that directory and its - subdirectories. + Find processes that have open file handles to a specific file or directory. .DESCRIPTION - This script utilizes the handle.exe tool from Microsoft's Sysinternals suite to find open file handles. - It filters the results to show only file handles within the specified target directory path. - The script requires handle.exe to be downloaded and its path correctly configured in the $handleExePath - variable. - For best results, run this script with Administrator privileges. + Uses Sysinternals handle.exe (NtHandle v5.0) to find all processes holding handles + to the specified file or directory path. Supports detailed output and process information. -.PARAMETER TargetDirectory - The path to the directory to scan for open file handles. Defaults to the Windows directory (C:\Windows). +.PARAMETER Path + The file or directory path to search for. If not specified, prompts for input. + +.PARAMETER HandleExePath + Path to handle.exe. If not specified, auto-detects from common locations. + +.PARAMETER ShowDetails + Show detailed handle information including handle ID, type, and user. + +.PARAMETER OutputFile + Save results to the specified file (e.g., handle.out). .EXAMPLE - .\Find-FileHandles.ps1 - (Scans the default C:\Windows directory) + .\Find-FileHandles.ps1 -Path "D:\Project\video-av1\vav2\docs" .EXAMPLE - .\Find-FileHandles.ps1 -TargetDirectory "D:\Project\video-av1" - (Scans the D:\Project\video-av1 directory) + .\Find-FileHandles.ps1 -Path "D:\Project\video-av1\sample\test.webm" -ShowDetails + +.EXAMPLE + .\Find-FileHandles.ps1 -Path "D:\Project" -OutputFile "handle.out" + +.EXAMPLE + .\Find-FileHandles.ps1 -Path "D:\Project" -ShowDetails -OutputFile "handle.out" #> param ( - [string]$TargetDirectory = $env:SystemRoot # Defaults to C:\Windows + [Parameter(Position=0)] + [string]$Path, + + [Parameter(Mandatory=$false)] + [string]$HandleExePath, + + [Parameter(Mandatory=$false)] + [switch]$ShowDetails, + + [Parameter(Mandatory=$false)] + [string]$OutputFile ) # 콘솔 인코딩 설정 (한글 깨짐 방지) [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8 -# --- CONFIGURATION --- -# handle.exe 경로 자동 탐지 -$possiblePaths = @( - "C:\Sysinternals\handle.exe", - "$env:ProgramFiles\Sysinternals\handle.exe", - "$env:USERPROFILE\Downloads\handle.exe", - "$env:TEMP\handle.exe", - "handle.exe" # PATH에 있는 경우 -) - -$handleExePath = $null -foreach ($path in $possiblePaths) { - if (Test-Path $path) { - $handleExePath = $path - break +# Path 파라미터가 없으면 사용자에게 입력 받기 +if (-not $Path) { + $Path = Read-Host "Enter the file or directory path to search for handles" + if (-not $Path) { + Write-Host "[ERROR] No path specified." -ForegroundColor Red + exit 1 } } -# PATH에서 handle.exe 찾기 -if (-not $handleExePath) { - try { - $handleExePath = (Get-Command "handle.exe" -ErrorAction Stop).Source - } catch { - $handleExePath = $null +# 경로 검증 및 정규화 +if (Test-Path $Path) { + $TargetPath = (Resolve-Path $Path).Path +} else { + Write-Host "[WARNING] Path does not exist: $Path" -ForegroundColor Yellow + Write-Host "Searching anyway (path might be locked or inaccessible)..." -ForegroundColor Gray + $TargetPath = $Path +} + +# --- CONFIGURATION --- +# handle.exe 경로 자동 탐지 +if (-not $HandleExePath) { + $possiblePaths = @( + "C:\Sysinternals\handle.exe", + "$env:ProgramFiles\Sysinternals\handle.exe", + "$env:USERPROFILE\Downloads\handle.exe", + "$env:TEMP\handle.exe", + "handle.exe" # PATH에 있는 경우 + ) + + foreach ($path in $possiblePaths) { + if (Test-Path $path) { + $HandleExePath = $path + break + } + } + + # PATH에서 handle.exe 찾기 + if (-not $HandleExePath) { + try { + $HandleExePath = (Get-Command "handle.exe" -ErrorAction Stop).Source + } catch { + $HandleExePath = $null + } } } # --------------------- # handle.exe 파일 존재 여부 확인 -if (-not $handleExePath -or -not (Test-Path $handleExePath)) { - Write-Host "[ERROR] handle.exe not found in any of the following locations:" -ForegroundColor Red - $possiblePaths | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow } +if (-not $HandleExePath -or -not (Test-Path $HandleExePath)) { + Write-Host "[ERROR] handle.exe not found!" -ForegroundColor Red Write-Host "" Write-Host "Please download handle.exe from:" -ForegroundColor Cyan Write-Host "https://learn.microsoft.com/en-us/sysinternals/downloads/handle" -ForegroundColor Cyan Write-Host "" - Write-Host "And place it in one of the above locations, or add it to your PATH." -ForegroundColor Green - return + Write-Host "And place it in one of these locations:" -ForegroundColor Green + @( + "C:\Sysinternals\handle.exe", + "$env:ProgramFiles\Sysinternals\handle.exe", + "Or add it to your PATH" + ) | ForEach-Object { Write-Host " - $_" -ForegroundColor Yellow } + exit 1 } -Write-Host "Scanning for open file handles in '$TargetDirectory'..." -Write-Host "This may take a moment..." +# 타이틀 박스 동적 생성 +$title = "Scanning for handles to: $TargetPath" +$boxWidth = [Math]::Max($title.Length + 4, 60) +$padding = $boxWidth - $title.Length - 2 + +Write-Host "╔$('═' * $boxWidth)╗" -ForegroundColor Cyan +Write-Host "║ $title$(' ' * $padding)║" -ForegroundColor Cyan +Write-Host "╚$('═' * $boxWidth)╝" -ForegroundColor Cyan +Write-Host "" # Execute handle.exe to get all open file handle information +$handleOutput = "" +$outputLines = @() + try { - Write-Host "Executing handle.exe..." -ForegroundColor Gray - $handleOutput = & $handleExePath -nobanner -a -u "$TargetDirectory" 2>$null | Out-String + Write-Host "[*] Executing handle.exe (this may take 10-30 seconds)..." -ForegroundColor Gray + + # 진행 상황 표시를 위한 타이머 + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + + # handle.exe를 비동기로 실행하고 진행 표시 + $job = Start-Job -ScriptBlock { + param($exePath, $targetPath) + & $exePath -accepteula -nobanner "$targetPath" 2>&1 + } -ArgumentList $HandleExePath, $TargetPath + + # 진행 표시 + $spinnerChars = @('|', '/', '-', '\') + $spinnerIndex = 0 + + while ($job.State -eq 'Running') { + $elapsed = $stopwatch.Elapsed.TotalSeconds + Write-Host "`r[*] Scanning system handles... $($spinnerChars[$spinnerIndex]) [$([math]::Round($elapsed, 1))s]" -NoNewline -ForegroundColor Yellow + $spinnerIndex = ($spinnerIndex + 1) % 4 + Start-Sleep -Milliseconds 200 + } + + $stopwatch.Stop() + Write-Host "`r[✓] Scan completed in $([math]::Round($stopwatch.Elapsed.TotalSeconds, 1))s " -ForegroundColor Green + + # 결과 가져오기 + $handleOutput = Receive-Job -Job $job + Remove-Job -Job $job + + $outputLines = $handleOutput } catch { Write-Host "[ERROR] Failed to execute handle.exe: $($_.Exception.Message)" -ForegroundColor Red - return + exit 1 } -# 결과를 저장할 배열 초기화 -$foundProcesses = @() +# 결과를 저장할 배열 초기화 (ArrayList 사용으로 성능 향상) +$foundProcesses = New-Object System.Collections.ArrayList +$currentProcess = $null + +# 디버깅: 출력 라인 수 표시 +Write-Host "[DEBUG] Total output lines: $($outputLines.Count)" -ForegroundColor DarkGray # Analyze handle.exe output line by line -if ($handleOutput) { - # Use regex to extract process name, PID, and file path - # handle.exe output format variations: - # "ProcessName.exe pid: 1234 type: File C:\path\to\file" - # "ProcessName.exe pid: 1234 user: DOMAIN\User type: File C:\path\to\file" - $regex = "^(.+?)\s+pid:\s*(\d+)\s+.*?\s+type:\s+File\s+(.+?)\s*$" +foreach ($line in $outputLines) { + # "No matching handles found." 메시지 체크 + if ($line -match "No matching handles found") { + Write-Host "✓ No processes are holding handles to this path." -ForegroundColor Green + exit 0 + } - $handleOutput.Split([Environment]::NewLine) | ForEach-Object { - if ($_ -match $regex) { - $processName = $matches[1].Trim() - $processId = [int]$matches[2] - $filePath = $matches[3].Trim() - - # 지정된 디렉토리 경로에 포함되는 결과만 필터링 - if ($filePath.StartsWith($TargetDirectory, [System.StringComparison]::OrdinalIgnoreCase)) { - # PSCustomObject를 생성 - $customObject = [PSCustomObject]@{ - ProcessName = $processName - PID = $processId - FilePath = $filePath - } - $foundProcesses += $customObject - } + # Process line format: "processname.exe pid: 1234 type: File user: DOMAIN\User" + # 또는: "processname.exe pid: 1234 user: DOMAIN\User" + if ($line -match '^(\S+\.exe)\s+pid:\s+(\d+)\s+(.*)$') { + $processName = $matches[1] + $processId = [int]$matches[2] + $remainder = $matches[3] + + # user 정보 추출 + $userName = "" + if ($remainder -match 'user:\s+([^\s]+)') { + $userName = $matches[1] } + + $currentProcess = [PSCustomObject]@{ + ProcessName = $processName + PID = $processId + User = $userName + Handles = New-Object System.Collections.ArrayList + } + [void]$foundProcesses.Add($currentProcess) + + Write-Host "[DEBUG] Found process: $processName (PID: $processId)" -ForegroundColor DarkGray + } + # Handle detail line format: " 1A4: File (RWD) C:\path\to\file" + # 또는 간단한 형식: " 1A4: C:\path\to\file" + elseif ($line -match '^\s+([0-9A-F]+):\s+(.+)$' -and $currentProcess) { + $handleId = $matches[1] + $handleDetails = $matches[2].Trim() + + # Type 정보가 있는지 확인 + $handleType = "File" + if ($handleDetails -match '^(\w+)\s+(.+)$') { + $handleType = $matches[1] + $handleDetails = $matches[2].Trim() + } + + $handleInfo = [PSCustomObject]@{ + HandleID = $handleId + Type = $handleType + Details = $handleDetails + } + [void]$currentProcess.Handles.Add($handleInfo) + + Write-Host "[DEBUG] Handle: $handleId -> $handleDetails" -ForegroundColor DarkGray } } +# 핸들이 없는 프로세스 제거 +$processesWithHandles = New-Object System.Collections.ArrayList +foreach ($proc in $foundProcesses) { + if ($proc.Handles.Count -gt 0) { + [void]$processesWithHandles.Add($proc) + } else { + Write-Host "[DEBUG] Removing process $($proc.ProcessName) (no handles found)" -ForegroundColor DarkGray + } +} +$foundProcesses = $processesWithHandles + +# 파일로 출력 (옵션) +if ($OutputFile) { + $outputLines | Out-File -FilePath $OutputFile -Encoding UTF8 + Write-Host "[+] Raw output saved to: $OutputFile" -ForegroundColor Green +} + # 결과 출력 -if ($foundProcesses.Count -gt 0) { - Write-Host "`n[+] Found $($foundProcesses.Count) open file handles in '$TargetDirectory':" - $foundProcesses | Sort-Object -Property ProcessName, FilePath | Format-Table -AutoSize -} else { - Write-Host "`n[-] No open file handles were found for the specified directory." -} \ No newline at end of file +if ($foundProcesses.Count -eq 0) { + Write-Host "✓ No processes are holding handles to this path." -ForegroundColor Green + exit 0 +} + +Write-Host "⚠ Found $($foundProcesses.Count) process(es) with open handles:" -ForegroundColor Yellow +Write-Host "" + +foreach ($proc in $foundProcesses) { + Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray + Write-Host "Process: " -NoNewline + Write-Host "$($proc.ProcessName)" -ForegroundColor White -NoNewline + Write-Host " (PID: " -NoNewline + Write-Host "$($proc.PID)" -ForegroundColor Cyan -NoNewline + Write-Host ")" + + if ($proc.User) { + Write-Host " User: " -NoNewline -ForegroundColor Gray + Write-Host "$($proc.User)" -ForegroundColor Yellow + } + + Write-Host " Handles: " -NoNewline -ForegroundColor Gray + Write-Host "$($proc.Handles.Count)" -ForegroundColor Magenta + + # 항상 핸들 상세 정보 표시 + if ($proc.Handles.Count -gt 0) { + Write-Host "" + foreach ($handle in $proc.Handles) { + if ($ShowDetails) { + # 상세 모드: Handle ID와 Type도 표시 + Write-Host " ├─ Handle ID: " -NoNewline -ForegroundColor DarkGray + Write-Host "$($handle.HandleID)" -ForegroundColor Cyan + Write-Host " │ Type: " -NoNewline -ForegroundColor DarkGray + Write-Host "$($handle.Type)" -ForegroundColor Green + Write-Host " │ Path: " -NoNewline -ForegroundColor DarkGray + Write-Host "$($handle.Details)" -ForegroundColor White + Write-Host " │" -ForegroundColor DarkGray + } else { + # 기본 모드: 경로만 표시 + Write-Host " • " -NoNewline -ForegroundColor Yellow + Write-Host "$($handle.Details)" -ForegroundColor White + } + } + } + Write-Host "" +} + +Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray +Write-Host "" +Write-Host "💡 To close these handles:" -ForegroundColor Yellow +Write-Host " 1. Close the applications manually" -ForegroundColor Gray +Write-Host " 2. Use Task Manager to end the processes" -ForegroundColor Gray +Write-Host " 3. Run: " -NoNewline -ForegroundColor Gray +Write-Host "Stop-Process -Id -Force" -ForegroundColor White +Write-Host "" \ No newline at end of file