crystal
crystal copied to clipboard
Redirecting standard streams feels more hackish/clumsy than it needs to be
This is something that came up as a discussion on my Fediverse account earlier.
Sometimes a program may need to redirect stdin/stdout/stderr programmatically at runtime, but it feels like the process to do this in Crystal is more clumsy than it needs to be. The fact that Crystal will change its own stderr's FD to something other than 2 just sorta complicates matters, leaving a program dealing with two separate stderr FD's.
The use-case where I encountered this was with my Benben program, where a C library I bind will sometimes print to stderr with no way to change this behavior via its API. This then corrupts up my TUI display, which is annoying. So I instead redirect both FD 2 and FD 5 to a file, then keeps the original real stderr in memory to prevent it from being GC'd. Doing so requires some code similar to this:
lib LibC
# Local binding for dup(). We'll name it this to prevent a possible future
# conflict.
fun remidup = "dup"(fd : LibC::Int) : LibC::Int
end
class MyProgram
class_property! errorOutput : File?
class_property! errorOutputReal : IO::FileDescriptor?
class_property origError : LibC::Int = -1
class_property origErrorReal : LibC::Int = -1
def redirectStandardFiles : Nil
filename = "/home/alexa/neat-stderr.log" # Or anything
# Open a File that we'll redirect stderr into.
newErr = File.open(filename, "w+b")
# dup() the original stderr and store it in case we need it
MyProgram.origError = LibC.remidup(STDERR.fd) # Usually fd 5?
# Change Crystal's stderr to use our new file.
STDERR.reopen(newErr)
# Create a new IO::FileDescriptor for the _real_ stderr's FD.
#
# This needs to be adjusted for Crystal versions earlier than 1.6.0 since
# they don't have "close_on_finalize"
realStderr = IO::FileDescriptor.new(2, close_on_finalize: false)
# Store both.
MyProgram.origErrorReal = realStderr.fd
MyProgram.errorOutputReal = realStderr # Don't let the GC close it
# Reopen the real stderr to use our file.
realStderr.reopen(newErr)
# Don't GC the new file
MyProgram.errorOutput = newErr
end
end
The full original code where I do this in Benben can be found here.
Having a much nicer way to do this in the stdlib, where I don't have to bind dup()
and create a new IO::FileDescriptor
would be great. Maybe something that accepts a block?