FileSystemWatcher Fencing(Part 1)

 

This post is a follow up to the FileSystemWatcher Follies post.  I received a lot of feedback that it would be useful to highlight what would be appropriate to guide against some of the pitfalls that I mentioned in that post.  I’ll cover several of the issues here over a couple of posts and propose things that could be done to detect that they are there before using the FileSystemWatcher class against them.  Though the code examples will all be in C#, there will be some P/Invoke involved here as not all of this functionality is exposed through .NET Framework classes at this time. 

Determining whether a file system is local or remote

A lot of the pitfalls of the FileSystemWatcher class come into play when remote file shares are introduced.  There are a couple of ways to do this based off of a file or directory path that we’ll go over; we’ll discuss each individually then show a code sample that incorporates all of them later on in the post:

  1. GetFinalPathNameByHandle
  2. GetFileInformationByHandleEx

GetFinalPathNameByHandle returns the final path, after resolving junctions such as symlinks, for the specified file.  This means that it will return the full path to a file on a mapped drive for instance.  Using it, we can quickly check for UNC paths.

GetFileInformationByHandleEx can obtain many different types of data, but the type that we’ll be most interested in is FILE_REMOTE_PROTOCOL_INFO.   The most important thing about this function for determining whether a file system is local or remote is that it will simply fail with the error ERROR_INVALID_PARAMETER (87) if used against a non-remote share.  Also, in the event that the path is from a local share and done through the loopback address, that information will be given too. 

Determining what type of file sharing protocol is in use and whether or not a share is in offline mode

GetFileInformationByHandleEx /FILE_REMOTE_PROTOCOL_INFO comes into play again here.  It lets us know what type of file sharing protocol is in use.  Remember from the last post that FileSystemWatcher is not supported on all types of file shares.  An example of what is obtained from this function can be found later on in this post.

Code Example – Extension for System.IO.FileInfo

Here is the code for a sample application which does the things mentioned above.  Please do note that there would be some slight modifications necessary to use directory names instead of file names (use the supplied CreateFIleW to obtain a handle to the directory for use by the other functions and close it with CloseHandle when finished using it).

 using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace FileInformationChecks
{
    public enum FileInformationClassEnum : uint
    {
        FileBasicInfo = 0,//    FILE_BASIC_INFO
        FileStandardInfo = 1,//    FILE_STANDARD_INFO
        FileNameInfo = 2,//    FILE_NAME_INFO
        FileStreamInfo = 7,//    FILE_STREAM_INFO
        FileCompressionInfo = 8,//    FILE_COMPRESSION_INFO
        FileAttributeTagInfo = 9,//    FILE_ATTRIBUTE_TAG_INFO
        FileIdBothDirectoryInfo = 0xa,//    FILE_ID_BOTH_DIR_INFO
        FileIdBothDirectoryRestartInfo = 0xb,//    FILE_ID_BOTH_DIR_INFO
        FileRemoteProtocolInfo = 0xd,//    FILE_REMOTE_PROTOCOL_INFO
        FileFullDirectoryInfo = 0xe,//    FILE_FULL_DIR_INFO
        FileFullDirectoryRestartInfo = 0xf,//    FILE_FULL_DIR_INFO
        FileStorageInfo = 0x10,//    FILE_STORAGE_INFO
        FileAlignmentInfo = 0x11,//    FILE_ALIGNMENT_INFO
        FileIdInfo = 0x12,//    FILE_ID_INFO
        FileIdExtdDirectoryInfo = 0x13,//    FILE_ID_EXTD_DIR_INFO
        FileIdExtdDirectoryRestartInfo = 0x14//    FILE_ID_EXTD_DIR_INFO
    }
    
    [StructLayout(LayoutKind.Sequential)]
    public struct FILE_REMOTE_PROTOCOL_INFORMATION
    {
        // Structure Version
        public ushort StructureVersion;     // 1 if before Windows 8 (ProtocolSpecificReserved only), 
             //2 if >= Windows 8 (Smb2 protocol information may be present - see properties)
        public ushort StructureSize;        // sizeof(FILE_REMOTE_PROTOCOL_INFORMATION)

        // Protocol Version & Type
        public RemoteProtocolEnum Protocol; // Protocol (WNNC_NET_*) defined in winnetwk.h or ntifs.h.
        public ushort ProtocolMajorVersion;
        public ushort ProtocolMinorVersion;
        public ushort ProtocolRevision;

        public ushort Reserved;

        public long Flags;

        // Protocol-Generic Information
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
        public int[] GenericReserved; //Length is always 8

        // Protocol specific information
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
        public int[] ProtocolSpecificReserved; //Length is always 16

        public int SMB2ServerCapabilities
        {
            get{
                if (StructureVersion == 2){
                    return ProtocolSpecificReserved[0];
                }
                return 0;
            }
        }
        public int SMB2ShareCapabilities
        {
            get{
                if (StructureVersion == 2){
                    return ProtocolSpecificReserved[1];
                }
                return 0;
            }
        }
        public int SMB2ShareCachingFlags
        {
            get{
                if (StructureVersion == 2){
                    return ProtocolSpecificReserved[2];
                }
                return 0;
            }
        }
    }

    [Flags]
    public enum RemoteProtocolFlags : uint
    {
        Loopback = 1,
        Offline = 2,
        PersistentHandle = 4,
        Privacy = 8,
        Integrity = 0x10,
        MutalAuth = 0x20
    }
    public enum RemoteProtocolEnum : uint
    {
        MSNET = 0x00010000,
        SMB = 0x00020000,
        LANMAN = 0x00020000,
        NETWARE = 0x00030000,
        VINES = 0x00040000,
        TENNET = 0x00050000,
        LOCUS = 0x00060000,
        SUN_PC_NFS = 0x00070000,
        LANSTEP = 0x00080000,
        NINETILES = 0x00090000,
        LANTASTIC = 0x000A0000,
        AS400 = 0x000B0000,
        FTP_NFS = 0x000C0000,
        PATHWORKS = 0x000D0000,
        LIFENET = 0x000E0000,
        POWERLAN = 0x000F0000,
        BWNFS = 0x00100000,
        COGENT = 0x00110000,
        FARALLON = 0x00120000,
        APPLETALK = 0x00130000,
        INTERGRAPH = 0x00140000,
        SYMFONET = 0x00150000,
        CLEARCASE = 0x00160000,
        FRONTIER = 0x00170000,
        BMC = 0x00180000,
        DCE = 0x00190000,
        AVID = 0x001A0000,
        DOCUSPACE = 0x001B0000,
        MANGOSOFT = 0x001C0000,
        SERNET = 0x001D0000,
        RIVERFRONT1 = 0x001E0000,
        RIVERFRONT2 = 0x001F0000,
        DECORB = 0x00200000,
        PROTSTOR = 0x00210000,
        FJ_REDIR = 0x00220000,
        DISTINCT = 0x00230000,
        TWINS = 0x00240000,
        RDR2SAMPLE = 0x00250000,
        CSC = 0x00260000,
        THREEIN1 = 0x00270000,
        EXTENDNET = 0x00290000,
        STAC = 0x002A0000,
        FOXBAT = 0x002B0000,
        YAHOO = 0x002C0000,
        EXIFS = 0x002D0000,
        DAV = 0x002E0000,
        KNOWARE = 0x002F0000,
        OBJECT_DIRE = 0x00300000,
        MASFAX = 0x00310000,
        HOB_NFS = 0x00320000,
        SHIVA = 0x00330000,
        IBMAL = 0x00340000,
        LOCK = 0x00350000,
        TERMSRV = 0x00360000,
        SRT = 0x00370000,
        QUINCY = 0x00380000,
        OPENAFS = 0x00390000,
        AVID1 = 0x003A0000,
        DFS = 0x003B0000,
        KWNP = 0x003C0000,
        ZENWORKS = 0x003D0000,
        DRIVEONWEB = 0x003E0000,
        VMWARE = 0x003F0000,
        RSFX = 0x00400000,
        MFILES = 0x00410000,
        MS_NFS = 0x00420000,
        GOOGLE = 0x00430000,
        NDFS = 0x00440000
    }

    internal partial class NativeMethods
    {

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern int QueryDosDeviceW(
            [MarshalAs(UnmanagedType.LPWStr)]
            string name,
            StringBuilder path,
            int pathSize
            );

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern int GetFileInformationByHandleEx(IntPtr hFile,
            int FileInformationClass,
            IntPtr fileInformation,
            int bufferSize
            );
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern int GetFinalPathNameByHandleW(IntPtr hFile,
            StringBuilder filePath,
            int fpLength,
            int flags
            );

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern int GetFileInformationByHandleEx(IntPtr hFile,
            int FileInformationClass_Remote_0x0d,
            ref FILE_REMOTE_PROTOCOL_INFORMATION fileInformation,
            int bufferSize
            );
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern IntPtr CreateFileW(
            [MarshalAs(UnmanagedType.LPWStr)]
            string lpFileName,
            uint dwDesiredAccess,
            uint dwShareMode,
            IntPtr lpSecurityAttributes,
            uint dwCreationDisposition,
            uint dwFlagsAndAttributes,
            IntPtr hTemplateFile
            );

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
        internal static extern int CloseHandle(IntPtr handle);


        internal const uint REMOTE_PROTOCOL_FLAG_LOOPBACK = 0x00000001;

        internal const uint REMOTE_PROTOCOL_FLAG_OFFLINE = 0x00000002;

        internal const uint ERROR_MORE_DATA = 234;
    }

    internal partial class NativeWrapped
    {
        public static string VerifyVolumeIndicator(string str)
        {
            if (str.StartsWith(@"\\?\")){
                return str;
            }
            else if (str.StartsWith(@"\?\")){
                return @"\" + str;
            }
            else if (str.StartsWith(@"?\")){
                return @"\\" + str;
            }
            else if (str.StartsWith(@"\")){
                return @"\\?" + str;
            }
            return @"\\?\" + str;
        }

        public static string RemoveVolumeIndicator(string str)
        {
            if (str.StartsWith(@"\\?\")){
                return str.Substring(4);
            }
            else return str;
        }
        public static string DeviceFromVolume(string volume)
        {
            string v = RemoveVolumeIndicator(volume); //Take off the \\?\
            if (v.EndsWith("\\"))
            {
                v = v.Substring(0, v.Length - 1); //Remove the trailing slash if present
            }
            StringBuilder sb = new StringBuilder(5000);
            if (NativeMethods.QueryDosDeviceW(v, sb, 5000) == 0) 
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            return sb.ToString();
        }

        public static string GetFinalPathNameByHandle(IntPtr hFile, int flags = 0, bool throwOnError = true)
        {
            StringBuilder sb = new StringBuilder(10000);
            if (NativeMethods.GetFinalPathNameByHandleW(hFile, sb, 10000, flags) == 0){
                if (throwOnError) ThrowLastError();
            }
            return sb.ToString();
        }
        internal static void ThrowLastError()
        {
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        }
    }


    public static class FileInfoExtension
    {
        //Gets remote protocol information for a FileInfo instance
        public static FILE_REMOTE_PROTOCOL_INFORMATION GetRemoteProtocolInformation(this FileInfo fi)
        {
            //FileStream gives access to the handle
            using (FileStream fs = fi.OpenRead()){
                FILE_REMOTE_PROTOCOL_INFORMATION f = new FILE_REMOTE_PROTOCOL_INFORMATION();
                f.StructureVersion = (Environment.OSVersion.Version.Major >= 6 && 
                    Environment.OSVersion.Version.Minor >= 2) ? (ushort)2 : (ushort)1;
                f.StructureSize = (ushort)Marshal.SizeOf(f);
                if (NativeMethods.GetFileInformationByHandleEx(fs.SafeFileHandle.DangerousGetHandle(),
                    (int)FileInformationClassEnum.FileRemoteProtocolInfo,
                    ref f,
                    f.StructureSize) == 0){
                    NativeWrapped.ThrowLastError();
                }
                return f;
            }
        }
        //Gets remote protocol information for a FileInfo instance and returns the error code
        public static int TryGetRemoteProtocolInformation(this FileInfo fi, out FILE_REMOTE_PROTOCOL_INFORMATION f)
        {
            //FileStream gives access to the handle
            using (FileStream fs = fi.OpenRead()){
                f = new FILE_REMOTE_PROTOCOL_INFORMATION();
                f.StructureVersion = (Environment.OSVersion.Version.Major >= 6 
                    && Environment.OSVersion.Version.Minor >= 2) ? (ushort)2 : (ushort)1;
                f.StructureSize = (ushort)Marshal.SizeOf(f);
                if (NativeMethods.GetFileInformationByHandleEx(fs.SafeFileHandle.DangerousGetHandle(),
                    (int)FileInformationClassEnum.FileRemoteProtocolInfo,
                    ref f,
                    f.StructureSize) == 0)
                {
                    int lastError = Marshal.GetLastWin32Error();
                    return lastError;
                }
            }
            return 0;
        }
        //Gets the final path name for a FileInfo instance
        public static string FinalPathName(this FileInfo fi)
        {
            using (FileStream fs = fi.OpenRead()){
                return NativeWrapped.GetFinalPathNameByHandle(fs.SafeFileHandle.DangerousGetHandle());
            }
        }
        //Short and simple way to determine if the final path is UNC
        public static bool FinalPathNameIsUNC(this FileInfo fi)
        {
            string finalPathName = FinalPathName(fi);
            if (!string.IsNullOrEmpty(finalPathName))
            {
                if (finalPathName.StartsWith(@"\\?\UNC\"))
                {
                    return true;
                }
            }
            return false;
        }
        //Returns a DriveInfo instance for a FileInfo instance if one can be obtained, otherwise returns null
        public static DriveInfo GetDriveInfo(this FileInfo fi)
        {
            //Final Path without the volume indicator
            string finalPath = FinalPathName(fi);
            //Remove the volume indicator if not null
            if (!string.IsNullOrEmpty(finalPath))
            {
                finalPath = NativeWrapped.RemoveVolumeIndicator(finalPath);
            }
            //Extract the drive letter if possible
            if (!string.IsNullOrEmpty(finalPath))
            {
                if (finalPath.Length > 1)
                {
                    //e.g. C:, D:, etc.
                    if (finalPath.Substring(1, 1).Equals(":"))
                    {
                        //DriveInfo wants just the drive letter
                        return new DriveInfo(finalPath.Substring(0, 1));
                    }
                }
            }
            return null;
        }
        //Determines if the FileInfo instance is from a local path
        public static bool IsRemote(this FileInfo fi, out bool offLine, 
            bool includeLoopbackAsLocal=false, bool verifyRemoteProtocolInfo=true)
        {
            offLine = false;
            //Try to get System.IO.DriveInfo first
            var driveInfo = GetDriveInfo(fi);
            if(driveInfo != null)
            {
                if(driveInfo.DriveType == DriveType.Network)
                {
                    if (!verifyRemoteProtocolInfo && !includeLoopbackAsLocal) return true;
                    FILE_REMOTE_PROTOCOL_INFORMATION frpi;
                    int error = TryGetRemoteProtocolInformation(fi, out frpi);
                    if (error == 0)
                    {
                        offLine = ((frpi.Flags & NativeMethods.REMOTE_PROTOCOL_FLAG_OFFLINE) 
                                     == NativeMethods.REMOTE_PROTOCOL_FLAG_OFFLINE);
                        //Successfull
                        if (includeLoopbackAsLocal)
                        {
                            return ((frpi.Flags & NativeMethods.REMOTE_PROTOCOL_FLAG_LOOPBACK) 
                                     != NativeMethods.REMOTE_PROTOCOL_FLAG_LOOPBACK);
                        }
                        else
                        {
                            return true;
                        }
                    }
                    else if (error == 87) //Invalid parameter
                    {
                        if (verifyRemoteProtocolInfo)
                        {
                            return false;
                        }
                        else return true;
                    }
                    else
                    {
                        NativeWrapped.ThrowLastError();
                    }
                }
            }
            else
            {
                //Failed to get the DriveInfo object
                FILE_REMOTE_PROTOCOL_INFORMATION frpi;
                int error = TryGetRemoteProtocolInformation(fi, out frpi);
                if (error == 0)
                {
                    offLine = ((frpi.Flags & NativeMethods.REMOTE_PROTOCOL_FLAG_OFFLINE) 
                                   == NativeMethods.REMOTE_PROTOCOL_FLAG_OFFLINE);
                    //Successfull
                    if (includeLoopbackAsLocal)
                    {
                        return ((frpi.Flags & NativeMethods.REMOTE_PROTOCOL_FLAG_LOOPBACK) 
                                   != NativeMethods.REMOTE_PROTOCOL_FLAG_LOOPBACK);
                    }
                    else
                    {
                        return true;
                    }
                }
                else
                {
                    NativeWrapped.ThrowLastError();
                }
            }
            return false;
        }
        //Determines if the FileInfo instance is from a local path
        public static bool IsRemote(this FileInfo fi, bool includeLoopbackAsLocal=false, 
            bool verifyRemoteProtocolInfo=true)
        {
            bool offLine;
            return IsRemote(fi, out offLine, includeLoopbackAsLocal, verifyRemoteProtocolInfo);
        }
    }

    class Program
    {
        static void WriteInfo(string filename)
        {
            FileInfo fi = new FileInfo(filename);
            Console.WriteLine("Filename:\t{0}", filename);
            Console.WriteLine("Final Path Name:\t{0}", fi.FinalPathName());
            if (!fi.IsRemote())
            {
                DriveInfo di = fi.GetDriveInfo();
                if (di != null)
                {
                    if (di.IsReady)
                    {
                        Console.WriteLine("Root Directory:\t{0}", di.RootDirectory);
                        Console.WriteLine("Drive Type:\t{0}", di.DriveType.ToString());
                        Console.WriteLine("Drive Format:\t{0}", di.DriveFormat);
                        if (!di.DriveType.Equals(DriveType.Network))
                        {
                            return;
                        }
                    }
                    else
                    {
                        Console.WriteLine("Drive not ready");
                    }
                }
            }
            var f = fi.GetRemoteProtocolInformation();
            Console.WriteLine("Remote Protocol Information:");
            Console.WriteLine("Protocol:\t{0} v{1}.{2}", f.Protocol.ToString(), 
                f.ProtocolMajorVersion, f.ProtocolMinorVersion);
            Console.WriteLine("Loopback:\t{0}", ((f.Flags & NativeMethods.REMOTE_PROTOCOL_FLAG_LOOPBACK) 
                == NativeMethods.REMOTE_PROTOCOL_FLAG_LOOPBACK));
            Console.WriteLine("Offline:\t{0}", ((f.Flags & NativeMethods.REMOTE_PROTOCOL_FLAG_OFFLINE) 
            == NativeMethods.REMOTE_PROTOCOL_FLAG_OFFLINE));
        }
        static void Main(string[] args)
        {
            //notepad
            string filePath1 = Path.Combine(Environment.SystemDirectory, "notepad.exe");
            //Admin cache notepad...needs elevation for this (or just use another path that doesn't require elevation)
            string filePath2 = string.Format(@"\\{0}\{1}", Environment.MachineName, filePath1.Replace(@":\", @"$\"));
            WriteInfo(filePath1);
            Console.WriteLine();
            WriteInfo(filePath2);
            Console.ReadLine();
        }
    }
}

Sample output from a laptop named cllptop01 running Windows 8.1 with the code compiled to x86:

Filename:       C:\Windows\system32\notepad.exe

Final Path Name:        \\?\C:\Windows\SysWOW64\notepad.exe

Root Directory: C:\

Drive Type:     Fixed

Drive Format:   NTFS

Filename:       \\CLLPTOP01\C$\Windows\system32\notepad.exe

Final Path Name:        \\?\UNC\CLLPTOP01\C$\Windows\System32\notepad.exe

Remote Protocol Information:

Protocol:       SMB v3.0

Loopback:       True

Offline:        False

Follow us on Twitter, www.twitter.com/WindowsSDK.