crystal run test.cr <&-
Unhandled exception: Unable to get info: Bad file descriptor (IO::Error)
from /opt/homebrew/Cellar/crystal/1.12.1_1/bin/crystal in 'raise<IO::Error>:NoReturn'
from /opt/homebrew/Cellar/crystal/1.12.1_1/bin/crystal in 'Crystal::System::FileDescriptor::system_info<Int32>:File::Info'
from /opt/homebrew/Cellar/crystal/1.12.1_1/bin/crystal in 'IO::FileDescriptor::new<Int32>:IO::FileDescriptor'
from /opt/homebrew/Cellar/crystal/1.12.1_1/bin/crystal in 'IO::FileDescriptor::from_stdio<Int32>:IO::FileDescriptor'
from /opt/homebrew/Cellar/crystal/1.12.1_1/bin/crystal in '__crystal_main'
from /opt/homebrew/Cellar/crystal/1.12.1_1/bin/crystal in 'main'
There’s no error when I first build test.cr:
crystal build test.cr
./test <&-
However, with some other code, I have also seen the error after building.
In that case, I managed to get rid of the error like so (that doesn’t help in the case of test.cr above though):
# fix for closed STDIN:
require "io/file_descriptor"
class IO::FileDescriptor < IO
def initialize(fd, blocking = nil, *, @close_on_finalize = true)
@volatile_fd = Atomic.new(fd)
@closed = system_closed?
# --- new --------------------- {
if ! @closed
# --- new --------------------- }
if blocking.nil?
blocking =
case system_info.type
when .pipe?, .socket?, .character_device?
false
else
true
end
end
system_blocking_init(blocking)
# --- new --------------------- {
end
# --- new --------------------- }
end
end
I have yet to come up with a minimal example of how to reproduce the problem in that case.
puts STDIN.closed? # or STDIN.tty?, or STDIN.class, ...
crystal build --release test.cr && ./test <&-
Unhandled exception: Unable to get info: Bad file descriptor (IO::Error)
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/system_error.cr:75 in 'system_info'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/io/file_descriptor.cr:48:14 in 'new'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/io/buffered.cr:240:5 in 'from_stdio'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/kernel.cr:25:1 in '__crystal_main'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/main.cr:129:5 in 'main'
crystal build test.cr && ./test <&-
Unhandled exception: Unable to get info: Bad file descriptor (IO::Error)
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/raise.cr:219:1 in 'raise'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/system/unix/file_descriptor.cr:81:7 in 'system_info'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/system/unix/file_descriptor.cr:88:5 in 'system_info'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/io/file_descriptor.cr:48:14 in 'initialize'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/io/file_descriptor.cr:42:3 in 'new'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/system/unix/file_descriptor.cr:190:12 in 'from_stdio'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/io/file_descriptor.cr:61:5 in 'from_stdio'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/kernel.cr:8:9 in '~STDIN:const_init'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/kernel.cr:25:1 in '__crystal_main'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/main.cr:129:5 in 'main_user_code'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/main.cr:115:7 in 'main'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/main.cr:141:3 in 'main'
This problem goes away if I fix it as described above.
No error for closed STDERR.
Note that STDERR.closed? reports false.
STDOUT:
test.cr empty:
crystal build test.cr && ./test >&-
Unhandled exception: Unable to get info: Bad file descriptor (IO::Error)
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/raise.cr:219:1 in 'raise'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/system/unix/file_descriptor.cr:81:7 in 'system_info'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/system/unix/file_descriptor.cr:88:5 in 'system_info'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/io/file_descriptor.cr:48:14 in 'initialize'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/io/file_descriptor.cr:42:3 in 'new'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/system/unix/file_descriptor.cr:190:12 in 'from_stdio'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/io/file_descriptor.cr:61:5 in 'from_stdio'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/kernel.cr:25:10 in '~STDOUT:const_init'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/kernel.cr:42:1 in '__crystal_main'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/main.cr:129:5 in 'main_user_code'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/main.cr:115:7 in 'main'
from /opt/homebrew/Cellar/crystal/1.12.1_1/share/crystal/src/crystal/main.cr:141:3 in 'main'
zsh: segmentation fault ./test >&-
This is probably be due to the fact that the standard stream constants are not mapped to the actual file descriptors. They are cloned from the original ones. So STDERR is not file descriptor 2, but a different file descriptor (typically 4 in a normal program; you can check STDERR.fd). And the closed state apparently doesn’t pass over to the clone.
Also, for STDERR closed (2>&-) .tty? still returns true, for whatever reason.
For STDERR redirected (2>>/dev/null) things seem to work ok.
I get the impression that somewhere file descriptors might get mixed up.
Note how STDERR.fd usually is 4, but if STDERR is closed, STDERR.fd is 3. Is that expected?
If you close a file descriptor, that number becomes available for the next opened file descriptor. This is of course quite brittle. So it’s usually not recommended to close standard streams because this causes confusion.
Note that the closed? method is not invoking a system call to check the actual status but just checking an instance variable. So that creating new file descriptors will not be considered closed until that particular instance is closed from within crystal.
Yes, I’ve seen that system_closed? is cached.
Is that the expected behavior in general?
Is it the expected behavior if the file descriptor was closed right from the start?
Hm yeah the semantics of FileDescriptor#closed? seem quite unexpected.
I think it should always reflect the state of the sytem file descriptor, not the wrapper object.
Yeah, I don’t think Ruby needs to read from the standard streams non-blocking.
The reason for this duplication is that we want to read non-blockingly so we can swap in another fiber if we’re waiting for IO. But we shouldn’t modify the original file descriptors because they’re inherited from the parent process and would reflect back. More context: Use blocking IO on a TTY if it can't be reopened by Timbus · Pull Request #6660 · crystal-lang/crystal · GitHub