diff --git a/spec/std/file_spec.cr b/spec/std/file_spec.cr index a393049f596a..e9270a48b483 100644 --- a/spec/std/file_spec.cr +++ b/spec/std/file_spec.cr @@ -933,22 +933,21 @@ describe "File" do end end - # TODO: implement flock on windows describe "flock" do - pending_win32 "exclusively locks a file" do + it "#flock_exclusive" do File.open(datapath("test_file.txt")) do |file1| File.open(datapath("test_file.txt")) do |file2| file1.flock_exclusive do exc = expect_raises(IO::Error, "Error applying file lock: file is already locked") do file2.flock_exclusive(blocking: false) { } end - exc.os_error.should eq Errno::EWOULDBLOCK + exc.os_error.should eq({% if flag?(:win32) %}WinError::ERROR_LOCK_VIOLATION{% else %}Errno::EWOULDBLOCK{% end %}) end end end end - pending_win32 "shared locks a file" do + it "#flock_shared" do File.open(datapath("test_file.txt")) do |file1| File.open(datapath("test_file.txt")) do |file2| file1.flock_shared do @@ -958,7 +957,7 @@ describe "File" do end end - pending_win32 "#flock_shared soft blocking fiber" do + it "#flock_shared soft blocking fiber" do File.open(datapath("test_file.txt")) do |file1| File.open(datapath("test_file.txt")) do |file2| done = Channel(Nil).new @@ -975,7 +974,7 @@ describe "File" do end end - pending_win32 "#flock_exclusive soft blocking fiber" do + it "#flock_exclusive soft blocking fiber" do File.open(datapath("test_file.txt")) do |file1| File.open(datapath("test_file.txt")) do |file2| done = Channel(Nil).new diff --git a/src/crystal/system/win32/file.cr b/src/crystal/system/win32/file.cr index 0df1926cc047..bda41cde8b69 100644 --- a/src/crystal/system/win32/file.cr +++ b/src/crystal/system/win32/file.cr @@ -258,15 +258,54 @@ module Crystal::System::File end private def system_flock_shared(blocking : Bool) : Nil - raise NotImplementedError.new("File#flock_shared") + flock(false, blocking) end private def system_flock_exclusive(blocking : Bool) : Nil - raise NotImplementedError.new("File#flock_exclusive") + flock(true, blocking) end private def system_flock_unlock : Nil - raise NotImplementedError.new("File#flock_unlock") + unlock_file(windows_handle) + end + + private def flock(exclusive, retry) + flags = LibC::LOCKFILE_FAIL_IMMEDIATELY + flags |= LibC::LOCKFILE_EXCLUSIVE_LOCK if exclusive + + handle = windows_handle + if retry + until lock_file(handle, flags) + ::Fiber.yield + end + else + lock_file(handle, flags) || raise IO::Error.from_winerror("Error applying file lock: file is already locked") + end + end + + private def lock_file(handle, flags) + # lpOverlapped must be provided despite the synchronous use of this method. + overlapped = LibC::OVERLAPPED.new + # lock the entire file with offset 0 in overlapped and number of bytes set to max value + if 0 != LibC.LockFileEx(handle, flags, 0, 0xFFFF_FFFF, 0xFFFF_FFFF, pointerof(overlapped)) + true + else + winerror = WinError.value + if winerror == WinError::ERROR_LOCK_VIOLATION + false + else + raise IO::Error.from_winerror("LockFileEx", winerror) + end + end + end + + private def unlock_file(handle) + # lpOverlapped must be provided despite the synchronous use of this method. + overlapped = LibC::OVERLAPPED.new + # unlock the entire file with offset 0 in overlapped and number of bytes set to max value + if 0 == LibC.UnlockFileEx(handle, 0, 0xFFFF_FFFF, 0xFFFF_FFFF, pointerof(overlapped)) + raise IO::Error.from_winerror("UnLockFileEx") + end end private def system_fsync(flush_metadata = true) : Nil diff --git a/src/lib_c/x86_64-windows-msvc/c/fileapi.cr b/src/lib_c/x86_64-windows-msvc/c/fileapi.cr index 42533d369411..701a9590ca22 100644 --- a/src/lib_c/x86_64-windows-msvc/c/fileapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/fileapi.cr @@ -75,6 +75,21 @@ lib LibC fun FindNextFileW(hFindFile : HANDLE, lpFindFileData : WIN32_FIND_DATAW*) : BOOL fun FindClose(hFindFile : HANDLE) : BOOL + fun LockFileEx( + hFile : HANDLE, + dwFlags : DWORD, + dwReserved : DWORD, + nNumberOfBytesToLockLow : DWORD, + nNumberOfBytesToLockHigh : DWORD, + lpOverlapped : OVERLAPPED* + ) : BOOL + fun UnlockFileEx( + hFile : HANDLE, + dwReserved : DWORD, + nNumberOfBytesToUnlockLow : DWORD, + nNumberOfBytesToUnlockHigh : DWORD, + lpOverlapped : OVERLAPPED* + ) : BOOL fun SetFileTime(hFile : HANDLE, lpCreationTime : FILETIME*, lpLastAccessTime : FILETIME*, lpLastWriteTime : FILETIME*) : BOOL end diff --git a/src/lib_c/x86_64-windows-msvc/c/minwinbase.cr b/src/lib_c/x86_64-windows-msvc/c/minwinbase.cr index d01b5232e387..cb56e3269de2 100644 --- a/src/lib_c/x86_64-windows-msvc/c/minwinbase.cr +++ b/src/lib_c/x86_64-windows-msvc/c/minwinbase.cr @@ -45,6 +45,9 @@ lib LibC GetFileExMaxInfoLevel end + LOCKFILE_FAIL_IMMEDIATELY = DWORD.new(0x00000001) + LOCKFILE_EXCLUSIVE_LOCK = DWORD.new(0x00000002) + STATUS_PENDING = 0x103 STILL_ACTIVE = STATUS_PENDING end