crystal icon indicating copy to clipboard operation
crystal copied to clipboard

Redirecting standard streams feels more hackish/clumsy than it needs to be

Open MistressRemilia opened this issue 9 months ago • 3 comments

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?

MistressRemilia avatar May 09 '24 02:05 MistressRemilia