diff --git a/spec/std/dir_spec.cr b/spec/std/dir_spec.cr index 2d707612eb9c..76d98d58cecc 100644 --- a/spec/std/dir_spec.cr +++ b/spec/std/dir_spec.cr @@ -42,7 +42,7 @@ describe "Dir" do end it "tests empty? on nonexistent directory" do - expect_raises Errno do + expect_raises OSError::FileNotFound do Dir.empty?(File.join([__DIR__, "/foo/bar/"])) end end @@ -57,7 +57,7 @@ describe "Dir" do end it "tests mkdir with an existing path" do - expect_raises Errno do + expect_raises OSError::FileExists do Dir.mkdir(__DIR__, 0o700) end end @@ -73,19 +73,19 @@ describe "Dir" do it "tests mkdir_p with an existing path" do Dir.mkdir_p(__DIR__).should eq(0) - expect_raises Errno do + expect_raises OSError::FileExists do Dir.mkdir_p(__FILE__) end end it "tests rmdir with an nonexistent path" do - expect_raises Errno do + expect_raises OSError::FileNotFound do Dir.rmdir("/tmp/crystal_mkdir_test_#{Process.pid}/") end end it "tests rmdir with a path that cannot be removed" do - expect_raises Errno do + expect_raises OSError do Dir.rmdir(__DIR__) end end @@ -213,12 +213,18 @@ describe "Dir" do Dir.current.should eq(cwd) end - it "raises" do - expect_raises do + it "raises for nonexistent directory" do + expect_raises OSError::FileNotFound do Dir.cd("/nope") end end + it "raises for file" do + expect_raises OSError::NotADirectory do + Dir.cd("/usr/bin/env") + end + end + it "accepts a block" do cwd = Dir.current diff --git a/spec/std/file_spec.cr b/spec/std/file_spec.cr index 4a08b7d10110..83496d1fa2ec 100644 --- a/spec/std/file_spec.cr +++ b/spec/std/file_spec.cr @@ -116,7 +116,7 @@ describe "File" do it "raises an error when the file does not exist" do filename = "#{__DIR__}/data/non_existing_file.txt" - expect_raises Errno do + expect_raises OSError::FileNotFound do File.empty?(filename) end end @@ -286,7 +286,7 @@ describe "File" do end it "raises when destination doesn't exist" do - expect_raises(Errno) do + expect_raises(OSError::FileNotFound) do File.chmod("#{__DIR__}/data/unknown_chmod_path.txt", 0o664) end end @@ -353,7 +353,7 @@ describe "File" do end it "gets stat for non-existent file and raises" do - expect_raises Errno do + expect_raises OSError::FileNotFound do File.stat("non-existent") end end @@ -387,9 +387,16 @@ describe "File" do File.exists?(filename).should be_false end - it "raises errno when file doesn't exist" do + it "raises when file doesn't exist" do filename = "#{__DIR__}/data/temp1.txt" - expect_raises Errno do + expect_raises OSError::FileNotFound do + File.delete(filename) + end + end + + it "raises when it's a directory" do + filename = "#{__DIR__}/data/dir/subdir2" + expect_raises (OSError::IsADirectory | OSError::PermissionError) do File.delete(filename) end end @@ -409,7 +416,7 @@ describe "File" do it "raises if old file doesn't exist" do filename = "#{__DIR__}/data/temp1.txt" - expect_raises Errno do + expect_raises OSError::FileNotFound do File.rename(filename, "#{filename}.new") end end @@ -519,8 +526,8 @@ describe "File" do File.real_path("/usr/share/..").should eq("/usr") end - it "raises Errno if file doesn't exist" do - expect_raises Errno do + it "raises if file doesn't exist" do + expect_raises OSError::FileNotFound do File.real_path("/usr/share/foo/bar") end end @@ -724,7 +731,7 @@ describe "File" do filename = "#{__DIR__}/data/temp_write.txt" File.write(filename, "0123456789") File.open(filename, "r") do |f| - expect_raises(Errno) do + expect_raises(OSError) do # TODO: this should be an IO error f.truncate(4) end end @@ -733,12 +740,11 @@ describe "File" do end describe "flock" do - it "exlusively locks a file" do + it "exclusively locks a file" do File.open(__FILE__) do |file1| File.open(__FILE__) do |file2| file1.flock_exclusive do - # BUG: check for EWOULDBLOCK when exception filters are implemented - expect_raises(Errno) do + expect_raises(OSError::BlockingIO) do file2.flock_exclusive(blocking: false) { } end end @@ -974,7 +980,7 @@ describe "File" do atime = Time.new(2000, 1, 2) mtime = Time.new(2000, 3, 4) - expect_raises Errno, "Error setting time to file" do + expect_raises OSError::FileNotFound, "Error setting time to file" do File.utime(atime, mtime, "#{__DIR__}/nonexistent_file") end end @@ -1021,13 +1027,13 @@ describe "File" do end it "raises if path contains non-existent directory" do - expect_raises Errno, "Error opening file" do + expect_raises OSError::FileNotFound, "Error opening file" do File.touch("/tmp/non/existent/directory/test.tmp") end end it "raises if file cannot be accessed" do - expect_raises Errno, "Operation not permitted" do + expect_raises OSError::PermissionError, "Operation not permitted" do File.touch("/bin/ls") end end diff --git a/spec/std/file_utils_spec.cr b/spec/std/file_utils_spec.cr index 8ff578c32873..ca1cb376faca 100644 --- a/spec/std/file_utils_spec.cr +++ b/spec/std/file_utils_spec.cr @@ -284,7 +284,7 @@ describe "FileUtils" do end it "raises an error if non correct arguments" do - expect_raises Errno do + expect_raises OSError::FileNotFound do FileUtils.mv("/tmp/crystal_mv_test/a", "/tmp/crystal_mv_test/b") end end @@ -363,16 +363,16 @@ describe "FileUtils" do end it "tests mkdir with an existing path" do - expect_raises Errno do + expect_raises OSError::FileExists do Dir.mkdir(__DIR__, 0o700) end end it "tests mkdir with multiples existing paths" do - expect_raises Errno do + expect_raises OSError::FileExists do FileUtils.mkdir([__DIR__, __DIR__], 0o700) end - expect_raises Errno do + expect_raises OSError::FileExists do FileUtils.mkdir(["/tmp/crystal_mkdir_test_#{Process.pid}/", __DIR__], 0o700) end end @@ -401,38 +401,38 @@ describe "FileUtils" do it "tests mkdir_p with an existing path" do FileUtils.mkdir_p(__DIR__).should be_nil - expect_raises Errno do + expect_raises OSError::FileExists do FileUtils.mkdir_p(__FILE__) end end it "tests mkdir_p with multiple existing path" do FileUtils.mkdir_p([__DIR__, __DIR__]).should be_nil - expect_raises Errno do + expect_raises OSError::FileExists do FileUtils.mkdir_p([__FILE__, "/tmp/crystal_mkdir_ptest_#{Process.pid}/"]) end end it "tests rmdir with an non existing path" do - expect_raises Errno do + expect_raises OSError::FileNotFound do FileUtils.rmdir("/tmp/crystal_mkdir_test_#{Process.pid}/tmp/") end end it "tests rmdir with multiple non existing path" do - expect_raises Errno do + expect_raises OSError::FileNotFound do FileUtils.rmdir(["/tmp/crystal_mkdir_test_#{Process.pid}/tmp/", "/tmp/crystal_mkdir_test_#{Process.pid + 1}/tmp/"]) end end it "tests rmdir with a path that cannot be removed" do - expect_raises Errno do + expect_raises OSError do FileUtils.rmdir(__DIR__) end end it "tests rmdir with multiple path that cannot be removed" do - expect_raises Errno do + expect_raises OSError do FileUtils.rmdir([__DIR__, __DIR__]) end end @@ -445,7 +445,7 @@ describe "FileUtils" do end it "tests rm with non existing path" do - expect_raises Errno do + expect_raises OSError::FileNotFound do FileUtils.rm("/tmp/crystal_rm_test_#{Process.pid}") end end @@ -461,7 +461,7 @@ describe "FileUtils" do end it "tests rm with some non existing paths" do - expect_raises Errno do + expect_raises OSError::FileNotFound do path1 = "/tmp/crystal_rm_test_#{Process.pid}" path2 = "/tmp/crystal_rm_test_#{Process.pid + 1}" File.write(path1, "") diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index fef20485d253..eae2ed958309 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -8,8 +8,8 @@ private class RaiseErrno include IO def read(slice : Bytes) - Errno.value = @value - raise Errno.new "..." + OSError.errno = @value + raise OSError.create "..." end def write(slice : Bytes) : Nil @@ -294,9 +294,9 @@ module HTTP )) end - it "handles Errno" do + it "handles OSError" do processor = HTTP::Server::RequestProcessor.new { } - input = RaiseErrno.new(Errno::ECONNRESET) + input = RaiseErrno.new(OSError::ECONNRESET) output = IO::Memory.new processor.process(input, output) output.rewind.gets_to_end.empty?.should be_true diff --git a/spec/std/socket_spec.cr b/spec/std/socket_spec.cr index 0caa51af5b04..6f8a1f2c60b8 100644 --- a/spec/std/socket_spec.cr +++ b/spec/std/socket_spec.cr @@ -222,7 +222,7 @@ describe UNIXServer do server = UNIXServer.new(path) begin - expect_raises(Errno) { UNIXServer.new(path) } + expect_raises(OSError) { UNIXServer.new(path) } ensure server.close end @@ -235,7 +235,7 @@ describe UNIXServer do File.exists?(path).should be_true begin - expect_raises Errno, /(already|Address) in use/ do + expect_raises OSError, /(already|Address) in use/ do UNIXServer.new(path) end @@ -416,7 +416,7 @@ describe TCPServer do it "fails when port is in use" do port = free_udp_socket_port - expect_raises Errno, /(already|Address) in use/ do + expect_raises OSError, /(already|Address) in use/ do sock = Socket.tcp(Socket::Family::INET6) sock.bind(Socket::IPAddress.new("::1", port)) @@ -426,7 +426,7 @@ describe TCPServer do it "doesn't reuse the TCP port by default (SO_REUSEPORT)" do TCPServer.open("::", 0) do |server| - expect_raises(Errno) do + expect_raises(OSError) do TCPServer.open("::", server.local_address.port) { } end end @@ -532,7 +532,7 @@ describe TCPSocket do server.local_address.port end - expect_raises(Errno, "Error connecting to 'localhost:#{port}': Connection refused") do + expect_raises(ConnectionError::ConnectionRefused, "Error connecting to 'localhost:#{port}': Connection refused") do TCPSocket.new("localhost", port) end end diff --git a/src/compiler/crystal/codegen/cache_dir.cr b/src/compiler/crystal/codegen/cache_dir.cr index 1c05f707cb97..f3e9c4dc31e0 100644 --- a/src/compiler/crystal/codegen/cache_dir.cr +++ b/src/compiler/crystal/codegen/cache_dir.cr @@ -81,7 +81,7 @@ module Crystal begin Dir.mkdir_p(candidate) return @dir = candidate - rescue Errno + rescue OSError # Try next one end end diff --git a/src/crystal/system/unix/errno.cr b/src/crystal/system/unix/errno.cr new file mode 100644 index 000000000000..7d79200beb21 --- /dev/null +++ b/src/crystal/system/unix/errno.cr @@ -0,0 +1,47 @@ +{% skip_file() unless flag?(:unix) %} + +require "c/errno" +require "c/string" + +lib LibC + {% if flag?(:linux) %} + {% if flag?(:musl) %} + fun __errno_location : Int* + {% else %} + @[ThreadLocal] + $errno : Int + {% end %} + {% elsif flag?(:darwin) || flag?(:freebsd) %} + fun __error : Int* + {% elsif flag?(:openbsd) %} + fun __error = __errno : Int* + {% end %} +end + +module Crystal::System::Errno + # Returns the value of libc's errno. + def self.value : LibC::Int + {% if flag?(:linux) %} + {% if flag?(:musl) %} + LibC.__errno_location.value + {% else %} + LibC.errno + {% end %} + {% elsif flag?(:darwin) || flag?(:freebsd) || flag?(:openbsd) %} + LibC.__error.value + {% end %} + end + + # Sets the value of libc's errno. + def self.value=(value) + {% if flag?(:linux) %} + {% if flag?(:musl) %} + LibC.__errno_location.value = value + {% else %} + LibC.errno = value + {% end %} + {% elsif flag?(:darwin) || flag?(:freebsd) || flag?(:openbsd) %} + LibC.__error.value = value + {% end %} + end +end diff --git a/src/crystal/system/unix/getrandom.cr b/src/crystal/system/unix/getrandom.cr index 616345173347..b14d94dd7e68 100644 --- a/src/crystal/system/unix/getrandom.cr +++ b/src/crystal/system/unix/getrandom.cr @@ -62,7 +62,7 @@ module Crystal::System::Random end read_bytes = sys_getrandom(buf[0, chunk_size]) - raise Errno.new("getrandom") if read_bytes == -1 + raise OSError.create("getrandom") if read_bytes == -1 buf += read_bytes end @@ -70,7 +70,7 @@ module Crystal::System::Random # Low-level wrapper for the `getrandom(2)` syscall, returns the number of # bytes read or `-1` if an error occured (or the syscall isn't available) - # and sets `Errno.value`. + # and sets `OSError.errno`. # # We use the kernel syscall instead of the `getrandom` C function so any # binary compiled for Linux will always use getrandom if the kernel is 3.17+ @@ -79,7 +79,7 @@ module Crystal::System::Random private def self.sys_getrandom(buf : Bytes) loop do read_bytes = LibC.syscall(LibC::SYS_getrandom, buf, LibC::SizeT.new(buf.size), 0) - if read_bytes < 0 && (Errno.value == Errno::EINTR || Errno.value == Errno::EAGAIN) + if read_bytes < 0 && (OSError.errno == OSError::EINTR || OSError.errno == OSError::EAGAIN) Fiber.yield else return read_bytes diff --git a/src/crystal/system/unix/hostname.cr b/src/crystal/system/unix/hostname.cr index e803ef514185..e4ef198f5f21 100644 --- a/src/crystal/system/unix/hostname.cr +++ b/src/crystal/system/unix/hostname.cr @@ -4,7 +4,7 @@ module Crystal::System def self.hostname String.new(255) do |buffer| unless LibC.gethostname(buffer, LibC::SizeT.new(255)) == 0 - raise Errno.new("Could not get hostname") + raise OSError.create("Could not get hostname") end len = LibC.strlen(buffer) {len, len} diff --git a/src/crystal/system/unix/time.cr b/src/crystal/system/unix/time.cr index 2017c200e78e..18e6cb793e31 100644 --- a/src/crystal/system/unix/time.cr +++ b/src/crystal/system/unix/time.cr @@ -19,7 +19,7 @@ module Crystal::System::Time seconds_from_epoch = LibC::TimeT.new(seconds - UnixEpochInSeconds) # current TZ may have DST, either in past, present or future ret = LibC.localtime_r(pointerof(seconds_from_epoch), out tm) - raise Errno.new("localtime_r") if ret.null? + raise OSError.create("localtime_r") if ret.null? offset = tm.tm_gmtoff.to_i64 end @@ -29,11 +29,11 @@ module Crystal::System::Time def self.compute_utc_second_and_tenth_microsecond : {Int64, Int64} {% if LibC.methods.includes?("clock_gettime".id) %} ret = LibC.clock_gettime(LibC::CLOCK_REALTIME, out timespec) - raise Errno.new("clock_gettime") unless ret == 0 + raise OSError.create("clock_gettime") unless ret == 0 {timespec.tv_sec.to_i64 + UnixEpochInSeconds, timespec.tv_nsec / 100} {% else %} ret = LibC.gettimeofday(out timeval, nil) - raise Errno.new("gettimeofday") unless ret == 0 + raise OSError.create("gettimeofday") unless ret == 0 {timeval.tv_sec.to_i64 + UnixEpochInSeconds, timeval.tv_usec.to_i64 * 10} {% end %} end diff --git a/src/dir.cr b/src/dir.cr index 48f69dcce621..daed315f8a74 100644 --- a/src/dir.cr +++ b/src/dir.cr @@ -16,10 +16,12 @@ class Dir getter path : String # Returns a new directory object for the named directory. + # + # Raises `OSError` on failure. def initialize(@path) @dir = LibC.opendir(@path.check_no_null_byte) unless @dir - raise Errno.new("Error opening directory '#{@path}'") + raise OSError.create("Error opening directory '#{@path}'") end @closed = false end @@ -126,12 +128,12 @@ class Dir # ``` def read # readdir() returns NULL for failure and sets errno or returns NULL for EOF but leaves errno as is. wtf. - Errno.value = 0 + OSError.errno = 0 ent = LibC.readdir(@dir) if ent String.new(ent.value.d_name.to_unsafe) - elsif Errno.value != 0 - raise Errno.new("readdir") + elsif OSError.errno != 0 + raise OSError.create("readdir") else nil end @@ -147,7 +149,7 @@ class Dir def close return if @closed if LibC.closedir(@dir) != 0 - raise Errno.new("closedir") + raise OSError.create("closedir") end @closed = true end @@ -157,14 +159,18 @@ class Dir if dir = LibC.getcwd(nil, 0) String.new(dir).tap { LibC.free(dir.as(Void*)) } else - raise Errno.new("getcwd") + raise OSError.create("getcwd") end end # Changes the current working directory of the process to the given string. + # + # Raises `OSError::FileNotFound` if the directory does not exist, + # `OSError::NotADirectory` if the path points to a file, or + # other kinds of `OSError` in unusual cases. def self.cd(path) if LibC.chdir(path.check_no_null_byte) != 0 - raise Errno.new("Error while changing directory to #{path.inspect}") + raise OSError.create("Error while changing directory to #{path.inspect}") end end @@ -216,17 +222,19 @@ class Dir # Returns `true` if the given path exists and is a directory def self.exists?(path) : Bool if LibC.stat(path.check_no_null_byte, out stat) != 0 - if Errno.value == Errno::ENOENT || Errno.value == Errno::ENOTDIR + if OSError.errno == OSError::ENOENT || OSError.errno == OSError::ENOTDIR return false else - raise Errno.new("stat") + raise OSError.create("stat") end end File::Stat.new(stat).directory? end # Returns `true` if the directory at *path* is empty, otherwise returns `false`. - # Raises `Errno` if the directory at *path* does not exist. + # + # Raises `OSError::FileNotFound` if the directory at *path* does not exist, or + # other kinds of `OSError` in unusual cases. # # ``` # Dir.mkdir("bar") @@ -235,7 +243,7 @@ class Dir # Dir.empty?("bar") # => false # ``` def self.empty?(path) : Bool - raise Errno.new("Error determining size of '#{path}'") unless exists?(path) + raise OSError.create("Error determining size of '#{path}'") unless exists?(path) each_child(path) do |f| return false @@ -245,9 +253,13 @@ class Dir # Creates a new directory at the given path. The linux-style permission mode # can be specified, with a default of 777 (0o777). + # + # Raises `OSError::FileExists` if the directory already exists, + # `OSError::FileNotFound` if the parent directory does not exist, or + # other kinds of `OSError` in unusual cases. def self.mkdir(path, mode = 0o777) if LibC.mkdir(path.check_no_null_byte, mode) == -1 - raise Errno.new("Unable to create directory '#{path}'") + raise OSError.create("Unable to create directory '#{path}'") end 0 end @@ -255,6 +267,9 @@ class Dir # Creates a new directory at the given path, including any non-existing # intermediate directories. The linux-style permission mode can be specified, # with a default of 777 (0o777). + # + # Raises `OSError::FileExists` if the directory already exists, or + # other kinds of `OSError` in unusual cases. def self.mkdir_p(path, mode = 0o777) return 0 if Dir.exists?(path) @@ -277,7 +292,7 @@ class Dir # Removes the directory at the given path. def self.rmdir(path) if LibC.rmdir(path.check_no_null_byte) == -1 - raise Errno.new("Unable to remove directory '#{path}'") + raise OSError.create("Unable to remove directory '#{path}'") end 0 end diff --git a/src/ecr/process.cr b/src/ecr/process.cr index 9c5c2d5cecab..fdee39c45109 100644 --- a/src/ecr/process.cr +++ b/src/ecr/process.cr @@ -5,11 +5,7 @@ buffer_name = ARGV[1] begin puts ECR.process_file(filename, buffer_name) -rescue ex : Errno - if {Errno::ENOENT, Errno::EISDIR}.includes?(ex.errno) - STDERR.puts ex.message - exit 1 - else - raise ex - end +rescue ex : OSError::FileNotFound | OSError::IsADirectory + STDERR.puts ex.message + exit 1 end diff --git a/src/env.cr b/src/env.cr index da03c9ef0026..7fc170163168 100644 --- a/src/env.cr +++ b/src/env.cr @@ -35,7 +35,7 @@ module ENV def self.[]=(key : String, value : String?) if value if LibC.setenv(key, value, 1) != 0 - raise Errno.new("Error setting environment variable \"#{key}\"") + raise OSError.create("Error setting environment variable \"#{key}\"") end else LibC.unsetenv(key) diff --git a/src/event/signal_child_handler.cr b/src/event/signal_child_handler.cr index 694a03386879..42689bb90ad0 100644 --- a/src/event/signal_child_handler.cr +++ b/src/event/signal_child_handler.cr @@ -31,7 +31,7 @@ class Event::SignalChildHandler when 0 return nil when -1 - raise Errno.new("waitpid") unless Errno.value == Errno::ECHILD + raise OSError.create("waitpid") unless OSError.errno == OSError::ECHILD return nil else status = Process::Status.new exit_code diff --git a/src/fiber.cr b/src/fiber.cr index 5037c1dd982d..296690215bff 100644 --- a/src/fiber.cr +++ b/src/fiber.cr @@ -95,7 +95,7 @@ class Fiber LibC::MAP_PRIVATE | LibC::MAP_ANON, -1, 0 ).tap do |pointer| - raise Errno.new("Cannot allocate new fiber stack") if pointer == LibC::MAP_FAILED + raise OSError.create("Cannot allocate new fiber stack") if pointer == LibC::MAP_FAILED {% if flag?(:linux) %} LibC.madvise(pointer, Fiber::STACK_SIZE, LibC::MADV_NOHUGEPAGE) {% end %} diff --git a/src/file.cr b/src/file.cr index b67b0c7c98fe..0d84badeeab5 100644 --- a/src/file.cr +++ b/src/file.cr @@ -27,7 +27,7 @@ class File < IO::FileDescriptor fd = LibC.open(filename.check_no_null_byte, oflag, perm) if fd < 0 - raise Errno.new("Error opening file '#{filename}' with mode '#{mode}'") + raise OSError.create("Error opening file '#{filename}' with mode '#{mode}'") end @path = filename @@ -77,7 +77,7 @@ class File < IO::FileDescriptor getter path : String # Returns a `File::Stat` object for the file given by *path* or raises - # `Errno` in case of an error. In case of a symbolic link + # `OSError` in case of an error. In case of a symbolic link # it is followed and information about the target is returned. # # ``` @@ -87,13 +87,13 @@ class File < IO::FileDescriptor # ``` def self.stat(path) : Stat if LibC.stat(path.check_no_null_byte, out stat) != 0 - raise Errno.new("Unable to get stat for '#{path}'") + raise OSError.create("Unable to get stat for '#{path}'") end Stat.new(stat) end # Returns a `File::Stat` object for the file given by *path* or raises - # `Errno` in case of an error. In case of a symbolic link + # `OSError` in case of an error. In case of a symbolic link # information about it is returned. # # ``` @@ -103,7 +103,7 @@ class File < IO::FileDescriptor # ``` def self.lstat(path) : Stat if LibC.lstat(path.check_no_null_byte, out stat) != 0 - raise Errno.new("Unable to get lstat for '#{path}'") + raise OSError.create("Unable to get lstat for '#{path}'") end Stat.new(stat) end @@ -121,7 +121,9 @@ class File < IO::FileDescriptor end # Returns `true` if the file at *path* is empty, otherwise returns `false`. - # Raises `Errno` if the file at *path* does not exist. + # + # Raises `OSError::FileNotFound` if the file at *path* does not exist, or + # other kinds of `OSError` in unusual cases. # # ``` # File.write("foo", "") @@ -132,8 +134,8 @@ class File < IO::FileDescriptor def self.empty?(path) : Bool begin stat(path).size == 0 - rescue Errno - raise Errno.new("Error determining size of '#{path}'") + rescue OSError + raise OSError.create("Error determining size of '#{path}'") end end @@ -183,10 +185,10 @@ class File < IO::FileDescriptor # ``` def self.file?(path) : Bool if LibC.stat(path.check_no_null_byte, out stat) != 0 - if Errno.value == Errno::ENOENT + if OSError.errno == OSError::ENOENT return false else - raise Errno.new("stat") + raise OSError.create("stat") end end File::Stat.new(stat).file? @@ -279,7 +281,7 @@ class File < IO::FileDescriptor else LibC.chown(path, uid, gid) end - raise Errno.new("Error changing owner of '#{path}'") if ret == -1 + raise OSError.create("Error changing owner of '#{path}'") if ret == -1 end # Changes the permissions of the specified file. @@ -296,21 +298,24 @@ class File < IO::FileDescriptor # ``` def self.chmod(path, mode : Int) if LibC.chmod(path, mode) == -1 - raise Errno.new("Error changing permissions of '#{path}'") + raise OSError.create("Error changing permissions of '#{path}'") end end - # Delete the file at *path*. Deleting non-existent file will raise an exception. + # Delete the file at *path*. + # + # Raises `OSError::FileNotFound` if the file does not exist, or + # other kinds of `OSError` in unusual cases. # # ``` # File.write("foo", "") # File.delete("./foo") - # File.delete("./bar") # raises Errno (No such file or directory) + # File.delete("./bar") # raises OSError::FileNotFound # ``` def self.delete(path) err = LibC.unlink(path.check_no_null_byte) if err == -1 - raise Errno.new("Error deleting file '#{path}'") + raise OSError.create("Error deleting file '#{path}'") end end @@ -383,7 +388,7 @@ class File < IO::FileDescriptor # Resolves the real path of *path* by following symbolic links. def self.real_path(path) : String real_path_ptr = LibC.realpath(path, nil) - raise Errno.new("Error resolving real path of #{path}") unless real_path_ptr + raise OSError.create("Error resolving real path of #{path}") unless real_path_ptr String.new(real_path_ptr).tap { LibC.free(real_path_ptr.as(Void*)) } end @@ -391,24 +396,24 @@ class File < IO::FileDescriptor # given by *old_path*. def self.link(old_path, new_path) ret = LibC.link(old_path.check_no_null_byte, new_path.check_no_null_byte) - raise Errno.new("Error creating link from #{old_path} to #{new_path}") if ret != 0 + raise OSError.create("Error creating link from #{old_path} to #{new_path}") if ret != 0 ret end # Creates a symbolic link at *new_path* to an existing file given by *old_path. def self.symlink(old_path, new_path) ret = LibC.symlink(old_path.check_no_null_byte, new_path.check_no_null_byte) - raise Errno.new("Error creating symlink from #{old_path} to #{new_path}") if ret != 0 + raise OSError.create("Error creating symlink from #{old_path} to #{new_path}") if ret != 0 ret end # Returns `true` if the *path* is a symbolic link. def self.symlink?(path) : Bool if LibC.lstat(path.check_no_null_byte, out stat) != 0 - if Errno.value == Errno::ENOENT + if OSError.errno == OSError::ENOENT return false else - raise Errno.new("stat") + raise OSError.create("stat") end end (stat.st_mode & LibC::S_IFMT) == LibC::S_IFLNK @@ -579,7 +584,7 @@ class File < IO::FileDescriptor def self.rename(old_filename, new_filename) code = LibC.rename(old_filename.check_no_null_byte, new_filename.check_no_null_byte) if code != 0 - raise Errno.new("Error renaming file '#{old_filename}' to '#{new_filename}'") + raise OSError.create("Error renaming file '#{old_filename}' to '#{new_filename}'") end code end @@ -591,7 +596,7 @@ class File < IO::FileDescriptor timevals[1] = to_timeval(mtime) ret = LibC.utimes(filename, timevals) if ret != 0 - raise Errno.new("Error setting time to file '#{filename}'") + raise OSError.create("Error setting time to file '#{filename}'") end end @@ -622,7 +627,7 @@ class File < IO::FileDescriptor flush code = LibC.ftruncate(fd, size) if code != 0 - raise Errno.new("Error truncating file '#{path}'") + raise OSError.create("Error truncating file '#{path}'") end code end diff --git a/src/file/flock.cr b/src/file/flock.cr index b88634251ea5..d0db2cd6e808 100644 --- a/src/file/flock.cr +++ b/src/file/flock.cr @@ -24,7 +24,8 @@ class File end # Place a shared advisory lock. More than one process may hold a shared lock for a given file at a given time. - # `Errno::EWOULDBLOCK` is raised if *blocking* is set to `false` and an existing exclusive lock is set. + # + # `OSError::BlockingIO` is raised if *blocking* is set to `false` and an existing exclusive lock is set. def flock_shared(blocking = true) flock LibC::FlockOp::SH, blocking end @@ -39,7 +40,8 @@ class File end # Place an exclusive advisory lock. Only one process may hold an exclusive lock for a given file at a given time. - # `Errno::EWOULDBLOCK` is raised if *blocking* is set to `false` and any existing lock is set. + # + # `OSError::BlockingIO` is raised if *blocking* is set to `false` and any existing lock is set. def flock_exclusive(blocking = true) flock LibC::FlockOp::EX, blocking end @@ -53,7 +55,7 @@ class File op |= LibC::FlockOp::NB unless blocking if LibC.flock(@fd, op) != 0 - raise Errno.new("flock") + raise OSError.create("flock") end nil diff --git a/src/file/preader.cr b/src/file/preader.cr index e539c80a0759..ba9c012cb4ad 100644 --- a/src/file/preader.cr +++ b/src/file/preader.cr @@ -16,7 +16,7 @@ class File::PReader bytes_read = LibC.pread(@fd, slice.pointer(count).as(Void*), count, @offset + @pos) if bytes_read == -1 - raise Errno.new "Error reading file" + raise OSError.create "Error reading file" end @pos += bytes_read diff --git a/src/file/stat.cr b/src/file/stat.cr index 12f883aff478..4950f2fba6c0 100644 --- a/src/file/stat.cr +++ b/src/file/stat.cr @@ -4,7 +4,7 @@ class File struct Stat def initialize(filename : String) if LibC.stat(filename, out @stat) != 0 - raise Errno.new("Unable to get stat for '#{filename}'") + raise OSError.create("Unable to get stat for '#{filename}'") end end diff --git a/src/file_utils.cr b/src/file_utils.cr index f9e34450384c..5b6d19b49e76 100644 --- a/src/file_utils.cr +++ b/src/file_utils.cr @@ -217,7 +217,7 @@ module FileUtils srcs.each do |src| begin mv(src, File.join(dest, File.basename(src))) - rescue Errno + rescue OSError end end end @@ -298,7 +298,7 @@ module FileUtils def rm_rf(path : String) : Nil begin rm_r(path) - rescue Errno + rescue OSError end end @@ -313,7 +313,7 @@ module FileUtils paths.each do |path| begin rm_r(path) - rescue Errno + rescue OSError end end end diff --git a/src/http/server/request_processor.cr b/src/http/server/request_processor.cr index 44d05d837737..fb4fb3dd8c04 100644 --- a/src/http/server/request_processor.cr +++ b/src/http/server/request_processor.cr @@ -59,12 +59,12 @@ class HTTP::Server::RequestProcessor # didn't read it all, for the next request request.body.try &.close end - rescue ex : Errno + rescue ex : OSError # IO-related error, nothing to do ensure begin input.close if must_close - rescue ex : Errno + rescue ex : OSError # IO-related error, nothing to do end end diff --git a/src/iconv.cr b/src/iconv.cr index 3d41f1c05603..15bd10a18251 100644 --- a/src/iconv.cr +++ b/src/iconv.cr @@ -18,7 +18,7 @@ struct Iconv @iconv = LibC.iconv_open(to, from) if @iconv.address == LibC::SizeT.new(-1) - if Errno.value == Errno::EINVAL + if OSError.errno == OSError::EINVAL if original_from == "UTF-8" raise ArgumentError.new("Invalid encoding: #{original_to}") elsif original_to == "UTF-8" @@ -27,7 +27,7 @@ struct Iconv raise ArgumentError.new("Invalid encoding: #{original_from} -> #{original_to}") end else - raise Errno.new("iconv_open") + raise OSError.create("iconv_open") end end end @@ -59,10 +59,10 @@ struct Iconv inbytesleft.value -= 1 end else - case Errno.value - when Errno::EINVAL + case OSError.errno + when OSError::EINVAL raise ArgumentError.new "Incomplete multibyte sequence" - when Errno::EILSEQ + when OSError::EILSEQ raise ArgumentError.new "Invalid multibyte sequence" end end @@ -70,7 +70,7 @@ struct Iconv def close if LibC.iconv_close(@iconv) == -1 - raise Errno.new("iconv_close") + raise OSError.create("iconv_close") end end end diff --git a/src/io.cr b/src/io.cr index 2f0a67ec0f1d..2cbdecacebd7 100644 --- a/src/io.cr +++ b/src/io.cr @@ -146,7 +146,7 @@ module IO def self.pipe(read_blocking = false, write_blocking = false) pipe_fds = uninitialized StaticArray(LibC::Int, 2) if LibC.pipe(pipe_fds) != 0 - raise Errno.new("Could not create pipe") + raise OSError.create("Could not create pipe") end r = IO::FileDescriptor.new(pipe_fds[0], read_blocking) diff --git a/src/io/console.cr b/src/io/console.cr index e138c61bb999..6514fd46f7c0 100644 --- a/src/io/console.cr +++ b/src/io/console.cr @@ -22,7 +22,7 @@ class IO::FileDescriptor # Only call this when this IO is a TTY, such as a not redirected stdin. def noecho! if LibC.tcgetattr(fd, out mode) != 0 - raise Errno.new "can't set IO#noecho!" + raise OSError.create "can't set IO#noecho!" end noecho_from_tc_mode! end @@ -51,7 +51,7 @@ class IO::FileDescriptor # Only call this when this IO is a TTY, such as a not redirected stdin. def cooked! if LibC.tcgetattr(fd, out mode) != 0 - raise Errno.new "can't set IO#cooked!" + raise OSError.create "can't set IO#cooked!" end cooked_from_tc_mode! end @@ -89,7 +89,7 @@ class IO::FileDescriptor # Only call this when this IO is a TTY, such as a not redirected stdin. def raw! if LibC.tcgetattr(fd, out mode) != 0 - raise Errno.new "can't set IO#raw!" + raise OSError.create "can't set IO#raw!" end raw_from_tc_mode! @@ -102,7 +102,7 @@ class IO::FileDescriptor private def preserving_tc_mode(msg) if LibC.tcgetattr(fd, out mode) != 0 - raise Errno.new msg + raise OSError.create msg end before = mode begin diff --git a/src/io/encoding.cr b/src/io/encoding.cr index d9a785a49f36..1e061e7beefe 100644 --- a/src/io/encoding.cr +++ b/src/io/encoding.cr @@ -94,11 +94,11 @@ module IO # Check for errors if result == -1 - case Errno.value - when Errno::EILSEQ + case OSError.errno + when OSError::EILSEQ # For an illegal sequence we just skip one byte and we'll continue next @iconv.handle_invalid(pointerof(@in_buffer), pointerof(@in_buffer_left)) - when Errno::EINVAL + when OSError::EINVAL # EINVAL means "An incomplete multibyte sequence has been encountered in the input." old_in_buffer_left = @in_buffer_left diff --git a/src/io/file_descriptor.cr b/src/io/file_descriptor.cr index 7f061e8b65b4..7d35f56f800c 100644 --- a/src/io/file_descriptor.cr +++ b/src/io/file_descriptor.cr @@ -43,7 +43,7 @@ class IO::FileDescriptor def self.fcntl(fd, cmd, arg = 0) r = LibC.fcntl fd, cmd, arg - raise Errno.new("fcntl() failed") if r == -1 + raise OSError.create("fcntl() failed") if r == -1 r end @@ -53,7 +53,7 @@ class IO::FileDescriptor def stat if LibC.fstat(@fd, out stat) != 0 - raise Errno.new("Unable to get stat") + raise OSError.create("Unable to get stat") end File::Stat.new(stat) end @@ -79,7 +79,7 @@ class IO::FileDescriptor seek_value = LibC.lseek(@fd, offset, whence) if seek_value == -1 - raise Errno.new "Unable to seek" + raise OSError.create "Unable to seek" end @in_buffer_rem = Bytes.empty @@ -118,7 +118,7 @@ class IO::FileDescriptor check_open seek_value = LibC.lseek(@fd, 0, Seek::Current) - raise Errno.new "Unable to tell" if seek_value == -1 + raise OSError.create "Unable to tell" if seek_value == -1 seek_value - @in_buffer_rem.size end @@ -157,7 +157,7 @@ class IO::FileDescriptor def reopen(other : IO::FileDescriptor) if LibC.dup2(other.fd, self.fd) == -1 - raise Errno.new("Could not reopen file descriptor") + raise OSError.create("Could not reopen file descriptor") end # flag is lost after dup @@ -191,7 +191,7 @@ class IO::FileDescriptor private def unbuffered_write(slice : Bytes) write_syscall_helper(slice, "Error writing file") do |slice| LibC.write(@fd, slice, slice.size).tap do |return_code| - if return_code == -1 && Errno.value == Errno::EBADF + if return_code == -1 && OSError.errno == OSError::EBADF raise IO::Error.new "File not open for writing" end end @@ -220,11 +220,11 @@ class IO::FileDescriptor err = nil if LibC.close(@fd) != 0 - case Errno.value - when Errno::EINTR, Errno::EINPROGRESS + case OSError.errno + when OSError::EINTR, OSError::EINPROGRESS # ignore else - err = Errno.new("Error closing file") + err = OSError.create("Error closing file") end end diff --git a/src/io/syscall.cr b/src/io/syscall.cr index 65c6826b5842..16e0814b60c6 100644 --- a/src/io/syscall.cr +++ b/src/io/syscall.cr @@ -47,10 +47,10 @@ module IO::Syscall return bytes_read end - if Errno.value == Errno::EAGAIN + if OSError.errno == OSError::EAGAIN wait_readable else - raise Errno.new(errno_msg) + raise OSError.create(errno_msg) end end ensure @@ -66,10 +66,10 @@ module IO::Syscall slice += bytes_written return if slice.size == 0 else - if Errno.value == Errno::EAGAIN + if OSError.errno == OSError::EAGAIN wait_writable else - raise Errno.new(errno_msg) + raise OSError.create(errno_msg) end end end diff --git a/src/openssl.cr b/src/openssl.cr index 2222d887d76f..9f2078a30c96 100644 --- a/src/openssl.cr +++ b/src/openssl.cr @@ -110,7 +110,7 @@ module OpenSSL when 0 message = "Unexpected EOF" when -1 - raise Errno.new(func || "OpenSSL") + raise OSError.create(func || "OpenSSL") else message = "Unknown error" end diff --git a/src/openssl/ssl/socket.cr b/src/openssl/ssl/socket.cr index e2b131d556de..23fd3e8c8b61 100644 --- a/src/openssl/ssl/socket.cr +++ b/src/openssl/ssl/socket.cr @@ -138,15 +138,15 @@ abstract class OpenSSL::SSL::Socket ret = LibSSL.ssl_shutdown(@ssl) break if ret == 1 raise OpenSSL::SSL::Error.new(@ssl, ret, "SSL_shutdown") if ret < 0 - rescue e : Errno + rescue e : OSError case e.errno when 0 # OpenSSL claimed an underlying syscall failed, but that didn't set any error state, # assume we're done break - when Errno::EAGAIN + when OSError::EAGAIN # Ignore/retry, shutdown did not complete yet - when Errno::EINPROGRESS + when OSError::EINPROGRESS # Ignore/retry, another operation not complete yet else raise e diff --git a/src/errno.cr b/src/os_error.cr similarity index 69% rename from src/errno.cr rename to src/os_error.cr index f035fadf5c18..2e068cb988c7 100644 --- a/src/errno.cr +++ b/src/os_error.cr @@ -1,26 +1,11 @@ -require "c/errno" -require "c/string" - -lib LibC - {% if flag?(:linux) %} - {% if flag?(:musl) %} - fun __errno_location : Int* - {% else %} - @[ThreadLocal] - $errno : Int - {% end %} - {% elsif flag?(:darwin) || flag?(:freebsd) %} - fun __error : Int* - {% elsif flag?(:openbsd) %} - fun __error = __errno : Int* - {% end %} -end +require "crystal/system/unix/errno" -# Errno wraps and gives access to libc's errno. This is mostly useful when -# dealing with C libraries. +# `OSError` is an exception that is raised when something goes wrong when using the operating +# system's API (for example, it can be based on libc's errno). More specific subclasses of it are +# available (see `OSError.create`). # -# This class is the exception thrown when errno errors are encountered. -class Errno < Exception +# See also: `WindowsError`. +class OSError < Exception # Argument list too long E2BIG = LibC::E2BIG # Operation not permitted @@ -198,48 +183,92 @@ class Errno < Exception # Previous owner died EOWNERDEAD = LibC::EOWNERDEAD - # Returns the numeric value of errno. + # Returns the error code from libc's `errno` that this exception is based on + # (one of the constants in this module). getter errno : Int32 - # Creates a new Errno with the given message. The message will - # have concatenated the message denoted by `Errno#value`. - # - # Typical usage: + # Returns the value of libc's errno. + def self.errno : LibC::Int + Crystal::System::Errno.value + end + + # Sets the value of libc's errno. + def self.errno=(value) + Crystal::System::Errno.value = value + end + + # :nodoc: + def self.errno_to_class(errno) : OSError.class + {% begin %} + case errno + {% for cls in OSError.all_subclasses %} + {% for errno in cls.constant("ERRORS") || [] of ASTNode %} + when {{errno}} then {{cls}} + {% end %} + {% end %} + else OSError + end + {% end %} + end + + def initialize(message, @errno) + super(message) + end + + # Creates an object of some subclass of `OSError` with the given message, + # based on the *errno* code (the last set error by default). # # ``` - # err = LibC.some_call - # if err == -1 - # raise Errno.new("some_call") + # if LibC.mkdir("foo/bar", mode) == -1 + # raise OSError.create("Unable to create directory") + # # Actually a `OSError::FileNotFound` if "foo" does not exist. # end # ``` - def initialize(message, errno = Errno.value) - @errno = errno - super "#{message}: #{String.new(LibC.strerror(errno))}" + def self.create(message = nil, errno = OSError.errno) : OSError + cls = errno_to_class(errno) + message ||= cls.name + cls.new("#{message}: #{String.new(LibC.strerror(errno))}", errno) end - # Returns the value of libc's errno. - def self.value : LibC::Int - {% if flag?(:linux) %} - {% if flag?(:musl) %} - LibC.__errno_location.value - {% else %} - LibC.errno - {% end %} - {% elsif flag?(:darwin) || flag?(:freebsd) || flag?(:openbsd) %} - LibC.__error.value - {% end %} + class BlockingIO < OSError + ERRORS = {EAGAIN, EALREADY, EINPROGRESS, EWOULDBLOCK} end - # Sets the value of libc's errno. - def self.value=(value) - {% if flag?(:linux) %} - {% if flag?(:musl) %} - LibC.__errno_location.value = value - {% else %} - LibC.errno = value - {% end %} - {% elsif flag?(:darwin) || flag?(:freebsd) || flag?(:openbsd) %} - LibC.__error.value = value - {% end %} + class FileExists < OSError + ERRORS = {EEXIST} + end + + class FileNotFound < OSError + ERRORS = {ENOENT} + end + + class IsADirectory < OSError + ERRORS = {EISDIR} + end + + class NotADirectory < OSError + ERRORS = {ENOTDIR} + end + + class PermissionError < OSError + ERRORS = {EACCES, EPERM} + end +end + +class ConnectionError < OSError + class BrokenPipe < ConnectionError + ERRORS = {EPIPE} # ESHUTDOWN is missing + end + + class ConnectionAborted < ConnectionError + ERRORS = {ECONNABORTED} + end + + class ConnectionRefused < ConnectionError + ERRORS = {ECONNREFUSED} + end + + class ConnectionReset < ConnectionError + ERRORS = {ECONNRESET} end end diff --git a/src/prelude.cr b/src/prelude.cr index 4aa5e5954b57..00d075dfe9f2 100644 --- a/src/prelude.cr +++ b/src/prelude.cr @@ -32,7 +32,6 @@ require "dir" require "enum" require "enumerable" require "env" -require "errno" require "ext" require "file" require "float" @@ -51,6 +50,7 @@ require "mutex" require "named_tuple" require "nil" require "number" +require "os_error" require "pointer" require "pretty_print" require "primitives" @@ -75,3 +75,4 @@ require "tuple" require "unicode" require "union" require "value" +require "windows_error" diff --git a/src/process.cr b/src/process.cr index 8af49fca8aa8..c130d4d439db 100644 --- a/src/process.cr +++ b/src/process.cr @@ -27,7 +27,7 @@ class Process # Returns the process group identifier of the process identified by *pid*. def self.pgid(pid : Int32) : LibC::PidT ret = LibC.getpgid(pid) - raise Errno.new("getpgid") if ret < 0 + raise OSError.create("getpgid") if ret < 0 ret end @@ -40,7 +40,7 @@ class Process def self.kill(signal : Signal, *pids : Int) pids.each do |pid| ret = LibC.kill(pid, signal.value) - raise Errno.new("kill") if ret < 0 + raise OSError.create("kill") if ret < 0 end nil end @@ -53,8 +53,8 @@ class Process if ret == 0 true else - return false if Errno.value == Errno::ESRCH - raise Errno.new("kill") + return false if OSError.errno == OSError::ESRCH + raise OSError.create("kill") end end @@ -124,7 +124,7 @@ class Process pid = nil Process.after_fork_child_callbacks.each(&.call) if run_hooks when -1 - raise Errno.new("fork") + raise OSError.create("fork") end pid end @@ -390,7 +390,7 @@ class Process Dir.cd(chdir) if chdir if LibC.execvp(command, argv) == -1 - raise Errno.new("execvp") + raise OSError.create("execvp") end end diff --git a/src/process/executable_path.cr b/src/process/executable_path.cr index 5c8d6e140c45..28f8e9fe03b5 100644 --- a/src/process/executable_path.cr +++ b/src/process/executable_path.cr @@ -23,7 +23,7 @@ class Process if executable = executable_path_impl begin File.real_path(executable) - rescue Errno + rescue OSError end end end diff --git a/src/socket.cr b/src/socket.cr index 554742997756..9ad7272c3e3b 100644 --- a/src/socket.cr +++ b/src/socket.cr @@ -68,7 +68,7 @@ class Socket def initialize(@family, @type, @protocol = Protocol::IP, blocking = false) fd = LibC.socket(family, type, protocol) - raise Errno.new("failed to create socket:") if fd == -1 + raise OSError.create("failed to create socket:") if fd == -1 init_close_on_exec(fd) @fd = fd @@ -118,22 +118,22 @@ class Socket end # Tries to connect to a remote address. Yields an `IO::Timeout` or an - # `Errno` error if the connection failed. + # `OSError` error if the connection failed. def connect(addr, timeout = nil) timeout = timeout.seconds unless timeout.is_a? Time::Span | Nil loop do if LibC.connect(fd, addr, addr.size) == 0 return end - case Errno.value - when Errno::EISCONN + case OSError.errno + when OSError::EISCONN return - when Errno::EINPROGRESS, Errno::EALREADY + when OSError::EINPROGRESS, OSError::EALREADY wait_writable(timeout: timeout) do |error| return yield IO::Timeout.new("connect timed out") end else - return yield Errno.new("connect") + return yield OSError.create("connect") end end end @@ -173,10 +173,10 @@ class Socket end # Tries to bind the socket to a local address. - # Yields an `Errno` if the binding failed. + # Yields an `OSError` if the binding failed. def bind(addr) unless LibC.bind(fd, addr, addr.size) == 0 - yield Errno.new("bind") + yield OSError.create("bind") end end @@ -186,10 +186,10 @@ class Socket end # Tries to listen for connections on the previously bound socket. - # Yields an `Errno` on failure. + # Yields an `OSError` on failure. def listen(backlog = SOMAXCONN) unless LibC.listen(fd, backlog) == 0 - yield Errno.new("listen") + yield OSError.create("listen") end end @@ -238,10 +238,10 @@ class Socket if client_fd == -1 if closed? return - elsif Errno.value == Errno::EAGAIN + elsif OSError.errno == OSError::EAGAIN wait_readable else - raise Errno.new("accept") + raise OSError.create("accept") end else return client_fd @@ -263,7 +263,7 @@ class Socket def send(message) slice = message.to_slice bytes_sent = LibC.send(fd, slice.to_unsafe.as(Void*), slice.size, 0) - raise Errno.new("Error sending datagram") if bytes_sent == -1 + raise OSError.create("Error sending datagram") if bytes_sent == -1 bytes_sent ensure # see IO::FileDescriptor#unbuffered_write @@ -283,7 +283,7 @@ class Socket def send(message, to addr : Address) slice = message.to_slice bytes_sent = LibC.sendto(fd, slice.to_unsafe.as(Void*), slice.size, 0, addr, addr.size) - raise Errno.new("Error sending datagram to #{addr}") if bytes_sent == -1 + raise OSError.create("Error sending datagram to #{addr}") if bytes_sent == -1 bytes_sent end @@ -326,10 +326,10 @@ class Socket loop do bytes_read = LibC.recvfrom(fd, message.to_unsafe.as(Void*), message.size, 0, sockaddr, pointerof(addrlen)) if bytes_read == -1 - if Errno.value == Errno::EAGAIN + if OSError.errno == OSError::EAGAIN wait_readable else - raise Errno.new("Error receiving datagram") + raise OSError.create("Error receiving datagram") end else return {bytes_read.to_i, sockaddr, addrlen} @@ -354,7 +354,7 @@ class Socket private def shutdown(how) if LibC.shutdown(@fd, how) != 0 - raise Errno.new("shutdown #{how}") + raise OSError.create("shutdown #{how}") end end @@ -445,7 +445,7 @@ class Socket def getsockopt(optname, optval, level = LibC::SOL_SOCKET) optsize = LibC::SocklenT.new(sizeof(typeof(optval))) ret = LibC.getsockopt(fd, level, optname, (pointerof(optval).as(Void*)), pointerof(optsize)) - raise Errno.new("getsockopt") if ret == -1 + raise OSError.create("getsockopt") if ret == -1 optval end @@ -453,7 +453,7 @@ class Socket def setsockopt(optname, optval, level = LibC::SOL_SOCKET) optsize = LibC::SocklenT.new(sizeof(typeof(optval))) ret = LibC.setsockopt(fd, level, optname, (pointerof(optval).as(Void*)), optsize) - raise Errno.new("setsockopt") if ret == -1 + raise OSError.create("setsockopt") if ret == -1 ret end @@ -501,7 +501,7 @@ class Socket def self.fcntl(fd, cmd, arg = 0) r = LibC.fcntl fd, cmd, arg - raise Errno.new("fcntl() failed") if r == -1 + raise OSError.create("fcntl() failed") if r == -1 r end @@ -557,11 +557,11 @@ class Socket err = nil if LibC.close(@fd) != 0 - case Errno.value - when Errno::EINTR, Errno::EINPROGRESS + case OSError.errno + when OSError::EINTR, OSError::EINPROGRESS # ignore else - err = Errno.new("Error closing socket") + err = OSError.create("Error closing socket") end end diff --git a/src/socket/address.cr b/src/socket/address.cr index 94e7b644a6f7..9c0863330477 100644 --- a/src/socket/address.cr +++ b/src/socket/address.cr @@ -117,7 +117,7 @@ class Socket private def address(addr : LibC::In6Addr) String.new(46) do |buffer| unless LibC.inet_ntop(family, pointerof(addr).as(Void*), buffer, 46) - raise Errno.new("Failed to convert IP address") + raise OSError.create("Failed to convert IP address") end {LibC.strlen(buffer), 0} end @@ -126,7 +126,7 @@ class Socket private def address(addr : LibC::InAddr) String.new(16) do |buffer| unless LibC.inet_ntop(family, pointerof(addr).as(Void*), buffer, 16) - raise Errno.new("Failed to convert IP address") + raise OSError.create("Failed to convert IP address") end {LibC.strlen(buffer), 0} end diff --git a/src/socket/addrinfo.cr b/src/socket/addrinfo.cr index 9b4327515b22..19f0a5efa2ea 100644 --- a/src/socket/addrinfo.cr +++ b/src/socket/addrinfo.cr @@ -65,8 +65,8 @@ class Socket end unless addrinfo = addrinfo.try(&.next?) - if error.is_a?(Errno) && error.errno == Errno::ECONNREFUSED - raise Errno.new("Error connecting to '#{domain}:#{service}'", error.errno) + if error.is_a?(OSError) && error.errno == OSError::ECONNREFUSED + raise OSError.create("Error connecting to '#{domain}:#{service}'", error.errno) else raise error if error end diff --git a/src/socket/ip_socket.cr b/src/socket/ip_socket.cr index 12b63b556438..bbfda328a168 100644 --- a/src/socket/ip_socket.cr +++ b/src/socket/ip_socket.cr @@ -6,7 +6,7 @@ class IPSocket < Socket addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrIn6)) if LibC.getsockname(fd, sockaddr, pointerof(addrlen)) != 0 - raise Errno.new("getsockname") + raise OSError.create("getsockname") end IPAddress.from(sockaddr, addrlen) @@ -19,7 +19,7 @@ class IPSocket < Socket addrlen = LibC::SocklenT.new(sizeof(LibC::SockaddrIn6)) if LibC.getpeername(fd, sockaddr, pointerof(addrlen)) != 0 - raise Errno.new("getpeername") + raise OSError.create("getpeername") end IPAddress.from(sockaddr, addrlen) diff --git a/src/socket/udp_socket.cr b/src/socket/udp_socket.cr index 194b79a07730..ec8997d668f4 100644 --- a/src/socket/udp_socket.cr +++ b/src/socket/udp_socket.cr @@ -39,15 +39,15 @@ require "./ip_socket" # server.close # ``` # -# The `send` methods may sporadically fail with `Errno::ECONNREFUSED` when sending datagrams +# The `send` methods may sporadically fail with `OSError::ECONNREFUSED` when sending datagrams # to a non-listening server. # Wrap with an exception handler to prevent raising. Example: # # ``` # begin # client.send(message, @destination) -# rescue ex : Errno -# if ex.errno == Errno::ECONNREFUSED +# rescue ex : OSError +# if ex.errno == OSError::ECONNREFUSED # p ex.inspect # end # end diff --git a/src/socket/unix_socket.cr b/src/socket/unix_socket.cr index 777941efca6f..89c92d58f305 100644 --- a/src/socket/unix_socket.cr +++ b/src/socket/unix_socket.cr @@ -68,7 +68,7 @@ class UNIXSocket < Socket {% end %} if LibC.socketpair(Family::UNIX, socktype, 0, fds) != 0 - raise Errno.new("socketpair:") + raise OSError.create("socketpair:") end {UNIXSocket.new(fds[0], type), UNIXSocket.new(fds[1], type)} diff --git a/src/tempfile.cr b/src/tempfile.cr index 6278eb9625fa..002a72393f41 100644 --- a/src/tempfile.cr +++ b/src/tempfile.cr @@ -38,7 +38,7 @@ class Tempfile < IO::FileDescriptor @path = "#{tmpdir}#{name}.XXXXXX" fileno = LibC.mkstemp(@path) if fileno == -1 - raise Errno.new("mkstemp") + raise OSError.create("mkstemp") end super(fileno, blocking: true) end diff --git a/src/thread.cr b/src/thread.cr index 8102a033dbe0..1774215a9858 100644 --- a/src/thread.cr +++ b/src/thread.cr @@ -22,7 +22,7 @@ class Thread @th = th if ret != 0 - raise Errno.new("pthread_create") + raise OSError.create("pthread_create") end end @@ -39,7 +39,7 @@ class Thread def join if LibGC.pthread_join(@th.not_nil!, out _ret) != 0 - raise Errno.new("pthread_join") + raise OSError.create("pthread_join") end @detached = true diff --git a/src/thread/condition_variable.cr b/src/thread/condition_variable.cr index 2eb8c26d72d4..40b0b71ddc94 100644 --- a/src/thread/condition_variable.cr +++ b/src/thread/condition_variable.cr @@ -6,31 +6,31 @@ class Thread class ConditionVariable def initialize if LibC.pthread_cond_init(out @cond, nil) != 0 - raise Errno.new("pthread_cond_init") + raise OSError.create("pthread_cond_init") end end def signal if LibC.pthread_cond_signal(self) != 0 - raise Errno.new("pthread_cond_signal") + raise OSError.create("pthread_cond_signal") end end def broadcast if LibC.pthread_cond_broadcast(self) != 0 - raise Errno.new("pthread_cond_broadcast") + raise OSError.create("pthread_cond_broadcast") end end def wait(mutex : Thread::Mutex) if LibC.pthread_cond_wait(self, mutex) != 0 - raise Errno.new("pthread_cond_wait") + raise OSError.create("pthread_cond_wait") end end def finalize if LibC.pthread_cond_destroy(self) != 0 - raise Errno.new("pthread_cond_broadcast") + raise OSError.create("pthread_cond_broadcast") end end diff --git a/src/thread/mutex.cr b/src/thread/mutex.cr index f1241a0c6549..9c286191c92f 100644 --- a/src/thread/mutex.cr +++ b/src/thread/mutex.cr @@ -6,25 +6,25 @@ class Thread class Mutex def initialize if LibC.pthread_mutex_init(out @mutex, nil) != 0 - raise Errno.new("pthread_mutex_init") + raise OSError.create("pthread_mutex_init") end end def lock if LibC.pthread_mutex_lock(self) != 0 - raise Errno.new("pthread_mutex_lock") + raise OSError.create("pthread_mutex_lock") end end def try_lock if LibC.pthread_mutex_trylock(self) != 0 - raise Errno.new("pthread_mutex_trylock") + raise OSError.create("pthread_mutex_trylock") end end def unlock if LibC.pthread_mutex_unlock(self) != 0 - raise Errno.new("pthread_mutex_unlock") + raise OSError.create("pthread_mutex_unlock") end end @@ -37,7 +37,7 @@ class Thread def finalize if LibC.pthread_mutex_destroy(self) != 0 - raise Errno.new("pthread_mutex_destroy") + raise OSError.create("pthread_mutex_destroy") end end diff --git a/src/winerror.cr b/src/windows_error.cr similarity index 95% rename from src/winerror.cr rename to src/windows_error.cr index c107dab01845..8c74d94119cc 100644 --- a/src/winerror.cr +++ b/src/windows_error.cr @@ -1,94 +1,135 @@ -class WinError < Errno - # NOTE: `get_last_error` must be called BEFORE an instance of this class - # is malloced as it would change the "last error" to SUCCESS - def self.new(message) - new(message, LibC._GetLastError) - end - - def initialize(message, code) +# This module is included by `OSError` only on Windows. It also provides constants with error codes +# from Windows API. +# +# If you wish to obtain the Windows-specific error codes in your application, rescue `WindowsError` +# and read its `windows_error` member. +# +# ``` +# begin +# Dir.mkdir("foo/bar") +# rescue OSError::FileNotFound +# # Can't create foo/bar because foo does not exist +# rescue WindowsError # Some other OSError happened, and we're on Windows +# e.windows_error == WindowsError::ERROR_ACCESS_DENIED +# rescue e : OSError # Some other OSError happened, and we're not on Windows +# e.errno +# end +# ``` +# +# Note that the `errno` member of `OSError` will still be set to one of the error codes in `OSError` +# but it will be a guess of what this error would have meant in POSIX. +# +# If you are implementing a wrapper for a Windows API and expecting get an error code in +# `GetLastError`, just use `WindowsError.create` to get the appropriate subclass of +# `OSError` based on the error code. +module WindowsError + # Creates an object of some subclass of `OSError` with the given message, + # based on the *windows_error* code (the last set error by default). + # + # ``` + # if Windows.CreateDirectoryA("foo\\bar", nil) == 0 + # raise WindowsError.create("Unable to create directory") + # # Actually a `OSError::FileNotFound` if "foo" does not exist. + # end + # ``` + def self.create(message = nil, windows_error code : UInt32 = WindowsError.windows_error) : WindowsError + # NOTE: `GetLastError` must be called BEFORE an instance of this class + # is malloced as it would change the "last error" to SUCCESS buffer = uninitialized UInt8[256] size = LibC._FormatMessageA(LibC::FORMAT_MESSAGE_FROM_SYSTEM, nil, code, 0, buffer, buffer.size, nil) details = String.new(buffer.to_unsafe, size).strip - super("#{message}: [WinError #{code}, #{details}]", winerror_to_errno(code)) + errno = winerror_to_errno(code) + cls = OSError.errno_to_class(errno) + message ||= cls.name + cls.new("#{message}: [WinError #{code}, #{details}]", errno, windows_error: code) + end + + # Returns the Windows-specific error code that this exception is based on. + getter windows_error : UInt32 + + # Returns the value of the last Windows-specific error (`GetLastError`). + def self.windows_error : UInt32 + LibC._GetLastError end - # https://github.com/python/cpython/blob/master/PC/generrmap.c - # https://github.com/python/cpython/blob/master/PC/errmap.h - def winerror_to_errno(winerror) + # :nodoc: + def self.winerror_to_errno(winerror) + # https://github.com/python/cpython/blob/master/PC/generrmap.c + # https://github.com/python/cpython/blob/master/PC/errmap.h case winerror - when ERROR_FILE_NOT_FOUND then Errno::ENOENT - when ERROR_PATH_NOT_FOUND then Errno::ENOENT - when ERROR_TOO_MANY_OPEN_FILES then Errno::EMFILE - when ERROR_ACCESS_DENIED then Errno::EACCES - when ERROR_INVALID_HANDLE then Errno::EBADF - when ERROR_ARENA_TRASHED then Errno::ENOMEM - when ERROR_NOT_ENOUGH_MEMORY then Errno::ENOMEM - when ERROR_INVALID_BLOCK then Errno::ENOMEM - when ERROR_BAD_ENVIRONMENT then Errno::E2BIG - when ERROR_BAD_FORMAT then Errno::ENOEXEC - when ERROR_INVALID_DRIVE then Errno::ENOENT - when ERROR_CURRENT_DIRECTORY then Errno::EACCES - when ERROR_NOT_SAME_DEVICE then Errno::EXDEV - when ERROR_NO_MORE_FILES then Errno::ENOENT - when ERROR_WRITE_PROTECT then Errno::EACCES - when ERROR_BAD_UNIT then Errno::EACCES - when ERROR_NOT_READY then Errno::EACCES - when ERROR_BAD_COMMAND then Errno::EACCES - when ERROR_CRC then Errno::EACCES - when ERROR_BAD_LENGTH then Errno::EACCES - when ERROR_SEEK then Errno::EACCES - when ERROR_NOT_DOS_DISK then Errno::EACCES - when ERROR_SECTOR_NOT_FOUND then Errno::EACCES - when ERROR_OUT_OF_PAPER then Errno::EACCES - when ERROR_WRITE_FAULT then Errno::EACCES - when ERROR_READ_FAULT then Errno::EACCES - when ERROR_GEN_FAILURE then Errno::EACCES - when ERROR_SHARING_VIOLATION then Errno::EACCES - when ERROR_LOCK_VIOLATION then Errno::EACCES - when ERROR_WRONG_DISK then Errno::EACCES - when ERROR_SHARING_BUFFER_EXCEEDED then Errno::EACCES - when ERROR_BAD_NETPATH then Errno::ENOENT - when ERROR_NETWORK_ACCESS_DENIED then Errno::EACCES - when ERROR_BAD_NET_NAME then Errno::ENOENT - when ERROR_FILE_EXISTS then Errno::EEXIST - when ERROR_CANNOT_MAKE then Errno::EACCES - when ERROR_FAIL_I24 then Errno::EACCES - when ERROR_NO_PROC_SLOTS then Errno::EAGAIN - when ERROR_DRIVE_LOCKED then Errno::EACCES - when ERROR_BROKEN_PIPE then Errno::EPIPE - when ERROR_DISK_FULL then Errno::ENOSPC - when ERROR_INVALID_TARGET_HANDLE then Errno::EBADF - when ERROR_WAIT_NO_CHILDREN then Errno::ECHILD - when ERROR_CHILD_NOT_COMPLETE then Errno::ECHILD - when ERROR_DIRECT_ACCESS_HANDLE then Errno::EBADF - when ERROR_SEEK_ON_DEVICE then Errno::EACCES - when ERROR_DIR_NOT_EMPTY then Errno::ENOTEMPTY - when ERROR_NOT_LOCKED then Errno::EACCES - when ERROR_BAD_PATHNAME then Errno::ENOENT - when ERROR_MAX_THRDS_REACHED then Errno::EAGAIN - when ERROR_LOCK_FAILED then Errno::EACCES - when ERROR_ALREADY_EXISTS then Errno::EEXIST - when ERROR_INVALID_STARTING_CODESEG then Errno::ENOEXEC - when ERROR_INVALID_STACKSEG then Errno::ENOEXEC - when ERROR_INVALID_MODULETYPE then Errno::ENOEXEC - when ERROR_INVALID_EXE_SIGNATURE then Errno::ENOEXEC - when ERROR_EXE_MARKED_INVALID then Errno::ENOEXEC - when ERROR_BAD_EXE_FORMAT then Errno::ENOEXEC - when ERROR_ITERATED_DATA_EXCEEDS_64k then Errno::ENOEXEC - when ERROR_INVALID_MINALLOCSIZE then Errno::ENOEXEC - when ERROR_DYNLINK_FROM_INVALID_RING then Errno::ENOEXEC - when ERROR_IOPL_NOT_ENABLED then Errno::ENOEXEC - when ERROR_INVALID_SEGDPL then Errno::ENOEXEC - when ERROR_AUTODATASEG_EXCEEDS_64k then Errno::ENOEXEC - when ERROR_RING2SEG_MUST_BE_MOVABLE then Errno::ENOEXEC - when ERROR_RELOC_CHAIN_XEEDS_SEGLIM then Errno::ENOEXEC - when ERROR_INFLOOP_IN_RELOC_CHAIN then Errno::ENOEXEC - when ERROR_FILENAME_EXCED_RANGE then Errno::ENOENT - when ERROR_NESTING_NOT_ALLOWED then Errno::EAGAIN - when ERROR_NO_DATA then Errno::EPIPE - when ERROR_DIRECTORY then Errno::ENOTDIR - when ERROR_NOT_ENOUGH_QUOTA then Errno::ENOMEM - else Errno::EINVAL + when ERROR_FILE_NOT_FOUND then OSError::ENOENT + when ERROR_PATH_NOT_FOUND then OSError::ENOENT + when ERROR_TOO_MANY_OPEN_FILES then OSError::EMFILE + when ERROR_ACCESS_DENIED then OSError::EACCES + when ERROR_INVALID_HANDLE then OSError::EBADF + when ERROR_ARENA_TRASHED then OSError::ENOMEM + when ERROR_NOT_ENOUGH_MEMORY then OSError::ENOMEM + when ERROR_INVALID_BLOCK then OSError::ENOMEM + when ERROR_BAD_ENVIRONMENT then OSError::E2BIG + when ERROR_BAD_FORMAT then OSError::ENOEXEC + when ERROR_INVALID_DRIVE then OSError::ENOENT + when ERROR_CURRENT_DIRECTORY then OSError::EACCES + when ERROR_NOT_SAME_DEVICE then OSError::EXDEV + when ERROR_NO_MORE_FILES then OSError::ENOENT + when ERROR_WRITE_PROTECT then OSError::EACCES + when ERROR_BAD_UNIT then OSError::EACCES + when ERROR_NOT_READY then OSError::EACCES + when ERROR_BAD_COMMAND then OSError::EACCES + when ERROR_CRC then OSError::EACCES + when ERROR_BAD_LENGTH then OSError::EACCES + when ERROR_SEEK then OSError::EACCES + when ERROR_NOT_DOS_DISK then OSError::EACCES + when ERROR_SECTOR_NOT_FOUND then OSError::EACCES + when ERROR_OUT_OF_PAPER then OSError::EACCES + when ERROR_WRITE_FAULT then OSError::EACCES + when ERROR_READ_FAULT then OSError::EACCES + when ERROR_GEN_FAILURE then OSError::EACCES + when ERROR_SHARING_VIOLATION then OSError::EACCES + when ERROR_LOCK_VIOLATION then OSError::EACCES + when ERROR_WRONG_DISK then OSError::EACCES + when ERROR_SHARING_BUFFER_EXCEEDED then OSError::EACCES + when ERROR_BAD_NETPATH then OSError::ENOENT + when ERROR_NETWORK_ACCESS_DENIED then OSError::EACCES + when ERROR_BAD_NET_NAME then OSError::ENOENT + when ERROR_FILE_EXISTS then OSError::EEXIST + when ERROR_CANNOT_MAKE then OSError::EACCES + when ERROR_FAIL_I24 then OSError::EACCES + when ERROR_NO_PROC_SLOTS then OSError::EAGAIN + when ERROR_DRIVE_LOCKED then OSError::EACCES + when ERROR_BROKEN_PIPE then OSError::EPIPE + when ERROR_DISK_FULL then OSError::ENOSPC + when ERROR_INVALID_TARGET_HANDLE then OSError::EBADF + when ERROR_WAIT_NO_CHILDREN then OSError::ECHILD + when ERROR_CHILD_NOT_COMPLETE then OSError::ECHILD + when ERROR_DIRECT_ACCESS_HANDLE then OSError::EBADF + when ERROR_SEEK_ON_DEVICE then OSError::EACCES + when ERROR_DIR_NOT_EMPTY then OSError::ENOTEMPTY + when ERROR_NOT_LOCKED then OSError::EACCES + when ERROR_BAD_PATHNAME then OSError::ENOENT + when ERROR_MAX_THRDS_REACHED then OSError::EAGAIN + when ERROR_LOCK_FAILED then OSError::EACCES + when ERROR_ALREADY_EXISTS then OSError::EEXIST + when ERROR_INVALID_STARTING_CODESEG then OSError::ENOEXEC + when ERROR_INVALID_STACKSEG then OSError::ENOEXEC + when ERROR_INVALID_MODULETYPE then OSError::ENOEXEC + when ERROR_INVALID_EXE_SIGNATURE then OSError::ENOEXEC + when ERROR_EXE_MARKED_INVALID then OSError::ENOEXEC + when ERROR_BAD_EXE_FORMAT then OSError::ENOEXEC + when ERROR_ITERATED_DATA_EXCEEDS_64k then OSError::ENOEXEC + when ERROR_INVALID_MINALLOCSIZE then OSError::ENOEXEC + when ERROR_DYNLINK_FROM_INVALID_RING then OSError::ENOEXEC + when ERROR_IOPL_NOT_ENABLED then OSError::ENOEXEC + when ERROR_INVALID_SEGDPL then OSError::ENOEXEC + when ERROR_AUTODATASEG_EXCEEDS_64k then OSError::ENOEXEC + when ERROR_RING2SEG_MUST_BE_MOVABLE then OSError::ENOEXEC + when ERROR_RELOC_CHAIN_XEEDS_SEGLIM then OSError::ENOEXEC + when ERROR_INFLOOP_IN_RELOC_CHAIN then OSError::ENOEXEC + when ERROR_FILENAME_EXCED_RANGE then OSError::ENOENT + when ERROR_NESTING_NOT_ALLOWED then OSError::EAGAIN + when ERROR_NO_DATA then OSError::EPIPE + when ERROR_DIRECTORY then OSError::ENOTDIR + when ERROR_NOT_ENOUGH_QUOTA then OSError::ENOMEM + else OSError::EINVAL end end @@ -2180,3 +2221,16 @@ class WinError < Errno ERROR_STATE_CONTAINER_NAME_SIZE_LIMIT_EXCEEDED = 15818_u32 ERROR_API_UNAVAILABLE = 15841_u32 end + +{% if flag?(:windows) %} + class OSError < Exception + include WindowsError + + @windows_error = 0u32 + + def initialize(message, errno : Int32?, *, @windows_error : UInt32) + @errno = errno || WindowsError.winerror_to_errno(windows_error) + super(message) + end + end +{% end %}