From c2509e88642f9d4cfc60f3374133ef16a9b3978e Mon Sep 17 00:00:00 2001 From: Antonio Nuno Monteiro Date: Sun, 17 Sep 2023 13:50:57 -0700 Subject: [PATCH] fix(sendfile): handle EAGAIN on macOS non-blocking sockets (#185) --- lib/posix.ml | 2 +- sendfile/sendfile.ml | 133 ++++++++++++++++++++++++++++++-------- sendfile/sendfile.mli | 10 ++- sendfile/sendfile_stubs.c | 37 ++++++++++- 4 files changed, 147 insertions(+), 35 deletions(-) diff --git a/lib/posix.ml b/lib/posix.ml index 66b7d274..27005e50 100644 --- a/lib/posix.ml +++ b/lib/posix.ml @@ -49,7 +49,7 @@ let sendfile * `Gluten.Server.report_write_result` here given that we put * bytes in the file descriptor off-band. `report_write_result` * just tracks the internal Faraday buffers. *) - Logs.debug (fun m -> m "`sendfile` wrote %d bytes successfully" sent); + Logs.debug (fun m -> m "`sendfile` wrote %d bytes" sent); Ok () | Error e -> Writer.close raw_write_body; diff --git a/sendfile/sendfile.ml b/sendfile/sendfile.ml index 728e93f2..79d15dfc 100644 --- a/sendfile/sendfile.ml +++ b/sendfile/sendfile.ml @@ -1,3 +1,90 @@ +exception Darwin_specific_unix_error of (Unix.error * int) + +let () = + Callback.register_exception + "sendfile_exn_unix_error" + (Darwin_specific_unix_error (Unix.E2BIG, 0)); + Printexc.register_printer (function + | Darwin_specific_unix_error (unix_error, len) -> + let msg = + (* Copied from + https://github.com/ocaml/ocaml/blob/trunk/otherlibs/unix/unix_unix.ml *) + match unix_error with + | E2BIG -> "E2BIG" + | EACCES -> "EACCES" + | EAGAIN -> "EAGAIN" + | EBADF -> "EBADF" + | EBUSY -> "EBUSY" + | ECHILD -> "ECHILD" + | EDEADLK -> "EDEADLK" + | EDOM -> "EDOM" + | EEXIST -> "EEXIST" + | EFAULT -> "EFAULT" + | EFBIG -> "EFBIG" + | EINTR -> "EINTR" + | EINVAL -> "EINVAL" + | EIO -> "EIO" + | EISDIR -> "EISDIR" + | EMFILE -> "EMFILE" + | EMLINK -> "EMLINK" + | ENAMETOOLONG -> "ENAMETOOLONG" + | ENFILE -> "ENFILE" + | ENODEV -> "ENODEV" + | ENOENT -> "ENOENT" + | ENOEXEC -> "ENOEXEC" + | ENOLCK -> "ENOLCK" + | ENOMEM -> "ENOMEM" + | ENOSPC -> "ENOSPC" + | ENOSYS -> "ENOSYS" + | ENOTDIR -> "ENOTDIR" + | ENOTEMPTY -> "ENOTEMPTY" + | ENOTTY -> "ENOTTY" + | ENXIO -> "ENXIO" + | EPERM -> "EPERM" + | EPIPE -> "EPIPE" + | ERANGE -> "ERANGE" + | EROFS -> "EROFS" + | ESPIPE -> "ESPIPE" + | ESRCH -> "ESRCH" + | EXDEV -> "EXDEV" + | EWOULDBLOCK -> "EWOULDBLOCK" + | EINPROGRESS -> "EINPROGRESS" + | EALREADY -> "EALREADY" + | ENOTSOCK -> "ENOTSOCK" + | EDESTADDRREQ -> "EDESTADDRREQ" + | EMSGSIZE -> "EMSGSIZE" + | EPROTOTYPE -> "EPROTOTYPE" + | ENOPROTOOPT -> "ENOPROTOOPT" + | EPROTONOSUPPORT -> "EPROTONOSUPPORT" + | ESOCKTNOSUPPORT -> "ESOCKTNOSUPPORT" + | EOPNOTSUPP -> "EOPNOTSUPP" + | EPFNOSUPPORT -> "EPFNOSUPPORT" + | EAFNOSUPPORT -> "EAFNOSUPPORT" + | EADDRINUSE -> "EADDRINUSE" + | EADDRNOTAVAIL -> "EADDRNOTAVAIL" + | ENETDOWN -> "ENETDOWN" + | ENETUNREACH -> "ENETUNREACH" + | ENETRESET -> "ENETRESET" + | ECONNABORTED -> "ECONNABORTED" + | ECONNRESET -> "ECONNRESET" + | ENOBUFS -> "ENOBUFS" + | EISCONN -> "EISCONN" + | ENOTCONN -> "ENOTCONN" + | ESHUTDOWN -> "ESHUTDOWN" + | ETOOMANYREFS -> "ETOOMANYREFS" + | ETIMEDOUT -> "ETIMEDOUT" + | ECONNREFUSED -> "ECONNREFUSED" + | EHOSTDOWN -> "EHOSTDOWN" + | EHOSTUNREACH -> "EHOSTUNREACH" + | ELOOP -> "ELOOP" + | EOVERFLOW -> "EOVERFLOW" + | EUNKNOWNERR x -> Printf.sprintf "EUNKNOWNERR %d" x + in + + Some + (Format.asprintf "Sendfile(Unix_error): %s; remaining len: %d" msg len) + | _ -> None) + external sendfile : src:Unix.file_descr -> dst:Unix.file_descr @@ -6,38 +93,32 @@ external sendfile : -> int = "ocaml_sendfile_sendfile_stub" -let sendfile_once_exn ?(off = 0) ?len ~src dst = - let len = - match len with Some len -> len | None -> (Unix.fstat src).st_size - off - in - sendfile ~src ~dst ~off ~len +let sendfile_once_exn ?(off = 0) ~len ~src dst = sendfile ~src ~dst ~off ~len -let sendfile_once ?(off = 0) ?len ~src dst = - try - let ret = sendfile_once_exn ~src ~off ?len dst in - Ok ret - with +let sendfile_once ?(off = 0) ~len ~src dst = + try Ok (sendfile_once_exn ~src ~off ~len dst) with | Unix.Unix_error (unix_err, _, _msg) -> Error unix_err -let rec sendfile_exn ?(off = 0) ?len ~src dst = +let sendfile_exn ?(off = 0) ?len ~src dst = + let rec sendfile_exn ~off ~len ~src dst = + match sendfile_once_exn ~src ~off ~len dst with + | c when c = len -> len + | c -> sendfile_exn ~src ~off:(off + c) ~len:(len - c) dst + | exception Unix.Unix_error ((EINTR | EAGAIN), _, _) -> + sendfile_exn ~src ~off ~len dst + | exception Darwin_specific_unix_error ((EINTR | EAGAIN), sent) -> + (* Darwin systems signal the number of bytes partially sent on EINTR / + EAGAIN. *) + sendfile_exn ~src ~off:(off + sent) ~len:(len - sent) dst + in let len = match len with Some len -> len | None -> (Unix.fstat src).st_size - off in - match sendfile_once_exn ~src ~off ~len dst with - | c when c = len -> len - | c -> sendfile_exn ~src ~off:(off + c) ~len:(len - c) dst - | exception Unix.Unix_error (Unix.EINTR, _, _) -> - (* From: - https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/sendfile.2.html - - A signal interrupted sendfile() before it could be completed. If - specified, the number of bytes success-fully successfully fully sent will - be returned in *len. *) - sendfile_exn ~src ~off ~len dst + let _sent = sendfile_exn ~off ~len ~src dst in + (* If we're here, we sent the whole file correctly, so we return the total + length sent. *) + len let sendfile ?off ?len ~src dst = - try - let ret = sendfile_exn ~src ?off ?len dst in - Ok ret - with + try Ok (sendfile_exn ~src ?off ?len dst) with | Unix.Unix_error (unix_err, _, _msg) -> Error unix_err diff --git a/sendfile/sendfile.mli b/sendfile/sendfile.mli index 51f8af33..9bb1cf5d 100644 --- a/sendfile/sendfile.mli +++ b/sendfile/sendfile.mli @@ -1,7 +1,6 @@ val sendfile_once_exn : ?off:int (** Defaults to 0. *) - -> ?len:int - (** Defaults to length of data (file) associated with descriptor [fd]. *) + -> len:int -> src:Unix.file_descr -> Unix.file_descr -> int @@ -11,8 +10,7 @@ val sendfile_once_exn : val sendfile_once : ?off:int (** Defaults to 0. *) - -> ?len:int - (** Defaults to length of data (file) associated with descriptor [fd]. *) + -> len:int -> src:Unix.file_descr -> Unix.file_descr -> (int, Unix.error) result @@ -27,8 +25,8 @@ val sendfile_exn : -> src:Unix.file_descr -> Unix.file_descr -> int -(** Calls `sendfile(2)`, possibly repeatedly (if EINTR is returned) until the - entire file has been sent. +(** Calls `sendfile(2)`, possibly repeatedly (if EINTR / EAGAIN is returned) + until the entire file has been sent. @raise Sys_error if `sendfile` is not supported by the platform. *) diff --git a/sendfile/sendfile_stubs.c b/sendfile/sendfile_stubs.c index b9d5cd8c..c6be288a 100644 --- a/sendfile/sendfile_stubs.c +++ b/sendfile/sendfile_stubs.c @@ -19,13 +19,28 @@ #include #endif +#include + #define CAML_NAME_SPACE +#include +#include +#include #include #include #include #include #include +value sendfile_unix_error(int errcode, off_t len) { + CAMLparam0(); + value err, res; + err = caml_unix_error_of_code(errcode); + res = caml_alloc_small(2, 0); + Field(res, 0) = err; + Field(res, 1) = Val_long(len); + CAMLreturn(res); +} + /* * Sendfile has different signatures on macOS and Linux. */ @@ -42,8 +57,26 @@ CAMLprim value ocaml_sendfile_sendfile_stub(value v_fd, value v_sock, ret = sendfile(Int_val(v_fd), Int_val(v_sock), offset, &len, NULL, 0); caml_acquire_runtime_system(); - if (ret == -1) - uerror("sendfile", Nothing); + /* + * For the macOS case we raise a custom exception with the remaining length + * to be sent, due to `sendfile` differences in the Apple platform. + * + * From + * https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/sendfile.2.html: + * + * EINTR: A signal interrupted sendfile() before it could be completed. If + * specified, the number of bytes successfully fully sent will + * be returned in *len. + * + * Similarly for EAGAIN: + * The socket is marked for non-blocking I/O and not all data was sent due to + * the socket buffer being full. If specified, the number of bytes + * successfully sent will be returned in *len. + */ + if (ret == -1) { + caml_raise_with_arg(*caml_named_value("sendfile_exn_unix_error"), + sendfile_unix_error(errno, len)); + } CAMLreturn(Val_long(len)); }