Skip to content

Commit 236d517

Browse files
ausimianclaude
andcommitted
fix: write the handle with :file.write rather than IO.binwrite
Elixir 1.20's type checker refused the error clause in written/3 as unreachable, failing mix compile --warnings-as-errors on all three 1.20 cells while 1.18 and 1.19 stayed green. It was right, and deleting the clause would have been the wrong way to agree with it. IO.binwrite/2 is specified to return :ok, and it earns that spec: its body calls :file.write/2 and raises whatever error comes back (:erlang.error/1, io.ex:308). So the clause really was dead, and what would have happened on a failed write was not an unreported error but an ErlangError leaving materialise/2 - past every caller written to expect {:error, message}, in a module whose whole discipline is that it reports rather than raises because it runs ahead of install_release/1 where an exception is a silent abort. The checker found a real defect, not a style violation. The right call was there all along. The handle is opened :raw, which makes it a :file_descriptor record rather than an io device - the IO functions accept one, but they are not for it - and :file.write/2 is what a raw handle takes. It returns the error instead of raising it: {:error, :ebadf} writing to a handle that cannot be written, {:error, :einval} to one already closed. Measured under both 1.19.5 and 1.20.0 rather than assumed, since an error branch that cannot be reached is what caused this. Audited the rest for the same shape. Two IO references in the project, both on standard output or standard error where an io device is what is wanted and nothing matches on the return - Castle.report!/1 and one test - and one raw handle in the codebase, the one this changes. Nothing else. Verified against the version that failed rather than only the one installed here: mise had 1.20.0-otp-28, so the failing cell's steps were run locally against a separate build root. The old call reproduces CI's warning exactly, at Castle.Peer.written/3; with this change mix compile --warnings-as-errors is clean and mix test passes 84 with no warnings from the test tree either. The other two red cells are 1.20 on OTP 27 and 29, which are not installed here, but the checker's judgement is Elixir's and not OTP's. Refs: #13 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4oaMvbR1cbxrZBj8qwkqN
1 parent a8157d6 commit 236d517

1 file changed

Lines changed: 12 additions & 1 deletion

File tree

lib/castle/peer.ex

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -821,8 +821,19 @@ defmodule Castle.Peer do
821821
with :ok <- written, :ok <- closed, do: chmod(path, 0o600)
822822
end
823823

824+
# `:file.write/2` rather than `IO.binwrite/2`, because the handle is opened
825+
# `:raw` and a raw handle is not an io device - it is a `:file_descriptor`
826+
# record that the `IO` functions happen to accept. The difference is not
827+
# cosmetic. `IO.binwrite/2` is specified to return `:ok`, and it earns that by
828+
# calling `:file.write/2` and raising whatever error comes back, so a failed
829+
# write would leave this module by way of an `ErlangError` rather than the
830+
# `{:error, message}` every caller here is written to expect - and everything
831+
# in this module reports rather than raises, because it runs ahead of
832+
# `install_release/1` where an exception is a silent abort. `:file.write/2`
833+
# returns the error instead: `{:error, :ebadf}` writing to a handle that cannot
834+
# be written, `{:error, :einval}` to one already closed. Both measured.
824835
defp written(handle, path, bytes) do
825-
case IO.binwrite(handle, bytes) do
836+
case :file.write(handle, bytes) do
826837
:ok -> :ok
827838
{:error, reason} -> {:error, "Cannot write #{path}. #{format_error(reason)}"}
828839
end

0 commit comments

Comments
 (0)