文件句柄占用解锁.psm1

# 文件句柄占用解锁.psm1
# PowerShell 模块:封装 文件句柄占用解锁.exe,提供列出占用进程、关闭占用句柄、抢夺串口等功能。
# 本模块仅负责解析参数并转发调用 exe,不重复实现其内部逻辑。
# 注意:exe 需管理员权限(加载 PROCEXP152 内核驱动、启用 SeDebug/SeLoadDriver 特权);
# 本模块在非管理员下会唤起 UAC 提权运行 exe 并把输出取回。

# 模块内共享:解析并缓存 exe 路径
$script:Exe路径 = $null

function Test-管理员权限
{
    <#
    .SYNOPSIS
        判断当前 PowerShell 进程是否以管理员(提升的)权限运行。
    .OUTPUTS
        System.Boolean 是管理员返回 $true,否则返回 $false。
    .EXAMPLE
        if (-not (Test-管理员权限)) { Write-Warning '需要管理员权限' }
    #>

    $身份 = [System.Security.Principal.WindowsIdentity]::GetCurrent()
    $主体 = [System.Security.Principal.WindowsPrincipal]::new($身份)
    return $主体.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Resolve-Exe路径
{
    <#
    .SYNOPSIS
        查找与本模块同目录的 文件句柄占用解锁.exe 的实际路径。
    .DESCRIPTION
        exe 经 vcxproj 配置输出到解决方案的 PowerShell 目录,与本模块同目录。
        找到后缓存到模块变量,后续调用直接复用。
    .OUTPUTS
        System.String exe 的完整路径;未找到时抛出异常。
    #>

    if ($script:Exe路径 -and (Test-Path $script:Exe路径)) { return $script:Exe路径 }
    # exe 经 vcxproj 输出到 命令行\PowerShell\(与本模块所在 PowerShell\ 平级)
    $模块根 = Split-Path $PSScriptRoot -Parent
    $候选列表 = @(
        (Join-Path $模块根 '命令行\PowerShell\文件句柄占用解锁.exe'),
        (Join-Path $PSScriptRoot '文件句柄占用解锁.exe')
    )
    foreach ($候选 in $候选列表)
    {
        if (Test-Path $候选)
        {
            $script:Exe路径 = (Resolve-Path $候选).Path
            return $script:Exe路径
        }
    }
    throw "找不到 文件句柄占用解锁.exe,请先编译 命令行 项目。已尝试:$($候选列表 -join ';')"
}

function Invoke-底层工具
{
    <#
    .SYNOPSIS
        调用 文件句柄占用解锁.exe 并透传参数,返回其原始输出与退出码。
    .DESCRIPTION
        供模块内各公开函数统一调用的内部转发器。把全部参数原样传给 exe。
        若当前不是管理员,则唤起 UAC 以管理员身份运行 exe(隐藏窗口),输出经临时文件读回。
        返回包含 输出、退出码 的对象。
    .PARAMETER 参数
        要透传给 exe 的参数数组。
    #>

    param([Parameter(ValueFromRemainingArguments = $true)][string[]]$参数)
    $exe = Resolve-Exe路径
    if (Test-管理员权限)
    {
        # 已是管理员:直接调用并捕获输出
        $输出 = & $exe @参数 2>&1 | Out-String
        return [pscustomobject]@{ 输出 = $输出; 退出码 = $LASTEXITCODE }
    }
    # 非管理员:唤起 UAC 提权运行 exe 本身。RunAs 无法直接重定向,故经 cmd /c 把输出写入临时文件再读回(用完即删)。
    $临时输出 = Join-Path $env:TEMP ('文件句柄占用解锁_' + [guid]::NewGuid().ToString('N') + '.log')
    $参数拼接 = ($参数 | ForEach-Object { '"' + $_ + '"' }) -join ' '
    $命令行 = '/c ""' + $exe + '" ' + $参数拼接 + ' > "' + $临时输出 + '" 2>&1"'
    try
    {
        $进程 = Start-Process -FilePath 'cmd.exe' -ArgumentList $命令行 -Verb RunAs -Wait -WindowStyle Hidden -PassThru
        $输出 = if (Test-Path $临时输出) { Get-Content $临时输出 -Raw -Encoding UTF8 } else { '' }
        return [pscustomobject]@{ 输出 = $输出; 退出码 = $进程.ExitCode }
    }
    finally
    {
        Remove-Item $临时输出 -Force -ErrorAction SilentlyContinue
    }
}

function Find-文件占用
{
    <#
    .SYNOPSIS
        列出占用指定文件、目录或串口的所有进程及其句柄。
    .DESCRIPTION
        转发调用 exe 的 list 命令。枚举系统句柄表,借助 PROCEXP152 内核驱动匹配目标路径,
        输出占用该目标的进程 PID、句柄值与可执行文件路径。
        结果解析为对象数组返回,便于进一步筛选或管道传给 Close-占用句柄。
    .PARAMETER 路径
        要检查的文件路径、目录路径,或串口号(如 COM3)。
    .PARAMETER 原始输出
        若指定此开关,则不解析为对象,直接返回 exe 的原始文本输出。
    .OUTPUTS
        PSCustomObject[] 含 进程ID、句柄值、进程路径 属性;未找到占用时返回空数组。
    .EXAMPLE
        Find-文件占用 -路径 'C:\test.txt'
        列出占用 C:\test.txt 的所有进程。
    .EXAMPLE
        Find-文件占用 -路径 'C:\test.txt' | Close-占用句柄
        查出占用 C:\test.txt 的进程句柄并全部关闭,解除占用。
    .EXAMPLE
        Find-文件占用 -路径 'COM3'
        列出占用串口 COM3 的进程。
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$路径,
        [switch]$原始输出
    )
    $结果 = Invoke-底层工具 'list' $路径
    if ($原始输出) { return $结果.输出 }
    $占用列表 = @()
    foreach ($匹配 in [regex]::Matches($结果.输出, 'PID=(\d+)\s+句柄=0x([0-9A-Fa-f]+)\s+(.+?)[\r\n]'))
    {
        $占用列表 += [pscustomobject]@{
            进程ID   = [int]$匹配.Groups[1].Value
            句柄值   = '0x' + $匹配.Groups[2].Value
            进程路径 = $匹配.Groups[3].Value.Trim()
        }
    }
    return $占用列表
}

function Close-占用句柄
{
    <#
    .SYNOPSIS
        关闭指定进程的指定句柄,解除其对文件或串口的占用。
    .DESCRIPTION
        转发调用 exe 的 close 命令。可一次传入多组 进程ID+句柄值。
        句柄值接受十六进制字符串(可省略 0x 前缀)或整数。
        支持从 Find-文件占用 经管道输入对象。
    .PARAMETER 进程ID
        占用进程的数字 PID。可传多个,与 句柄值 一一对应。
    .PARAMETER 句柄值
        占用句柄的十六进制字符串(如 0x1A8 或 1A8)。可传多个,与 进程ID 一一对应。
    .PARAMETER 输入对象
        从 Find-文件占用 管道传入的对象(含 进程ID 与 句柄值 属性)。
    .OUTPUTS
        System.String exe 的原始文本输出(每个句柄的关闭结果与汇总)。
    .EXAMPLE
        Close-占用句柄 -进程ID 1234 -句柄值 '0x1A8'
        关闭 PID 为 1234 的进程的句柄 0x1A8。
    .EXAMPLE
        Close-占用句柄 -进程ID 1234,5678 -句柄值 '0x1A8','0x2B4'
        一次关闭两组 进程/句柄。
    .EXAMPLE
        Find-文件占用 -路径 'C:\test.txt' | Close-占用句柄
        关闭占用 C:\test.txt 的全部句柄。
    #>

    [CmdletBinding(DefaultParameterSetName = '参数')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = '参数', Position = 0)]
        [int[]]$进程ID,
        [Parameter(Mandatory = $true, ParameterSetName = '参数', Position = 1)]
        [string[]]$句柄值,
        [Parameter(Mandatory = $true, ParameterSetName = '管道', ValueFromPipeline = $true)]
        [psobject[]]$输入对象
    )
    begin
    {
        $参数列表 = @()
    }
    process
    {
        if ($PSCmdlet.ParameterSetName -eq '管道')
        {
            foreach ($项 in $输入对象)
            {
                $参数列表 += [string]$项.进程ID
                $参数列表 += [string]$项.句柄值
            }
        }
    }
    end
    {
        if ($PSCmdlet.ParameterSetName -eq '参数')
        {
            if ($进程ID.Count -ne $句柄值.Count)
            {
                throw "进程ID 数量($($进程ID.Count))与 句柄值 数量($($句柄值.Count))不一致。"
            }
            for ($i = 0; $i -lt $进程ID.Count; $i++)
            {
                $参数列表 += [string]$进程ID[$i]
                $参数列表 += [string]$句柄值[$i]
            }
        }
        if ($参数列表.Count -eq 0) { throw "未提供任何要关闭的 进程ID/句柄值。" }
        $结果 = Invoke-底层工具 'close' @参数列表
        return $结果.输出
    }
}

function Clear-串口占用
{
    <#
    .SYNOPSIS
        强行关闭占用指定串口的其它进程的句柄,抢夺该串口。
    .DESCRIPTION
        转发调用 exe 的 snatch 命令。查询串口对应的内核设备路径,
        找到占用它的其它进程并关闭其句柄,使本进程可打开该串口。
        若串口正被本进程自己占用,将报错而不会抢夺。
    .PARAMETER 串口号
        要抢夺的串口号(如 COM3)。
    .OUTPUTS
        System.String exe 的原始文本输出(每个被关闭句柄的结果与汇总)。
    .EXAMPLE
        Clear-串口占用 -串口号 'COM3'
        关闭占用 COM3 的其它进程的句柄。
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$串口号
    )
    $结果 = Invoke-底层工具 'snatch' $串口号
    return $结果.输出
}

Export-ModuleMember -Function Find-文件占用, Close-占用句柄, Clear-串口占用, Test-管理员权限