Skip to content

Commit 7ee2ce6

Browse files
committed
Fix: windows cant append to file async (crystal-lang#15681)
1 parent 5abb62c commit 7ee2ce6

File tree

5 files changed

+53
-3
lines changed

5 files changed

+53
-3
lines changed

spec/std/file_spec.cr

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,33 @@ describe "File" do
127127
end
128128
end
129129
end
130+
131+
it "can append non-blocking to an existing file" do
132+
with_tempfile("append-existing.txt") do |path|
133+
File.write(path, "hello")
134+
File.write(path, " world", mode: "a", blocking: false)
135+
File.read(path).should eq("hello world")
136+
end
137+
end
138+
139+
it "returns the actual position after non-blocking append" do
140+
with_tempfile("delete-file.txt") do |filename|
141+
File.write(filename, "hello")
142+
143+
File.open(filename, "a", blocking: false) do |file|
144+
file.tell.should eq(0)
145+
146+
file.write "12345".to_slice
147+
file.tell.should eq(10)
148+
149+
file.seek(5, IO::Seek::Set)
150+
file.write "6789".to_slice
151+
file.tell.should eq(14)
152+
end
153+
154+
File.read(filename).should eq("hello123456789")
155+
end
156+
end
130157
end
131158

132159
it "reads entire file" do

src/crystal/event_loop/iocp.cr

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,24 @@ class Crystal::EventLoop::IOCP < Crystal::EventLoop
228228
end
229229

230230
def write(file_descriptor : Crystal::System::FileDescriptor, slice : Bytes) : Int32
231-
System::IOCP.overlapped_operation(file_descriptor, "WriteFile", file_descriptor.write_timeout, writing: true) do |overlapped|
231+
bytes_written = System::IOCP.overlapped_operation(file_descriptor, "WriteFile", file_descriptor.write_timeout, writing: true) do |overlapped|
232+
overlapped.offset = UInt64::MAX if file_descriptor.system_append?
233+
232234
ret = LibC.WriteFile(file_descriptor.windows_handle, slice, slice.size, out byte_count, overlapped)
233235
{ret, byte_count}
234236
end.to_i32
237+
238+
# The overlapped offset forced a write to the end of the file, but unlike
239+
# synchronous writes, an asynchronous write incorrectly updates the file
240+
# pointer: it merely adds the number of written bytes to the current
241+
# position, disregarding that the offset might have changed it.
242+
#
243+
# We could seek before the async write (it works), but a concurrent fiber or
244+
# parallel thread could also seek and we'd end up overwriting instead of
245+
# appending; we need both the offset + explicit seek.
246+
file_descriptor.system_seek(0, IO::Seek::End) if file_descriptor.system_append?
247+
248+
bytes_written
235249
end
236250

237251
def wait_writable(file_descriptor : Crystal::System::FileDescriptor) : Nil

src/crystal/system/win32/file.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module Crystal::System::File
1212
# On Windows we cannot rely on the system mode `FILE_APPEND_DATA` and
1313
# keep track of append mode explicitly. When writing data, this ensures to only
1414
# write at the end of the file.
15-
@system_append = false
15+
getter? system_append = false
1616

1717
def self.open(filename : String, mode : String, perm : Int32 | ::File::Permissions, blocking : Bool?) : FileDescriptor::Handle
1818
perm = ::File::Permissions.new(perm) if perm.is_a? Int32

src/crystal/system/win32/file_descriptor.cr

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ module Crystal::System::FileDescriptor
1818

1919
@system_blocking = true
2020

21+
def system_append?
22+
false
23+
end
24+
2125
private def system_read(slice : Bytes) : Int32
2226
handle = windows_handle
2327
if ConsoleUtils.console?(handle)
@@ -160,7 +164,7 @@ module Crystal::System::FileDescriptor
160164
FileDescriptor.system_info windows_handle
161165
end
162166

163-
private def system_seek(offset, whence : IO::Seek) : Nil
167+
def system_seek(offset, whence : IO::Seek) : Nil
164168
if LibC.SetFilePointerEx(windows_handle, offset, nil, whence) == 0
165169
raise IO::Error.from_winerror("Unable to seek", target: self)
166170
end

src/crystal/system/win32/iocp.cr

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@ struct Crystal::System::IOCP
264264
def initialize(@handle : LibC::HANDLE)
265265
end
266266

267+
def offset=(value : UInt64)
268+
@overlapped.union.offset.offset = LibC::DWORD.new!(value)
269+
@overlapped.union.offset.offsetHigh = LibC::DWORD.new!(value >> 32)
270+
end
271+
267272
def wait_for_result(timeout, & : WinError ->)
268273
wait_for_completion(timeout)
269274

0 commit comments

Comments
 (0)