diff --git a/spec/std/file_spec.cr b/spec/std/file_spec.cr index 1c4a2f5383ad..9b3b7bfce6d3 100644 --- a/spec/std/file_spec.cr +++ b/spec/std/file_spec.cr @@ -127,6 +127,33 @@ describe "File" do end end end + + it "can append non-blocking to an existing file" do + with_tempfile("append-existing.txt") do |path| + File.write(path, "hello") + File.write(path, " world", mode: "a", blocking: false) + File.read(path).should eq("hello world") + end + end + + it "returns the actual position after non-blocking append" do + with_tempfile("delete-file.txt") do |filename| + File.write(filename, "hello") + + File.open(filename, "a", blocking: false) do |file| + file.tell.should eq(0) + + file.write "12345".to_slice + file.tell.should eq(10) + + file.seek(5, IO::Seek::Set) + file.write "6789".to_slice + file.tell.should eq(14) + end + + File.read(filename).should eq("hello123456789") + end + end end it "reads entire file" do diff --git a/src/crystal/event_loop/iocp.cr b/src/crystal/event_loop/iocp.cr index 163a4602e29a..19a64904af92 100644 --- a/src/crystal/event_loop/iocp.cr +++ b/src/crystal/event_loop/iocp.cr @@ -228,10 +228,24 @@ class Crystal::EventLoop::IOCP < Crystal::EventLoop end def write(file_descriptor : Crystal::System::FileDescriptor, slice : Bytes) : Int32 - System::IOCP.overlapped_operation(file_descriptor, "WriteFile", file_descriptor.write_timeout, writing: true) do |overlapped| + bytes_written = System::IOCP.overlapped_operation(file_descriptor, "WriteFile", file_descriptor.write_timeout, writing: true) do |overlapped| + overlapped.offset = UInt64::MAX if file_descriptor.system_append? + ret = LibC.WriteFile(file_descriptor.windows_handle, slice, slice.size, out byte_count, overlapped) {ret, byte_count} end.to_i32 + + # The overlapped offset forced a write to the end of the file, but unlike + # synchronous writes, an asynchronous write incorrectly updates the file + # pointer: it merely adds the number of written bytes to the current + # position, disregarding that the offset might have changed it. + # + # We could seek before the async write (it works), but a concurrent fiber or + # parallel thread could also seek and we'd end up overwriting instead of + # appending; we need both the offset + explicit seek. + file_descriptor.system_seek(0, IO::Seek::End) if file_descriptor.system_append? + + bytes_written end def wait_writable(file_descriptor : Crystal::System::FileDescriptor) : Nil diff --git a/src/crystal/system/win32/file.cr b/src/crystal/system/win32/file.cr index e4e45ee3a664..c97600a573fc 100644 --- a/src/crystal/system/win32/file.cr +++ b/src/crystal/system/win32/file.cr @@ -12,7 +12,7 @@ module Crystal::System::File # On Windows we cannot rely on the system mode `FILE_APPEND_DATA` and # keep track of append mode explicitly. When writing data, this ensures to only # write at the end of the file. - @system_append = false + getter? system_append = false def self.open(filename : String, mode : String, perm : Int32 | ::File::Permissions, blocking : Bool?) : FileDescriptor::Handle perm = ::File::Permissions.new(perm) if perm.is_a? Int32 diff --git a/src/crystal/system/win32/file_descriptor.cr b/src/crystal/system/win32/file_descriptor.cr index 333f3c5a617d..b5c91e95e4f7 100644 --- a/src/crystal/system/win32/file_descriptor.cr +++ b/src/crystal/system/win32/file_descriptor.cr @@ -18,6 +18,10 @@ module Crystal::System::FileDescriptor @system_blocking = true + def system_append? + false + end + private def system_read(slice : Bytes) : Int32 handle = windows_handle if ConsoleUtils.console?(handle) @@ -160,7 +164,7 @@ module Crystal::System::FileDescriptor FileDescriptor.system_info windows_handle end - private def system_seek(offset, whence : IO::Seek) : Nil + def system_seek(offset, whence : IO::Seek) : Nil if LibC.SetFilePointerEx(windows_handle, offset, nil, whence) == 0 raise IO::Error.from_winerror("Unable to seek", target: self) end diff --git a/src/crystal/system/win32/iocp.cr b/src/crystal/system/win32/iocp.cr index 0c08cfe21bc6..cfb1fd1aef7e 100644 --- a/src/crystal/system/win32/iocp.cr +++ b/src/crystal/system/win32/iocp.cr @@ -264,6 +264,11 @@ struct Crystal::System::IOCP def initialize(@handle : LibC::HANDLE) end + def offset=(value : UInt64) + @overlapped.union.offset.offset = LibC::DWORD.new!(value) + @overlapped.union.offset.offsetHigh = LibC::DWORD.new!(value >> 32) + end + def wait_for_result(timeout, & : WinError ->) wait_for_completion(timeout)