使用 PowerShell 确定对文件具有写锁定的进程

Determinig process having write locks on files using PowerShell

我需要编写一个 PowerShell 脚本来移动包含大量文件(大约 1k 到 10k)的文件夹,然后再尝试移动我想检查文件中是否有任何进程有写锁,应该询问用户确认使用写锁终止进程并移动文件。我已经尝试了

中提出的解决方案

Identify locks on dll files -- 不适用于文件夹

Determine process that locks a file -- 太复杂且容易出错

Unlock files using handle.exe -- 仅关闭句柄并关闭所有句柄。

一种选择是使用 SysInternals 中的 Handle.exe 并终止具有写锁的进程。如果我们手动执行此操作很容易,因为通过使用进程名称,我们可以确定哪个进程锁定了文件并将其终止。有什么方法可以根据句柄类型(下图中以红色突出显示)确定它是否是写锁?句柄类型及其权限是否有任何映射?这样我就可以解析 handle.exe 具有写锁的输出和提取过程。

如果您只想使用 handle.exe 的输出来要求用户确认终止进程,您可以将其包装在一个简短的脚本中:

$path = "C:\Workspace"
$files = (C:\SysInternals\handle.exe -nobanner $path) | Out-String
$filesArray = $files.Split([Environment]::NewLine,[System.StringSplitOptions]::RemoveEmptyEntries)

foreach($file in $filesArray) {
   $parts = $file.split(" ",[System.StringSplitOptions]::RemoveEmptyEntries)

   $proc = $parts[0]
   $procId = $parts[2]
   $type = $parts[4]
   $handle = $parts[5].split(":")[0]
   $path = $parts[6]

   $Readhost = Read-Host "Terminate $($proc)? ( y / n ) " 
   Switch ($ReadHost) 
     { 
       Y {(.\handle -c $handle)} 
       N { Write-Host "Skipping $proc" }
     } 
 }

我没有找到任何简单的方法来提取有关它是哪种锁或涉及的权限的信息。

我找不到完成此任务的唯一 PowerShell 方法。我使用了这个 Answer 中的 .net 代码。这段代码的一个问题是它 returning 了一个进程对象列表,这对我来说是太多不必要的信息所以我对函数 WhoisLocking 进行了一些修改为 return Dictionary进程ID和进程名称。下面是函数的修改代码

static public Dictionary<int, String> WhoIsLocking(string path)
{
    uint handle;
    string key = Guid.NewGuid().ToString();
    List<Process> processes = new List<Process>();
    Dictionary<int, String> processDict = new Dictionary<int, String>(); 

    int res = RmStartSession(out handle, 0, key);
    if (res != 0) throw new Exception("Could not begin restart session.  Unable to determine file locker.");

    try
    {
        const int ERROR_MORE_DATA = 234;
        uint pnProcInfoNeeded = 0,
             pnProcInfo = 0,
             lpdwRebootReasons = RmRebootReasonNone;

        string[] resources = new string[] { path }; // Just checking on one resource.

        res = RmRegisterResources(handle, (uint)resources.Length, resources, 0, null, 0, null);

        if (res != 0) throw new Exception("Could not register resource.");                                    

        //Note: there's a race condition here -- the first call to RmGetList() returns
        //      the total number of process. However, when we call RmGetList() again to get
        //      the actual processes this number may have increased.
        res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, null, ref lpdwRebootReasons);

        if (res == ERROR_MORE_DATA)
        {
            // Create an array to store the process results
            RM_PROCESS_INFO[] processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded];
            pnProcInfo = pnProcInfoNeeded;

            // Get the list
            res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons);
            if (res == 0)
            {
                processes = new List<Process>((int)pnProcInfo);

                // Enumerate all of the results and add them to the 
                // list to be returned
                for (int i = 0; i < pnProcInfo; i++)
                {
                    try
                    {
                        processes.Add(Process.GetProcessById(processInfo[i].Process.dwProcessId));
                    }
                    // catch the error -- in case the process is no longer running
                    catch (ArgumentException) { }
                }
            }
            else throw new Exception("Could not list processes locking resource.");                    
        }
        else if (res != 0) throw new Exception("Could not list processes locking resource. Failed to get size of result.");                    
    }
    finally
    {
        RmEndSession(handle);
    }
    //Returning a dictionary to keep it simple
    foreach(Process p in processes){
        processDict.Add(p.Id, p.ProcessName);
    }
    return processDict;
}

此代码的另一个问题是它仅适用于文件,因此我在 PoweShell 中创建了一个包装器来为文件夹中的每个文件调用此函数。此 class 解析每次调用的所有句柄,因此为每个文件调用此方法效率低下。为了优化它,我只为有写锁的文件调用这个函数。我也一直在寻找一些方法来将进程所有者添加到信息中,但不幸的是,.net Process 对象不包含所有者信息,而且我没有找到在 .net 中执行此操作的任何简单方法。为了获取所有者信息,我正在使用 PowerShell Get-CimInstance Win32_ProcessInvoke-CimMethod。下面是我的 PowerShell 方法。

function Get-LockingProcess {
[CmdletBinding(SupportsShouldProcess = $true)]
param(
    # Specifies a path to the file to be modified
    [Parameter(Mandatory = $true)]
    $path ,
    [switch]$Raw
)
#adding using reflection to avoid file being locked
$bytes = [System.IO.File]::ReadAllBytes($($PSScriptRoot + "\lib\LockedProcess.dll"))
[System.Reflection.Assembly]::Load($bytes) | Out-Null
$process = @{}
$path = Get-Item -Path $path
if ($path.PSIsContainer) {
    Get-ChildItem -Path $path -Recurse | % {
        $TPath = $_
        try { [IO.File]::OpenWrite($TPath.FullName).close()}
        catch {
            if (! $TPath.PSIsContainer) {
                $resp = $([FileUtil]::WhoIsLocking($TPath.FullName))
            }
            foreach ($k in $resp.Keys) {
                if (! $process.ContainsKey($k)) {
                    $process.$k = $resp.$k
                }
            }
        }
    }
}
else {
    $process = $([FileUtil]::WhoIsLocking($path.FullName))
}
#adding additional details to the hash
$processList=@()
foreach($id in $process.Keys){
    $temp=@{}
    $temp.Name=$process.$id
    $proc=Get-CimInstance Win32_Process -Filter $("ProcessId="+$id)
    $proc=Invoke-CimMethod -InputObject $proc -MethodName GetOwner
    if($proc.Domain -ne ""){
        $temp.Owner=$proc.Domain+"\"+$proc.User
    }else{
        $temp.Owner=$proc.User
    }
    $temp.PID=$id
    $processList+=$temp
}
if($Raw){
    $processList
}else{
    $processList.ForEach({[PSCustomObject]$_}) | Format-Table -AutoSize
}

默认情况下,这会以表格形式打印结果,如果需要在脚本调用者中使用它,可以传递 Raw,这将 return 它作为 array。这可能不是最有效的解决方案,但可以很好地完成任务。如果您发现可以优化的地方,请随时发表评论。