この記事では C# でファイルがロックされているかどうか調べる方法を紹介する. なお,本記事の言語バージョンは C# 11以降を前提としている.
FileStreamインスタンス生成時のIOExceptionによって調べる
通常の C# の範疇では FileStream でファイルを開こうとしたときに IOException が発生するかどうかによって判別するしかない.
下記コード中の
IsReadLocked(): 読み込みロックされているかどうかIsWriteLocked(): 書き込みロックされているかどうか
を調べるメソッドになっている.
using System.IO; #if !NETFRAMEWORK && !WINDOWS using System.Runtime.InteropServices; #endif // !NETFRAMEWORK && !WINDOWS namespace Koturn { /// <summary> /// Provides utility methods to check whether a file is locked or not. /// </summary> public static class FileLockCheckUtils { #if NETFRAMEWORK || WINDOWS /// <summary> /// A flag whether current running platform is Windows or not. /// </summary> private const bool _isWindows = true; #else /// <summary> /// A flag whether current running platform is Windows or not. /// </summary> private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); #endif // NETFRAMEWORK || WINDOWS /// <summary> /// Determine whether specified file is read-locked or not. /// </summary> /// <param name="filePath">File path to determine.</param> /// <returns>true if specified file is read-locked, otherwise false.</returns> public static bool IsReadLocked(string filePath) { return IsLockedCore(filePath, FileAccess.Read); } /// <summary> /// Determine whether specified file is write-locked or not. /// </summary> /// <param name="filePath">File path to determine.</param> /// <returns>true if specified file is write-locked, otherwise false.</returns> public static bool IsWriteLocked(string filePath) { return IsLockedCore(filePath, FileAccess.Write); } /// <summary> /// Determine whether specified file is read/write-locked or not. /// </summary> /// <param name="filePath">File path to determine.</param> /// <returns>true if specified file is write-locked, otherwise false.</returns> /// <remarks> /// <seealso href="https://learn.microsoft.com/en-us/dotnet/standard/io/handling-io-errors"/> /// </remarks> private static bool IsLockedCore(string filePath, FileAccess access) { const int ErrorSharingViolation = 0x00000020; #if NETCOREAPP1_0_OR_GREATER // .NET: Buffer size must be greater than or equal to 0. const int BufferSize = 0; #else // .NET Framework or .NET Standard: Buffer size must be greater than 0; 0 is not allowed. const int BufferSize = 1; #endif // NETCOREAPP1_0_OR_GREATER if (!File.Exists(filePath)) { throw new FileNotFoundException(filePath); } FileStream? fs = null; try { if ((access & FileAccess.Write) != 0) { // Try to open for write. fs = new FileStream(filePath, FileMode.Open, FileAccess.Write, FileShare.Read, BufferSize); } else { // Try to open for read. fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Write, BufferSize); } } catch (IOException ex) { // Assume that VRChat process owns the log file. if (_isWindows && (ex.HResult & 0x0000ffff) != ErrorSharingViolation) { throw; } return true; } finally { fs?.Dispose(); } return false; } } }
重要なのが FileStream の FileShare を指定している第四引数である.
ここで 指定されなかった属性 は共有を許さないものとし,その読み込みまたは書き込み属性のロックを取得しにいく.
しかし,先んじて他アプリケーション,あるいは自身の処理で対象ファイルがその属性の共有を許可しなかった場合に IOException が発生する.
一口に IOException といっても,様々な理由で発生する可能性がある.
そこで HResult プロパティの値を確認すれば,発生理由を調べることが可能である.
MSDNに記載されているように,下位4バイトが32となっていれば共有違反が原因だとわかる.
ただし,Windows以外の環境だとHResultの値については上記の通りになっていないと考えられる.
事前にWindowsか否かを判定しておき,Windows以外であれば IOException 発生時は共有違反として扱うものとする(これでよいかどうか未調査).
とはいえ,.NET FrameworkやWindowsターゲットのバイナリである場合は,実行時にOSの判定を行う必要はないため,コンパイル時に NETFRAMEWORK, WINDOWS マクロの定義有無によってWindowsか否かを判定している.
余談であるが, FileStream のコンストラクタで指定できるバッファサイズは .NET Framework か .NET かによって許容される値が下記のように異なっている.
- .NET Framework: 1以上でなければならない(0は例外が発生する)
- .NET: 0以上であればOK
そのため,.NET であればバッファサイズは0を,そうでなければ(.NET Standardも)1を指定するようにした.
これは,バッファサイズ0であれば内部的に Array.Empty<byte>() が使用されることを期待しているためである.
Win32 APIを用いて調べる
ファイルがロックされているかどうかを調べるためだけに例外を発生させるのは大袈裟感が否めない. 例外送出はとてもコストの高い処理であるため,可能であれば単純な判定で済ませたいものである.
.NET Frameworkにおける FileStream の実装を追えばわかるように,最後には CreateFile() 関数に行き着く.
すなわち,Win32 APIの CreateFile() を用いて判定することで例外を発生させることなくファイルがロックされているかどうかがわかる.
Windowsであれば CreateFile() を用いてロック判定を行うように,最初に提示したコードを書き直すと下記のようになる.
#if NET7_0_OR_GREATER # define SUPPORT_LIBRARY_IMPORT #endif // NET7_0_OR_GREATER using System.IO; using System.Runtime.InteropServices; #if NETFRAMEWORK || WINDOWS using System; using Microsoft.Win32.SafeHandles; #if !NET7_0_OR_GREATER using System.Security; #endif // !NET7_0_OR_GREATER #endif // NETFRAMEWORK || WINDOWS namespace Koturn { /// <summary> /// Provides utility methods to check whether a file is locked or not. /// </summary> #if SUPPORT_LIBRARY_IMPORT && WINDOWS public static partial class FileLockCheckUtils #else public static class FileLockCheckUtils #endif // SUPPORT_LIBRARY_IMPORT && WINDOWS { #if !NETFRAMEWORK && !WINDOWS /// <summary> /// A flag whether current running platform is Windows or not. /// </summary> private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); #endif // !NETFRAMEWORK && !WINDOWS /// <summary> /// Determine whether specified file is read-locked or not. /// </summary> /// <param name="filePath">File path to determine.</param> /// <returns>true if specified file is read-locked, otherwise false.</returns> public static bool IsReadLocked(string filePath) { return IsLockedCore(filePath, FileAccess.Read); } /// <summary> /// Determine whether specified file is write-locked or not. /// </summary> /// <param name="filePath">File path to determine.</param> /// <returns>true if specified file is write-locked, otherwise false.</returns> public static bool IsWriteLocked(string filePath) { return IsLockedCore(filePath, FileAccess.Write); } /// <summary> /// Determine whether specified file is read/write-locked or not. /// </summary> /// <param name="filePath">File path to determine.</param> /// <returns>true if specified file is write-locked, otherwise false.</returns> /// <remarks> /// <seealso href="https://learn.microsoft.com/en-us/dotnet/standard/io/handling-io-errors"/> /// </remarks> private static bool IsLockedCore(string filePath, FileAccess access) { const int ErrorSharingViolation = 0x00000020; if (!File.Exists(filePath)) { throw new FileNotFoundException(filePath); } #if NETFRAMEWORK || WINDOWS // Try to open for write. const nint invalidHandleValue = -1; using (var hFile = (access & FileAccess.Write) != 0 ? SafeNativeMethods.CreateFile(filePath, GenericAccessRights.Write, FileShare.Read, IntPtr.Zero, FileMode.Open) : SafeNativeMethods.CreateFile(filePath, GenericAccessRights.Read, FileShare.Write, IntPtr.Zero, FileMode.Open)) { return hFile.DangerousGetHandle() == invalidHandleValue && Marshal.GetLastWin32Error() == ErrorSharingViolation; } #else #if NETCOREAPP1_0_OR_GREATER // .NET: Buffer size must be greater than or equal to 0. const int BufferSize = 0; #else // .NET Framework or .NET Standard: Buffer size must be greater than 0; 0 is not allowed. const int BufferSize = 1; #endif // NETCOREAPP1_0_OR_GREATER FileStream? fs = null; try { if ((access & FileAccess.Write) != 0) { // Try to open for write. fs = new FileStream(filePath, FileMode.Open, FileAccess.Write, FileShare.Read, BufferSize); } else { // Try to open for read. fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Write, BufferSize); } } catch (IOException ex) { // Assume that VRChat process owns the log file. if (_isWindows && (ex.HResult & 0x0000ffff) != ErrorSharingViolation) { throw; } return true; } finally { fs?.Dispose(); } return false; #endif } #if NETFRAMEWORK || WINDOWS [Flags] internal enum GenericAccessRights : uint { All = 0x10000000, Execute = 0x20000000, Write = 0x40000000, Read = 0x80000000, } [Flags] internal enum FileFlagAndAttributes : uint { AttrReadonly = 0x00000001, AttrHidden = 0x00000002, AttrSystem = 0x00000004, AttrArchive = 0x00000020, AttrNormal = 0x00000080, AttrTemporary = 0x00000100, AttrOffline = 0x00001000, AttrEncrypted = 0x00004000, SecurityAnonymous = 0x00000000, SecurityIdentification = 0x00010000, SecurityImpersonation = 0x00020000, SecurityDelegation = 0x00030000, SecurityContextTracking = 0x00040000, SecurityEffectiveOnly = 0x00080000, FlagOpenNoRecall = 0x00100000, FlagOpenReparsePoint = 0x00200000, FlagSessionAware = 0x00800000, FlagPosixSemantics = 0x01000000, FlagBackupSemantics = 0x02000000, FlagDeleteOnClose = 0x04000000, FlagSequentialScan = 0x08000000, FlagRandomAccess = 0x10000000, FlagNoBuffering = 0x20000000, FlagOverlapped = 0x40000000, FlagWriteThrough = 0x80000000 } /// <summary> /// Provides native methods. /// </summary> #if SUPPORT_LIBRARY_IMPORT private static partial class SafeNativeMethods #else [SuppressUnmanagedCodeSecurity] private static class SafeNativeMethods #endif // SUPPORT_LIBRARY_IMPORT { #if SUPPORT_LIBRARY_IMPORT [LibraryImport("kernel32.dll", EntryPoint = nameof(CreateFile) + "W", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)] public static partial SafeFileHandle CreateFile( string fileName, GenericAccessRights desiredAccess, FileShare shareMode, IntPtr pSecurityAttributes, FileMode creationDisposition, FileFlagAndAttributes flagsAndAttributes = FileFlagAndAttributes.AttrNormal, IntPtr hTemplateFile = default); #else [DllImport("kernel32.dll", EntryPoint = nameof(CreateFile) + "W", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] public static extern SafeFileHandle CreateFile( string fileName, GenericAccessRights desiredAccess, FileShare shareMode, IntPtr pSecurityAttributes, FileMode creationDisposition, FileFlagAndAttributes flagsAndAttributes = FileFlagAndAttributes.AttrNormal, IntPtr hTemplateFile = default); #endif // SUPPORT_LIBRARY_IMPORT } #endif // NETFRAMEWORK || WINDOWS } }