如何检测 MemoryMappedFile 是否在使用中(C# .NET Core)

How to detect if a MemoryMappedFile is in use (C# .NET Core)

我的情况与 this previous question 类似,但差异很大,之前的答案不起作用。

我正在生成一个 PDF 文件,然后告诉 Windows 使用用户安装的任何 PDF 应用程序打开该文件:

new Process
{
    StartInfo = new ProcessStartInfo(pdfFileName)
    {
        UseShellExecute = true
    }
}.Start();

这是给客户的,他们指定 PDF 文件始终具有相同的名称。问题是,如果他们用来查看 PDF 文件的应用程序是 Microsoft Edge(其他应用程序也可能如此),如果我尝试在用户关闭 Edge 之前生成第二个 PDF,我会得到一个异常 "The requested operation cannot be performed on a file with a user-mapped section open."

我想创建一个有用的 UI 告诉用户他们在关闭第一个报告之前不能生成第二个报告,我认为我需要非破坏性地进行,因为我会喜欢使用此信息在用户按下按钮之前禁用 "generate" 按钮,因此例如我可能会尝试删除该文件以检查它是否正在使用,但我不想在用户尝试生成一个新的。

我现在有这个代码:

public static bool CanWriteToFile(string pdfFileName)
{
    if (!File.Exists(pdfFileName))
        return true;

    try
    {
        using (Stream stream = new FileStream(pdfFileName, FileMode.Open, FileAccess.ReadWrite))
        {
        }
    }
    catch (Exception ex)
    {
        return false;
    }

    try
    {
        using (MemoryMappedFile map = MemoryMappedFile.CreateFromFile(pdfFileName, FileMode.Open, null, 0, MemoryMappedFileAccess.ReadWrite))
        {
            using (MemoryMappedViewStream stream = map.CreateViewStream())
            {
                stream.Position = 0;
                int firstByte = stream.ReadByte();
                if (firstByte != -1)
                {
                    stream.Position = 0;
                    stream.WriteByte((byte)firstByte);
                    stream.Flush();
                }
            }
        }
    }
    catch(Exception ex)
    {
        return false;
    }

    return true;
}

此代码 returns 'true' 即使文件在 Edge 中打开。看起来无法请求 "exclusive" 内存映射文件。

实际上有什么方法可以告诉另一个进程在特定物理文件上有一个打开的内存映射文件吗?

编辑

描述的 RestartManager 代码 here 没有捕捉到这种文件锁。

第二次编辑

似乎 MMI/WQL 可能包含我需要的数据,但我不知道该使用哪个查询。我已将其添加为 .

我用它来检查文件是否正在使用:

        public static bool IsFileLocked(string fullFileName)
        {
            var file = new FileInfo(fullFileName);
            FileStream stream = null;

            try
            {
                if (File.Exists(file.FullName))
                {
                    stream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None);
                }
                else
                {
                    return false;
                }
            }
            catch (IOException)
            {
                return true;
            }
            finally
            {
                if (stream != null)
                    stream.Close();
            }

            return false;
        }

希望对您有所帮助。

编辑:我还在需要时将它与此代码结合使用(它会不断检查文件何时空闲,然后才会继续执行更多行)。

        public static void WaitForFileReady(string fullFileName)
        {
            try
            {
                if (File.Exists(fullFileName))
                {
                    while (IsFileLocked(fullFileName))
                        Thread.Sleep(100);
                }
            }
            catch (Exception)
            {
                throw;
            }
        }

这感觉像是一个非常肮脏的 hack,但您可以尝试读取文件并用自身覆盖它。这会将另一个答案修改为:

public static bool IsFileLocked(string fullFileName)
{
    try
    {
        if (!File.Exists(fullFileName))
            return false;

        File.WriteAllBytes(fullFileName, File.ReadAllBytes(fullFileName));          
        return false;

    }
    catch (IOException)
    {
        return true;
    }
}

我不确定开销,但我尝试只覆盖第一个字节,但没有成功。我已经成功地用 edge 测试了上面的代码。

请注意,在写入新文件时仍应处理错误,因为文件可能会在检查之后和写入过程之前被锁定。

更新:

因此,通过查看 source code of the Process Hacker it seems that NtQueryVirtualMemory is the way to go. Conveniently, there is a .NET library called NtApiDotNet,它为这个功能和许多其他 NT 功能提供了托管 API。

下面是检查文件是否映射到另一个进程的方法:

  1. 确保您的目标是 x64 平台(否则您将无法查询 64 位进程)。
  2. 创建一个新的 C# 控制台应用程序。
  3. 运行 Install-Package NtApiDotNet 在程序包管理器控制台中。
  4. 更改此代码中的 filePathprocessName 值,然后执行它:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using NtApiDotNet;

class Program
{
    static bool IsFileMemoryMappedInProcess(string filePath, string processName = null)
    {
        if (!File.Exists(filePath))
        {
            return false;
        }
        string fileName = Path.GetFileName(filePath);
        Process[] processes;
        if (!String.IsNullOrEmpty(processName))
        {
            processes = Process.GetProcessesByName(processName);
        }
        else
        {
            processes = Process.GetProcesses();
        }
        foreach (Process process in processes)
        {
            using (NtProcess ntProcess = NtProcess.Open(process.Id,
                ProcessAccessRights.QueryLimitedInformation))
            {
                foreach (string deviceFilePath in ntProcess.QueryAllMappedFiles().
                    Select(mappedFile => mappedFile.Path))
                {
                    if (deviceFilePath.EndsWith(fileName,
                        StringComparison.CurrentCultureIgnoreCase))
                    {
                        string dosFilePath =
                            DevicePathConverter.ConvertToDosPath(deviceFilePath);
                        if (String.Compare(filePath, dosFilePath, true) == 0)
                        {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    static void Main(string[] args)
    {
        string filePath = @"C:\Temp\test.pdf";
        string processName = "MicrosoftPdfReader";
        if (IsFileMemoryMappedInProcess(filePath, processName))
        {
            Console.WriteLine("File is mapped");
        }
        else
        {
            Console.WriteLine("File is not mapped");
        }
    }
}

public class DevicePathConverter
{
    private const int MAX_PATH = 260;
    private const string cNetworkDevicePrefix = @"\Device\LanmanRedirector\";
    private readonly static Lazy<IList<Tuple<string, string>>> lazyDeviceMap =
        new Lazy<IList<Tuple<string, string>>>(BuildDeviceMap, true);

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    internal static extern int QueryDosDevice(
            [In] string lpDeviceName,
            [Out] StringBuilder lpTargetPath,
            [In] int ucchMax);

    public static string ConvertToDosPath(string devicePath)
    {
        IList<Tuple<string, string>> deviceMap = lazyDeviceMap.Value;
        Tuple<string, string> foundItem =
            deviceMap.FirstOrDefault(item => IsMatch(item.Item1, devicePath));
        if (foundItem == null)
        {
            return null;
        }
        return string.Concat(foundItem.Item2,
            devicePath.Substring(foundItem.Item1.Length));
    }

    private static bool IsMatch(string devicePathStart, string fullDevicePath)
    {
        if (!fullDevicePath.StartsWith(devicePathStart,
            StringComparison.InvariantCulture))
        {
            return false;
        }
        if (devicePathStart.Length == fullDevicePath.Length)
        {
            return true;
        }
        return fullDevicePath[devicePathStart.Length] == '\';
    }

    private static IList<Tuple<string, string>> BuildDeviceMap()
    {
        IEnumerable<string> logicalDrives = Environment.GetLogicalDrives().
            Select(drive => drive.Substring(0, 2));
        var driveTuples = logicalDrives.Select(drive =>
            Tuple.Create(NormalizeDeviceName(QueryDosDevice(drive)), drive)).ToList();
        var networkDevice = Tuple.Create(cNetworkDevicePrefix.
            Substring(0, cNetworkDevicePrefix.Length - 1), "\");
        driveTuples.Add(networkDevice);
        return driveTuples;
    }

    private static string QueryDosDevice(string dosDevice)
    {
        StringBuilder targetPath = new StringBuilder(MAX_PATH);
        int queryResult = QueryDosDevice(dosDevice, targetPath, MAX_PATH);
        if (queryResult == 0)
        {
            throw new Exception("QueryDosDevice failed");
        }
        return targetPath.ToString();
    }

    private static string NormalizeDeviceName(string deviceName)
    {
        if (deviceName.StartsWith(cNetworkDevicePrefix,
            StringComparison.InvariantCulture))
        {
            string shareName = deviceName.Substring(deviceName.
                IndexOf('\', cNetworkDevicePrefix.Length) + 1);
            return string.Concat(cNetworkDevicePrefix, shareName);
        }
        return deviceName;
    }
}

备注:

  1. DevicePathConverter class 实现是 this code.
  2. 的稍微重构版本
  3. 如果您想搜索所有 运行ning 进程(通过将 processName 作为 null 传递),您需要 运行 您的可执行文件具有更高的权限(作为管理员),否则 NtProcess.Open 会为某些系统进程抛出异常,例如 svchost。

原回答:

是的,这确实很棘手。我知道 Process Explorer manages to enumerate memory-mapped files for a process but I have no idea how does it do that. As I can see, MicrosoftPdfReader.exe process closes the file handle immediately after creating a memory-mapped view, so just enumerating the file handles of that process via NtQuerySystemInformation / NtQueryObject won't work because there is no file handle at that point and only the "internal reference" 使这个锁保持活动状态。我怀疑这就是 RestartManager 也未能检测到此文件引用的原因。

无论如何,经过反复试验,我偶然发现了一个类似于 但不需要重写整个文件的解决方案。我们可以只 trim 文件的最后一个字节,然后将其写回:

const int ERROR_USER_MAPPED_FILE = 1224; // from winerror.h

bool IsFileLockedByMemoryMappedFile(string filePath)
{
    if (!File.Exists(filePath))
    {
        return false;
    }
    try
    {
        using (FileStream stream = new FileStream(filePath, FileMode.Open,
            FileAccess.ReadWrite, FileShare.None))
        {
            stream.Seek(-1, SeekOrigin.End);
            int lastByte = stream.ReadByte();
            long fileLength = stream.Length;
            stream.SetLength(fileLength - 1);
            stream.WriteByte((byte)lastByte);
            return false;
        }
    }
    catch (IOException ex)
    {
        int errorCode = Marshal.GetHRForException(ex) & 0xffff;
        if (errorCode == ERROR_USER_MAPPED_FILE)
        {
            return true;
        }
        throw ex;
    }
}

如果文件在 Microsoft Edge 中打开,stream.SetLength(fileLength - 1) 操作将失败并在异常中显示 ERROR_USER_MAPPED_FILE 错误代码。

这也是一个非常肮脏的 hack,主要是因为我们依赖于 Microsoft Edge 将映射整个文件这一事实(我测试过的所有文件似乎都是这种情况)但替代方案是深入研究流程句柄数据结构(如果我要走那条路,我可能会从枚举所有部分句柄并检查其中一个是否对应于映射文件开始)或者只是对 Process Explorer 进行逆向工程。