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

馋奶兔 提交于 2020-05-31 04:04:00

问题


I have a situation similar to this previous question but different enough the previous answers don't work.

I am generating a PDF file and then telling Windows to open that file using whatever PDF application the user has installed:

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

This is for a client and they have specified that the PDF file always has the same name. The problem is, if the application they are using to view PDF files is Microsoft Edge (and this may be true for other applications as well), if I try to generate a second PDF before the user has closed Edge, I get an exception "The requested operation cannot be performed on a file with a user-mapped section open."

I would like to create a helpful UI that tells the user they can't generate a second report until they close the first, and I think I need to do it non-destructively because I'd like to use this information to disable the "generate" button before the user presses it, so for example I could probably try deleting the file to check if it's in use, but I don't want to delete the file long before the user tries to generate a new one.

I have this code right now:

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;
}

This code returns 'true' even when the file is open in Edge. It looks like there is no way to request an "exclusive" memory-mapped file.

Is there in fact any way to tell that another process has an open memory-mapped file on a specific physical file?

EDIT

The RestartManager code described here doesn't catch this kind of file lock.

SECOND EDIT

It seems possible that MMI/WQL might contain the data I need, but I don't know which query to use. I've added that as a separate question.


回答1:


UPDATE:

So, by looking into the source code of the Process Hacker suggested by @Simon Mourier it seems that NtQueryVirtualMemory is the way to go. Conveniently, there is a .NET library called NtApiDotNet which provides a managed API for this and many other NT functions.

So here's how you can check if a file is mapped in another process:

  1. Make sure you are targeting x64 platform (otherwise you won't be able to query 64-bit processes).
  2. Create a new C# console app.
  3. Run Install-Package NtApiDotNet in the package manager console.
  4. Change filePath and processName values in this code and then execute it:
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;
    }
}

Notes:

  1. DevicePathConverter class implementation is a slightly refactored version of this code.
  2. If you want to search across all running processes (by passing processName as null), you'll need to run your executable with elevated priveleges (as administrator), otherwise NtProcess.Open will throw an exception for some system processes like svchost.

Original answer:

Yes, this is indeed very tricky. I know that 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" keeps this lock alive. I suspect that's the reason why the RestartManager also fails to detect this file reference.

Anyway, after some trial and error I stumbled upon a solution similar to the one proposed by @somebody but not requiring to rewrite the whole file. We can just trim the last byte of the file and then write it back:

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;
    }
}

If the file is open in Microsoft Edge, the stream.SetLength(fileLength - 1) operation will fail with the ERROR_USER_MAPPED_FILE error code in the exception.

This is also a very dirty hack, mostly because we are relying on the fact that Microsoft Edge will map the whole file (which seems to be the case for all files I've tested) but the alternatives are either to dig into the process handle data structures (if I were to go that route I would probably start with enumerating all section handles and checking if one of them corresponds to a mapped file) or to just reverse engineer the Process Explorer.




回答2:


I use this to check if a file is in use:

        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;
        }

Hope this helps.

EDIT: I also use it in conjunction with this code when needed (it continuously checks when the file is free, only then will it continue execution of further lines).

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



回答3:


This feels like a really dirty hack but you could try to read the file and overwrite it with itself. That would modify the other answer to:

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;
    }
}

I'm not sure about the overhead but I tried to just overwrite the first byte and that didn't work. I have successfully tested the above code with edge.

Note that you still should handle errors when writing the new file because the file might get locked after the check and before the writing process.



来源:https://stackoverflow.com/questions/61736227/how-to-detect-if-a-memorymappedfile-is-in-use-c-net-core

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!