FileSystemWatcher locks parent folder, is there a fix or an alternative? - c#

Whether in .NET Core or .NET Framework, the use of a FileSystemWatcher on a folder makes it impossible to delete any parent folder with File Explorer.
File Explorer will ask you for admin rights. Launching File Explorer as admin doesn't fix the issue.
The only way that I can make it work is to set the FileSystemWatcher to the root drive, and include subdirectories, which could lead to big performance issues, and a buffer overflow on the FSW.
FileSystemWatcher fsw = new FileSystemWatcher(#"C:\One\Two\Three");
fsw.EnableRaisingEvents = true;
You will not be able to delete folders 'One' or 'Two'.
The problem starts as soon as set 'EnableRaisingEvents' to true, but if you don't, you will not get any notification.
Is there a way to overcome this problem, or an alternative to FileSystemWatcher?

#EricP gave the root cause: The FSW internally makes a call to CreateFileW to open a handle to the monitored directory Three, which tells Windows to prevent deleting the parent folders One and Two.
But the CreateFileW call is made with read+write+delete sharing mode, meaning Windows should allow any other processes to delete the monitored folder Three.
So there's no direct fix for your problem, but there is a workaround:
Delete Three first, which should release the handle, then delete Two and/or One, which should be allowed.
Here's a code sample reproing this without any FileSystemWatcher code, just direct Windows P/Invokes:
using Microsoft.Win32.SafeHandles;
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace MyNamespace
{
public static class MyClass
{
static void Main()
{
string path = #"C:\Users\YOURUSERNAME\Desktop\parent\child";
if (!Directory.Exists(path))
{
throw new Exception("Make sure to create the directories before testing.");
}
// This will lock the directory
SafeFileHandle _directoryHandle = CreateFile(
lpFileName: path,
dwDesiredAccess: 0x0001, //FILE_LIST_DIRECTORY
dwShareMode: FileShare.Read | FileShare.Delete | FileShare.Write,
dwCreationDisposition: FileMode.Open,
dwFlagsAndAttributes:
0x02000000 /*FILE_FLAG_BACKUP_SEMANTICS*/ |
0x40000000 /*FILE_FLAG_OVERLAPPED*/
);
Console.WriteLine("Try to manually delete the 'parent' directory using File Explorer. It should fail.");
Console.WriteLine("Now try to manually delete the 'child' directory, then the 'parent' directory, using File Explorer. It should succeed.");
Console.WriteLine("Press enter to exit.");
System.Diagnostics.Debugger.Break();
_directoryHandle.Dispose();
}
internal enum BOOL : int
{
FALSE = 0,
TRUE = 1,
}
[StructLayout(LayoutKind.Sequential)]
internal struct SECURITY_ATTRIBUTES
{
internal uint nLength;
internal IntPtr lpSecurityDescriptor;
internal BOOL bInheritHandle;
}
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false, ExactSpelling = true)]
private static extern unsafe SafeFileHandle CreateFilePrivate(
string lpFileName,
int dwDesiredAccess,
FileShare dwShareMode,
SECURITY_ATTRIBUTES* lpSecurityAttributes,
FileMode dwCreationDisposition,
int dwFlagsAndAttributes,
IntPtr hTemplateFile);
internal static unsafe SafeFileHandle CreateFile(
string lpFileName,
int dwDesiredAccess,
FileShare dwShareMode,
FileMode dwCreationDisposition,
int dwFlagsAndAttributes)
{
return CreateFilePrivate(lpFileName, dwDesiredAccess, dwShareMode, null, dwCreationDisposition, dwFlagsAndAttributes, IntPtr.Zero);
}
}
}

Related

How to get the target location of a Junction if the target directory does not exist?

I have a junction from A -> B.
B does not exist, but I want to read from A the target of the junction (B).
However I can't make this work (filename = A):
// directoryHandle.isInvalid == true
var directoryHandle = CreateFile(filename, EFileAccess.GenericRead, EFileShare.Read | EFileShare.Write, IntPtr.Zero, ECreationDisposition.OpenExisting, EFileAttributes.BackupSemantics, IntPtr.Zero);
StringBuilder path = new StringBuilder(1024);
// path is empty string
var res = GetFinalPathNameByHandle(directoryHandle, path, path.Capacity, 0);
GetFinalPathNameByHandle returns the location of the junction if B exists. But if B does not exist, I receive an invalid file handle so that the call to GetFinalPathNameByHandle also fails.
Question: How do I get the target of a junction if the target directory does not exist?
Here are the method definitions:
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern SafeFileHandle CreateFile(
string lpFileName,
EFileAccess dwDesiredAccess,
EFileShare dwShareMode,
IntPtr lpSecurityAttributes,
ECreationDisposition dwCreationDisposition,
EFileAttributes dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern int GetFinalPathNameByHandle([In] SafeFileHandle hFile, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder path, int bufLen, int flags);
Ask the junction directly.
To determine the type, use FindFirstFile (or GetFileInformationByHandleEx(FileAttributeTagInfo) if you already have a handle). The FILE_ATTRIBUTE_REPARSE_POINT bit must be set in the file attributes and the tag must be IO_REPARSE_TAG_MOUNT_POINT or IO_REPARSE_TAG_SYMLINK.
If all checks pass, call CreateFile with the FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS flags. Now call DeviceIoControl(FSCTL_GET_REPARSE_POINT) to read the REPARSE_DATA_BUFFER (allocate MAXIMUM_REPARSE_DATA_BUFFER_SIZE bytes for it). Look at the tag type to find PathBuffer and SubstituteNameOffset+SubstituteNameLength to find where in the buffer the path is.
You can find C# example code on Codeproject...

How to open volume by name

I need to open volume by name(get volume handle). The target volume name is "\?\Volume{A25CF44F-8CB3-46E7-B3A7-931385FDF8CB}\" and this should works. According to CreateFile function
You can also open a volume by referring to its volume name.
The C# code:
public static class Wrapper
{
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern SafeFileHandle CreateFile(
[MarshalAs(UnmanagedType.LPTStr)] string filename,
[MarshalAs(UnmanagedType.U4)] FileAccess access,
[MarshalAs(UnmanagedType.U4)] FileShare share,
IntPtr securityAttributes, // optional SECURITY_ATTRIBUTES struct or IntPtr.Zero
[MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
[MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
IntPtr templateFile);
}
static void Main(string[] args)
{
string sourceVol = #"\\?\Volume{A25CF44F-8CB3-46E7-B3A7-931385FDF8CB}\";
SafeFileHandle sourceVolHandle = Wrapper.CreateFile(sourceVol, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
if (sourceVolHandle.IsInvalid)
throw new Win32Exception(); //Here I got "The system cannot find the path specified"
So how to open volume by name? (I know that I can open volume using drive letter "\\.\C:" but that is not accepteble)
To open volume we need to remove trailing slash, so:
string sourceVol = #"\\?\Volume{A25CF44F-8CB3-46E7-B3A7-931385FDF8CB}";

Determine whether destination for file move is on same filesystem in C#

I have a Windows Forms application which uses File.Move to move files in response to user request. This is not an asynchronous method, but when the destination is on the same drive it happens almost instantly (I assume because it can just rewrite the header and not move anything). However, it blocks the application if the destination is not on the same drive. Not desirable.
So naturally I thought about switching to async. It looks like the way to do this is to use file streams and use Stream.CopyToAsync before removing the original. That's fine, except if it is on the same drive, this is a lot of wasteful copying.
Is there a reliable way to determine if a proposed destination for a file is on a different drive than that drive is currently stored on before deciding which operation to perform? I could obviously look at the drive letter, but that seems kind of hacky and I think there are some cases like junction points where it will not work correctly.
Another possible choice is to use something like Task.Run(() => File.Move(x)), although I have a vaguely bad feeling about that.
I think the best effort for this is going to be comparing volume serial numbers. GetFileInformationByHandle() is able to get the volume serial number of a file handle. This is definitely a YMMV for RAID / striped volumes, so if those apply, I would test this solution for the performance. It does work for junction points.
The DllImport for it is
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetFileInformationByHandle(
IntPtr hFile,
out BY_HANDLE_FILE_INFORMATION lpFileInformation);
We also need a way to get suitable file handles to use. Since we're working with potential destinations, if we assume the destination directory already exists, we can use CreateFile() to get a directory handle.
For CreateFile(), the DllImport is
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CreateFile(
[MarshalAs(UnmanagedType.LPTStr)] string filename,
[MarshalAs(UnmanagedType.U4)] FileAccess access,
[MarshalAs(UnmanagedType.U4)] FileShare share,
IntPtr securityAttributes,
[MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
[MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
IntPtr templateFile);
We'll also need CloseHandle(),
[DllImport("kernel32.dll", SetLastError = true)]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
With all those pieces in place, create a wrapper method that returns true if the volume serial number of the supplied paths match. It obviously makes sense throw more specific exceptions, but for the sake of brevity:
static bool SameVolume(string src, string dest)
{
IntPtr srcHandle = IntPtr.Zero, destHandle = IntPtr.Zero;
try
{
srcHandle = CreateFile(src, FileAccess.Read, FileShare.ReadWrite, IntPtr.Zero,
FileMode.Open, (FileAttributes) 0x02000000, IntPtr.Zero);
destHandle = CreateFile(dest, FileAccess.Read, FileShare.ReadWrite, IntPtr.Zero,
FileMode.Open, (FileAttributes) 0x02000000, IntPtr.Zero);
var srcInfo = new BY_HANDLE_FILE_INFORMATION();
var destInfo = new BY_HANDLE_FILE_INFORMATION();
if (!GetFileInformationByHandle(srcHandle, out srcInfo))
{
throw new Exception(Marshal.GetLastWin32Error().ToString());
}
if (!GetFileInformationByHandle(destHandle, out destInfo))
{
throw new Exception(Marshal.GetLastWin32Error().ToString());
}
return srcInfo.VolumeSerialNumber == destInfo.VolumeSerialNumber;
}
finally
{
if (srcHandle != IntPtr.Zero)
{
CloseHandle(srcHandle);
}
if (destHandle != IntPtr.Zero)
{
CloseHandle(destHandle);
}
}
}
Alternatively, you could just use
Task.Run(() => File.Move(x))
or another asynchronous construct to get this done without locking up the application's interface. I can't see anything wrong with that.

Get Size of Disk and/or Drive With Unknown File Format, C# .NET Framework?

I want to be able to programmatically get the size of a physical disk, or the logical drives that are part of the physical disk.
Under normal conditions, getting logical drive size is easy. But, for the disk I am using, I intentionally have written raw data to it, and thus it is expected to have no known drive format.
Because of the drive format being unknown to Windows, Windows drive properties and .NET Framework's DriveInfo cannot tell me the size of the logical drive(s) that make up this physical disk. In DriveInfo foo, by format I mean foo.DriveFormat, foo.IsReady is false, and foo.TotalSize triggers an exception. In just right clicking the drive and getting the size manually via Windows' properties option, it shows 0 bytes for the drive size.
Preferably using C#, using .NET Framework 4.0 or earlier, on a Windows 7 system, how do I learn the size of a physical disk, or the logical drives that make up the physical disk, if the logical drives associated with it have no known file structure?
Specifically physical disk size is preferable for my purposes.
I know there is a way in general using Windows 7, as I have seen applications give an accurate physical disk size of the same disk.
The solution ended up being fairly simple, and an extension from the existing question Getting Disk Size Properly
Posting my solution here, as perhaps the full code will help someone in the future:
(FYI: P/Invoke definitions obtained and minimally modified from http://pinvoke.net)
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CreateFile(
[MarshalAs(UnmanagedType.LPTStr)] string filename,
[MarshalAs(UnmanagedType.U4)] FileAccess access,
[MarshalAs(UnmanagedType.U4)] FileShare share,
IntPtr securityAttributes, // optional SECURITY_ATTRIBUTES struct or IntPtr.Zero
[MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
[MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
IntPtr templateFile);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern bool DeviceIoControl(IntPtr hDevice, uint dwIoControlCode,
IntPtr lpInBuffer, uint nInBufferSize,
IntPtr lpOutBuffer, uint nOutBufferSize,
out uint lpBytesReturned, IntPtr lpOverlapped);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr hObject);
struct GET_LENGTH_INFORMATION
{
public long Length;
};
long GetPhysDiskSize(string physDeviceID)
{
uint IOCTL_DISK_GET_LENGTH_INFO = 0x0007405C;
uint dwBytesReturned;
//Example, physDeviceID == #"\\.\PHYSICALDRIVE1"
IntPtr hVolume = CreateFile(physDeviceID, FileAccess.ReadWrite,
FileShare.None, IntPtr.Zero, FileMode.Open, FileAttributes.Normal, IntPtr.Zero);
GET_LENGTH_INFORMATION outputInfo = new GET_LENGTH_INFORMATION();
outputInfo.Length = 0;
IntPtr outBuff = Marshal.AllocHGlobal(Marshal.SizeOf(outputInfo));
bool devIOPass = DeviceIoControl(hVolume,
IOCTL_DISK_GET_LENGTH_INFO,
IntPtr.Zero, 0,
outBuff, (uint)Marshal.SizeOf(outputInfo),
out dwBytesReturned,
IntPtr.Zero);
CloseHandle(hVolume);
outputInfo = (GET_LENGTH_INFORMATION)Marshal.PtrToStructure(outBuff, typeof(GET_LENGTH_INFORMATION));
Marshal.FreeHGlobal(hVolume);
Marshal.FreeHGlobal(outBuff);
return outputInfo.Length;
}

PathTooLongException C# 4.5

I having trouble of copying some folder 260+ chars (for example: F:\NNNNNNNNNNNNNNNN\NNNNNNNNNNN\ROOT\$RECYCLE.BIN\S-1-5-21-3299053755-4209892151-505108915-1000\$RMSL3U8\NNNNNNNNN NNNNNNNN\NNNNNNNNNNN\NNNNNNNNNN\NNNNNNNNNN\publish\Application Files\TNNNNNNNNNNNN_1_0_0_0\NNNNNNNNNNNN.exe.manifest) to some other place with standart DrectoryInfo.Create(); adding \?\ or \?\UNC\ (like "\\?\UNC\") just throw another ArgumentException.
What am i doing wrong? What else i can do without using Directory.SetCurrentDirectory() ?
Actually you need to call win32 from c#. We have done this
using System;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
public static class LongPath
{
static class Win32Native
{
[StructLayout(LayoutKind.Sequential)]
public class SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr pSecurityDescriptor;
public int bInheritHandle;
}
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool CreateDirectory(string lpPathName, SECURITY_ATTRIBUTES lpSecurityAttributes);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, FileShare dwShareMode, SECURITY_ATTRIBUTES securityAttrs, FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
}
public static bool CreateDirectory(string path)
{
return Win32Native.CreateDirectory(String.Concat(#"\\?\", path), null);
}
public static FileStream Open(string path, FileMode mode, FileAccess access)
{
SafeFileHandle handle = Win32Native.CreateFile(String.Concat(#"\\?\", path), (int)0x10000000, FileShare.None, null, mode, (int)0x00000080, IntPtr.Zero);
if (handle.IsInvalid)
{
throw new System.ComponentModel.Win32Exception();
}
return new FileStream(handle, access);
}
}
A sample code:
string path = #"c:\".PadRight(255, 'a');
LongPath.CreateDirectory(path);
path = String.Concat(path, #"\", "".PadRight(255, 'a'));
LongPath.CreateDirectory(path);
string filename = Path.Combine(path, "test.txt");
FileStream fs = LongPath.Open(filename, FileMode.CreateNew, FileAccess.Write);
using (StreamWriter sw = new StreamWriter(fs))
{
sw.WriteLine("abc");
}
There's a great library on Microsoft TechNet for overcoming the long filenames problem, it's called Delimon.Win32.I​O Library (V4.0) and it has its own versions of key methods from System.IO
For example, you would replace:
System.IO.Directory.GetFiles
with
Delimon.Win32.IO.Directory.GetFiles
which will let you handle long files and folders.
From the website:
Delimon.Win32.IO replaces basic file functions of System.IO and
supports File & Folder names up to up to 32,767 Characters.
This Library is written on .NET Framework 4.0 and can be used either
on x86 & x64 systems. The File & Folder limitations of the standard
System.IO namespace can work with files that have 260 characters in a
filename and 240 characters in a folder name (MAX_PATH is usually
configured as 260 characters). Typically you run into the
System.IO.PathTooLongException Error with the Standard .NET Library.
Yes, using the standard APIs will give you this kind of limitations (255 chars IIRC).
From .NET you can use the AlphaFS project which lets you use very long paths (using the "\\?\" style) and mimics the System.IO namespace.
You will probably be able to use this library just as if you were using System.IO, for example : AlphaFS.Win32.Filesystem.File.Copy() instead System.IO.File.Copy()
If you don't want or cannot use AlphaFS you'll have to pinvoke the Win32 API

Categories

Resources