From a7693ed8afeb0ec31b83aa9621f86488afc8a68b Mon Sep 17 00:00:00 2001 From: Joe Eli McIlvain Date: Thu, 30 May 2024 16:37:44 -0700 Subject: [PATCH] Refactor to raise error values instead of yielding them. This code previously contained a weird/brittle pattern of functions that use `yield` before every possible `error!`, in order to convey an error code with each error, as a workaround for the fact that Savi didn't yet support error values. Now that [the latest version of Savi] _does_ support error values, we can use them here. Note that there is a separate issue uncovered here for the Savi compiler showing "" errors when every branch of a choice-inside-a-choice block jumps away. For now this code does a workaround of adding a fake unreachable branch that doesn't jump away, for each place this is a problem. --- spec/File.Result.Spec.savi | 1 + src/File.Dumper.savi | 76 ++++++------ src/File.Info.savi | 4 +- src/File.Loader.savi | 68 +++++------ src/File.Result.savi | 26 +++- src/_Unsafe.savi | 235 +++++++++++++++++++------------------ 6 files changed, 221 insertions(+), 189 deletions(-) diff --git a/spec/File.Result.Spec.savi b/spec/File.Result.Spec.savi index 7fa98d9..8f01d8b 100644 --- a/spec/File.Result.Spec.savi +++ b/spec/File.Result.Spec.savi @@ -10,6 +10,7 @@ :it "describes without using 2nd person language" File.Result.each_enum_member -> (member | assert: !member.description.includes("you") + assert: !member.description.includes("You") ) :it "describes without any un-escaped newline characters in the text" diff --git a/src/File.Dumper.savi b/src/File.Dumper.savi index d0c8a04..f12d81c 100644 --- a/src/File.Dumper.savi +++ b/src/File.Dumper.savi @@ -23,45 +23,47 @@ callback File.Dumper.ResultActor = File.Dumper.ResultActor.None ) try ( - _Unsafe.check_path!(path.string) -> (error | - callback.result_of_dump_to_file(path, error) - ) + _Unsafe.check_path!(path.string) + | error | + callback.result_of_dump_to_file(path, error) + return + ) - fd = _Unsafe.open_write_create!(path) -> (error | - case error == ( - | File.Result.PathPrefixNotFound | - // TODO: Create the prefix directories instead of being asinine. - None - | File.Result.AccessDenied | - if Platform.is_windows ( - // TODO: Check if the path refers to a directory. If it is, switch - // the error to File.Result.PathIsDirectory, which is more helpful. - ) - | File.Result.NeedsRetry | - @dump_to_file(path, chunks, callback) // try again - return + fd = try ( + _Unsafe.open_write_create!(path) + | error | + case error == ( + | File.Result.PathPrefixNotFound | + // TODO: Create the prefix directories instead of being asinine. + None + | File.Result.AccessDenied | + if Platform.is_windows ( + // TODO: Check if the path refers to a directory. If it is, switch + // the error to File.Result.PathIsDirectory, which is more helpful. ) - - callback.result_of_dump_to_file(path, error) + | File.Result.NeedsRetry | + @dump_to_file(path, chunks, callback) // try again + return ) - try ( - _Unsafe.writev_loop!(fd, chunks) -> (error | - callback.result_of_dump_to_file(path, error) - ) - _Unsafe.fsync!(fd) -> (error | - callback.result_of_dump_to_file(path, error) - ) - _Unsafe.close!(fd) -> (error | - callback.result_of_dump_to_file(path, error) - ) - callback.result_of_dump_to_file(path, File.Result.Success) - | - // If any of the above steps failed, just close the file descriptor. - _Unsafe.close!(fd) -> (error | - // This is the failure path - we already called a callback with an - // error above, so we don't want to forward an additional error here, - // even if the close step failed with an error. - ) - ) + callback.result_of_dump_to_file(path, error) + return + ) + + try ( + _Unsafe.writev_loop!(fd, chunks) + _Unsafe.fsync!(fd) + _Unsafe.close!(fd) + + callback.result_of_dump_to_file(path, File.Result.Success) + | error | + callback.result_of_dump_to_file(path, error) + + // If any of the above steps failed, just close the file descriptor. + // + // If this close raises an error we'll swallow it, because we are + // already in the failure path - we already called a callback with an + // error above, so we don't want to forward an additional error here, + // even if the close step failed with an error. + try _Unsafe.close!(fd) ) diff --git a/src/File.Info.savi b/src/File.Info.savi index b8bd3dc..3afc408 100644 --- a/src/File.Info.savi +++ b/src/File.Info.savi @@ -54,6 +54,4 @@ :: Raises an error if the file does not exist or could not be accessed :: (for any reason). :fun non for!(path File.Path.Readable) File.Info - _Unsafe.stat_as_file_info!(path) -> (error | - // Silently ignore the specific kind of error - we won't show the caller. - ) + _Unsafe.stat_as_file_info!(path) diff --git a/src/File.Loader.savi b/src/File.Loader.savi index 730341a..a2204c4 100644 --- a/src/File.Loader.savi +++ b/src/File.Loader.savi @@ -23,42 +23,44 @@ callback File.Loader.ResultActor ) try ( - _Unsafe.check_path!(path.string) -> (error | - callback.result_of_load_from_file(path, error, Bytes.new_iso) - ) + _Unsafe.check_path!(path.string) + | error | + callback.result_of_load_from_file(path, error, Bytes.new_iso) + return + ) - fd = _Unsafe.open_read!(path) -> (error | - case error == ( - | File.Result.AccessDenied | - if Platform.is_windows ( - // TODO: Check if the path refers to a directory. If it is, switch - // the error to File.Result.PathIsDirectory, which is more helpful. - ) - | File.Result.NeedsRetry | - @load_from_file(path, callback) // try again - return + fd = try ( + _Unsafe.open_read!(path) + | error | + case error == ( + | File.Result.AccessDenied | + if Platform.is_windows ( + // TODO: Check if the path refers to a directory. If it is, switch + // the error to File.Result.PathIsDirectory, which is more helpful. ) - - callback.result_of_load_from_file(path, error, Bytes.new_iso) + | File.Result.NeedsRetry | + @load_from_file(path, callback) // try again + return ) + callback.result_of_load_from_file(path, error, Bytes.new_iso) + return + ) - try ( - buffer = Bytes.new - _Unsafe.read_all!(path, fd, buffer) -> (error | - callback.result_of_load_from_file(path, error, buffer.take_buffer) - ) + buffer = Bytes.new + result = File.Result.Success + try ( + _Unsafe.read_all!(path, fd, buffer) + | error | + result = error + ) - // Success! - result = File.Result.Success - callback.result_of_load_from_file(path, result, buffer.take_buffer) - ) + // Close the file descriptor prior to exit (on success or failure). + // + // Note that we don't deal with the error here, because it seems like + // there's not much we can do in the case of failure. + // + // At any rate, this is only a read operation, so it won't leave + // the filesystem in an inconsistent state. It seems unlikely to matter. + try _Unsafe.close!(fd) - // Close the file descriptor prior to exit (on success or failure). - _Unsafe.close!(fd) -> (error | - // Note that we don't deal with the error here, because it seems like - // there's not much we can do in the case of failure. - // - // At any rate, this is only a read operation, so it won't leave - // the filesystem in an inconsistent state. It seems unlikely to matter. - ) - ) + callback.result_of_load_from_file(path, result, buffer.take_buffer) diff --git a/src/File.Result.savi b/src/File.Result.savi index 002bc46..0f03ecf 100644 --- a/src/File.Result.savi +++ b/src/File.Result.savi @@ -51,8 +51,15 @@ :member TooManyFilesOpenInSystem 405 :member TooManySymbolicLinksFollowed 406 :member TooManyFilesOpenInProcess 407 - :member LowLevelIOFailed 408 - :member LowLevelCorruption 409 + :member TooManyWatchesOpenInSystem 408 + :member LowLevelIOFailed 409 + :member LowLevelCorruption 410 + + /// + // Fundamental problems with the system where the program is running. + + :member PlatformDoesNotSupportFiles 501 + :member PlatformDoesNotSupportWatching 502 :fun description String case @ == ( @@ -155,6 +162,11 @@ perhaps it needs to impose explicit limits on itself to prevent this. \ It may be possible to increase the limit if more files are needed." + | File.Result.TooManyWatchesOpenInSystem | + "There are too many watched file paths currently in the system. \ + This may be caused by a user-level limit or system-level limit. \ + It may be possible to increase the limit if more watches are needed." + | File.Result.LowLevelIOFailed | "A low-level I/O failure occurred while synchronizing state with the \ underlying file. It may (or may not) help to retry the operation." @@ -163,6 +175,16 @@ "A low-level corruption while interacting with the file system \ prevented success. It may (or may not) help to retry the operation." + | File.Result.PlatformDoesNotSupportFiles | + "The platform where the program is running does not support accessing \ + files at all. The program is likely running on a sandboxed platform \ + (such as WebAssembly in the browser)." + + | File.Result.PlatformDoesNotSupportWatching | + "The platform where the program is running does not support watching \ + watching files/directories. The only platforms with usable watching APIs \ + are MacOS, Windows, Linux, and FreeBSD." + | // This should be unreachable, if we've covered all the enum members. // We have a test in File.Result.Spec which proves that we did. diff --git a/src/_Unsafe.savi b/src/_Unsafe.savi index 3861c3f..ac0a504 100644 --- a/src/_Unsafe.savi +++ b/src/_Unsafe.savi @@ -7,60 +7,58 @@ :: most of them do not take file path capability object as an argument, :: so they rely on the caller to have already enforced that requirement. :: -:: The partial methods here follow the pattern of yielding an error result code -:: and then raising an error, so that the caller can follow a pattern of -:: storing or sending the error result code to a callback, and then let the -:: raised error unwind the stack. -:: -:: These methods must follow the contract of never raising an error unless they -:: yield an error result code first, so the caller can rely on this pattern. +:: In case of any error, the functions in this module raise an error code. :module _Unsafe :: Check that the given path string is valid for use with FFI. :fun check_path!(path_string String) + :errors File.Result + // If the path string contains a null byte, C APIs that don't take a length // will stop reading the string at that byte, such that it wouldn't be // using the full path string the user requested. In such a case, we // should reject the path as invalid rather than open the wrong path. if path_string.includes("\0") ( - yield File.Result.PathInvalid - error! + error! File.Result.PathInvalid ) :: Open a file at the given path for reading, if it exists. :: :: On success, returns a valid file descriptor ready for reading. - :: Otherwise, an error result code is yielded, then an error is raised. + :: Otherwise, an error result code is raised. :fun open_read!(path File.Path.Readable) + :errors File.Result + fd = _FFI.open(path.string.cstring, _FFI.o_rdonly, 0) if fd == -1 ( case _FFI.errno == ( - | OSError.EACCES | yield File.Result.AccessDenied - | OSError.EFBIG | yield File.Result.FileIsTooLarge - | OSError.EINVAL | yield File.Result.PathInvalid - | OSError.EISDIR | yield File.Result.PathIsDirectory - | OSError.ELOOP | yield File.Result.TooManySymbolicLinksFollowed - | OSError.EMFILE | yield File.Result.TooManyFilesOpenInProcess - | OSError.ENAMETOOLONG | yield File.Result.PathTooLong - | OSError.ENFILE | yield File.Result.TooManyFilesOpenInSystem - | OSError.ENOENT | yield File.Result.PathNotFound - | OSError.ENODEV | yield File.Result.FileIsNonOpenableDevice - | OSError.ENOMEM | yield File.Result.MemoryBufferExhausted - | OSError.ENXIO | yield File.Result.FileIsNonOpenableDevice - | OSError.EOVERFLOW | yield File.Result.FileIsTooLarge - | OSError.EPERM | yield File.Result.FileIsSealed - | OSError.EAGAIN | yield File.Result.FileIsLockedPseudoTerminal // weird MacOS use of EAGAIN... - | OSError.EINTR | yield File.Result.NeedsRetry - | yield File.Result.BugInLibrary + | OSError.EACCES | error! File.Result.AccessDenied + | OSError.EFBIG | error! File.Result.FileIsTooLarge + | OSError.EINVAL | error! File.Result.PathInvalid + | OSError.EISDIR | error! File.Result.PathIsDirectory + | OSError.ELOOP | error! File.Result.TooManySymbolicLinksFollowed + | OSError.EMFILE | error! File.Result.TooManyFilesOpenInProcess + | OSError.ENAMETOOLONG | error! File.Result.PathTooLong + | OSError.ENFILE | error! File.Result.TooManyFilesOpenInSystem + | OSError.ENOENT | error! File.Result.PathNotFound + | OSError.ENODEV | error! File.Result.FileIsNonOpenableDevice + | OSError.ENOMEM | error! File.Result.MemoryBufferExhausted + | OSError.ENXIO | error! File.Result.FileIsNonOpenableDevice + | OSError.EOVERFLOW | error! File.Result.FileIsTooLarge + | OSError.EPERM | error! File.Result.FileIsSealed + | OSError.EAGAIN | error! File.Result.FileIsLockedPseudoTerminal // weird MacOS use of EAGAIN... + | OSError.EINTR | error! File.Result.NeedsRetry + | error! File.Result.BugInLibrary ) - error! ) fd :: Open a file at the given path for writing, creating it if needed. :: :: On success, returns a valid file descriptor ready for writing. - :: Otherwise, the file descriptor will be invalid an error result is yielded. + :: Otherwise, an error result is raised. :fun open_write_create!(path File.Path.Writable) + :errors File.Result + default_mode = case ( | Platform.is_posix | 0b111111101 // rwxrwxr-x | Platform.is_windows | 0x0080 // _S_IWRITE @@ -82,70 +80,73 @@ ) if fd == -1 ( case _FFI.errno == ( - | OSError.EACCES | yield File.Result.AccessDenied - | OSError.EDQUOT | yield File.Result.DiskQuotaExhausted - | OSError.EFBIG | yield File.Result.FileIsTooLarge - | OSError.EINVAL | yield File.Result.PathInvalid - | OSError.EISDIR | yield File.Result.PathIsDirectory - | OSError.ELOOP | yield File.Result.TooManySymbolicLinksFollowed - | OSError.EMFILE | yield File.Result.TooManyFilesOpenInProcess - | OSError.ENAMETOOLONG | yield File.Result.PathTooLong - | OSError.ENFILE | yield File.Result.TooManyFilesOpenInSystem - | OSError.ENODEV | yield File.Result.FileIsNonOpenableDevice - | OSError.ENOMEM | yield File.Result.MemoryBufferExhausted - | OSError.ENOSPC | yield File.Result.DiskSpaceExhausted - | OSError.ENOTDIR | yield File.Result.PathPrefixIsNotDirectory - | OSError.ENXIO | yield File.Result.FileIsNonOpenableDevice - | OSError.EOVERFLOW | yield File.Result.FileIsTooLarge - | OSError.EPERM | yield File.Result.FileIsSealed - | OSError.EROFS | yield File.Result.FileSystemIsReadOnly - | OSError.ETXTBSY | yield File.Result.FileIsBusy - | OSError.EAGAIN | yield File.Result.FileIsLockedPseudoTerminal // weird MacOS use of EAGAIN... - | OSError.ENOENT | yield File.Result.PathPrefixNotFound - | OSError.EINTR | yield File.Result.NeedsRetry - | yield File.Result.BugInLibrary + | OSError.EACCES | error! File.Result.AccessDenied + | OSError.EDQUOT | error! File.Result.DiskQuotaExhausted + | OSError.EFBIG | error! File.Result.FileIsTooLarge + | OSError.EINVAL | error! File.Result.PathInvalid + | OSError.EISDIR | error! File.Result.PathIsDirectory + | OSError.ELOOP | error! File.Result.TooManySymbolicLinksFollowed + | OSError.EMFILE | error! File.Result.TooManyFilesOpenInProcess + | OSError.ENAMETOOLONG | error! File.Result.PathTooLong + | OSError.ENFILE | error! File.Result.TooManyFilesOpenInSystem + | OSError.ENODEV | error! File.Result.FileIsNonOpenableDevice + | OSError.ENOMEM | error! File.Result.MemoryBufferExhausted + | OSError.ENOSPC | error! File.Result.DiskSpaceExhausted + | OSError.ENOTDIR | error! File.Result.PathPrefixIsNotDirectory + | OSError.ENXIO | error! File.Result.FileIsNonOpenableDevice + | OSError.EOVERFLOW | error! File.Result.FileIsTooLarge + | OSError.EPERM | error! File.Result.FileIsSealed + | OSError.EROFS | error! File.Result.FileSystemIsReadOnly + | OSError.ETXTBSY | error! File.Result.FileIsBusy + | OSError.EAGAIN | error! File.Result.FileIsLockedPseudoTerminal // weird MacOS use of EAGAIN... + | OSError.ENOENT | error! File.Result.PathPrefixNotFound + | OSError.EINTR | error! File.Result.NeedsRetry + | error! File.Result.BugInLibrary ) - error! ) fd :fun stat_as_file_info!(path File.Path.Any) File.Info + :errors File.Result + try ( _FFI.Stat.stat_as_file_info!(path) | case _FFI.errno == ( - | OSError.success | yield File.Result.BugInLibrary // (unsupported platform in the above function) - | OSError.EACCES | yield File.Result.AccessDenied - | OSError.ELOOP | yield File.Result.TooManySymbolicLinksFollowed - | OSError.ENAMETOOLONG | yield File.Result.PathTooLong - | OSError.ENOENT | yield File.Result.PathNotFound - | OSError.ENOTDIR | yield File.Result.PathNotFound - | OSError.ENOMEM | yield File.Result.MemoryBufferExhausted - | OSError.EOVERFLOW | yield File.Result.FileIsTooLarge - | OSError.EIO | yield File.Result.LowLevelIOFailed - | OSError.EAFNOSUPPORT | yield File.Result.LowLevelCorruption // TODO: called EINTEGRITY on FreeBSD - need to eventually patch OSError in some way to include this - | yield File.Result.BugInLibrary + | OSError.success | error! File.Result.BugInLibrary // (unsupported platform in the above function) + | OSError.EACCES | error! File.Result.AccessDenied + | OSError.ELOOP | error! File.Result.TooManySymbolicLinksFollowed + | OSError.ENAMETOOLONG | error! File.Result.PathTooLong + | OSError.ENOENT | error! File.Result.PathNotFound + | OSError.ENOTDIR | error! File.Result.PathNotFound + | OSError.ENOMEM | error! File.Result.MemoryBufferExhausted + | OSError.EOVERFLOW | error! File.Result.FileIsTooLarge + | OSError.EIO | error! File.Result.LowLevelIOFailed + | OSError.EAFNOSUPPORT | error! File.Result.LowLevelCorruption // TODO: called EINTEGRITY on FreeBSD - need to eventually patch OSError in some way to include this + | error! File.Result.BugInLibrary ) - error! ) :fun fstat_as_file_info!(path File.Path.Any, fd I32) File.Info + :errors File.Result + try ( _FFI.Stat.fstat_as_file_info!(path, fd) | case _FFI.errno == ( - | OSError.success | yield File.Result.BugInLibrary // (unsupported platform in the above function) - | OSError.EACCES | yield File.Result.AccessDenied - | OSError.ENOMEM | yield File.Result.MemoryBufferExhausted - | OSError.EOVERFLOW | yield File.Result.FileIsTooLarge - | OSError.EIO | yield File.Result.LowLevelIOFailed - | OSError.EAFNOSUPPORT | yield File.Result.LowLevelCorruption // TODO: called EINTEGRITY on FreeBSD - need to eventually patch OSError in some way to include this - | yield File.Result.BugInLibrary + | OSError.success | error! File.Result.BugInLibrary // (unsupported platform in the above function) + | OSError.EACCES | error! File.Result.AccessDenied + | OSError.ENOMEM | error! File.Result.MemoryBufferExhausted + | OSError.EOVERFLOW | error! File.Result.FileIsTooLarge + | OSError.EIO | error! File.Result.LowLevelIOFailed + | OSError.EAFNOSUPPORT | error! File.Result.LowLevelCorruption // TODO: called EINTEGRITY on FreeBSD - need to eventually patch OSError in some way to include this + | error! File.Result.BugInLibrary ) - error! ) :fun read!(fd I32, buffer Bytes'ref) Bool + :errors File.Result + space = buffer.space - buffer.size if space == 0 ( buffer.reserve(buffer.space + 1) // reserve the next exponential size up @@ -155,15 +156,15 @@ read_bytes = _FFI.read(fd, buffer.cpointer.offset(buffer.size), space) if read_bytes == -1 ( case _FFI.errno == ( - | OSError.EIO | yield File.Result.LowLevelIOFailed - | OSError.EISDIR | yield File.Result.PathIsDirectory - | OSError.ENOBUFS | yield File.Result.MemoryBufferFailed - | OSError.ENOMEM | yield File.Result.MemoryBufferExhausted - | OSError.ENXIO | yield File.Result.FileIsNonReadableDevice + | OSError.EIO | error! File.Result.LowLevelIOFailed + | OSError.EISDIR | error! File.Result.PathIsDirectory + | OSError.ENOBUFS | error! File.Result.MemoryBufferFailed + | OSError.ENOMEM | error! File.Result.MemoryBufferExhausted + | OSError.ENXIO | error! File.Result.FileIsNonReadableDevice | OSError.EINTR | return False // keep reading, trying again - | yield File.Result.BugInLibrary + | 0 | None // TODO: this unreachable case shouldn't be necessary + | error! File.Result.BugInLibrary ) - error! ) buffer.resize_possibly_including_uninitialized_memory( buffer.size + read_bytes.usize @@ -171,6 +172,8 @@ read_bytes == 0 // return True if we've finished reading the entire file :fun read_all!(path File.Path.Readable, fd I32, buffer Bytes'ref) None + :errors File.Result + // Get the file size to create an buffer of the right initial size, then // read all of the file into the buffer, taking into account the // edge case where the file has changed size since it was last measured @@ -179,16 +182,20 @@ // That is, measuring the size ahead of time should only be seen as // an optimization that prevents re-allocations from a too-small buffer // in the common case where the size has not changed since the stat call. - initial_stat = @fstat_as_file_info!(path, fd) -> (error | yield error) - buffer.reserve(initial_stat.size + 1) + try ( + initial_stat = @fstat_as_file_info!(path, fd) + buffer.reserve(initial_stat.size + 1) + ) + + // Now read the entire file into the buffer, one chunk at a time. did_finish = False while !did_finish ( - did_finish = @read!(fd, buffer) -> (error | - yield error - ) + did_finish = @read!(fd, buffer) ) :fun writev_loop!(fd I32, incoming_chunks Array(Bytes)'val) None + :errors File.Result + // Convert to a C array of POSIX iovec structs. bytes_total USize = 0 chunks = Array(_WriteChunk).new(incoming_chunks.size) @@ -204,9 +211,7 @@ // Loop until everything is written. while bytes_total > 0 ( - bytes_total = @_writev_inner!(fd, chunks, bytes_total) -> (error | - yield error - ) + bytes_total = @_writev_inner!(fd, chunks, bytes_total) ) :fun _writev_inner!( @@ -214,6 +219,8 @@ chunks Array(_WriteChunk) bytes_total USize ) USize + :errors File.Result + bytes_written_isize = case ( | Platform.is_posix | _FFI.writev_posix( @@ -235,17 +242,17 @@ ) if bytes_written_isize == -1 ( case _FFI.errno == ( - | OSError.EDQUOT | yield File.Result.DiskQuotaExhausted - | OSError.EFBIG | yield File.Result.FileIsTooLarge - | OSError.EINVAL | yield File.Result.PathInvalid - | OSError.EIO | yield File.Result.LowLevelIOFailed - | OSError.ENOSPC | yield File.Result.DiskSpaceExhausted - | OSError.EPERM | yield File.Result.FileIsSealed - | OSError.EPIPE | yield File.Result.PipeReaderHasClosed + | OSError.EDQUOT | error! File.Result.DiskQuotaExhausted + | OSError.EFBIG | error! File.Result.FileIsTooLarge + | OSError.EINVAL | error! File.Result.PathInvalid + | OSError.EIO | error! File.Result.LowLevelIOFailed + | OSError.ENOSPC | error! File.Result.DiskSpaceExhausted + | OSError.EPERM | error! File.Result.FileIsSealed + | OSError.EPIPE | error! File.Result.PipeReaderHasClosed | OSError.EINTR | return bytes_total // needs retry - | yield File.Result.BugInLibrary + | 0 | None // TODO: this unreachable case shouldn't be necessary + | error! File.Result.BugInLibrary ) - error! ) bytes_written = bytes_written_isize.usize @@ -280,35 +287,37 @@ bytes_total :fun fsync!(fd I32) + :errors File.Result + try_again = True while try_again ( try_again = False - failed = _FFI.fsync(fd) == -1 - - if failed ( + if _FFI.fsync(fd) == -1 ( case _FFI.errno == ( - | OSError.EIO | yield File.Result.LowLevelIOFailed - | OSError.ENOSPC | yield File.Result.DiskSpaceExhausted - | OSError.EDQUOT | yield File.Result.DiskQuotaExhausted - | OSError.EROFS | failed = False // can't fsync a pipe or FIFO - that's okay - | OSError.EINVAL | failed = False // can't fsync a pipe or FIFO - that's okay - | OSError.EAGAIN | failed = False, try_again = True - | OSError.EINTR | failed = False, try_again = True - | yield File.Result.BugInLibrary + | OSError.EIO | error! File.Result.LowLevelIOFailed + | OSError.ENOSPC | error! File.Result.DiskSpaceExhausted + | OSError.EDQUOT | error! File.Result.DiskQuotaExhausted + | OSError.EROFS | None // can't fsync a pipe or FIFO - that's okay + | OSError.EINVAL | None // can't fsync a pipe or FIFO - that's okay + | OSError.EAGAIN | try_again = True + | OSError.EINTR | try_again = True + | error! File.Result.BugInLibrary ) - error! if failed ) ) True :fun close!(fd I32) None + :errors File.Result + failed = _FFI.close(fd) == -1 if failed ( case _FFI.errno == ( - | OSError.EIO | yield File.Result.LowLevelIOFailed - | OSError.ENOSPC | yield File.Result.DiskSpaceExhausted - | OSError.EDQUOT | yield File.Result.DiskQuotaExhausted + | OSError.EIO | error! File.Result.LowLevelIOFailed + | OSError.ENOSPC | error! File.Result.DiskSpaceExhausted + | OSError.EDQUOT | error! File.Result.DiskQuotaExhausted + | 0 | None // TODO: this unreachable case shouldn't be necessary | OSError.EINTR | // POSIX recommends that we don't try again - the file descriptor // on Linux is guaranteed to be freed even if interrupted. @@ -318,8 +327,6 @@ // to not free the file descriptor when interrupted. Sad. return | - yield File.Result.BugInLibrary + error! File.Result.BugInLibrary ) - - error! )