When reading from stdin, put a wrapper around the IO object

The purpose of this commit is to fix Bug #21188.  We need to detect when
stdin has run in to an EOF case.  Unfortunately we can't _call_ the eof
function on IO because it will block.

Here is a short script to demonstrate the issue:

```ruby
x = STDIN.gets
puts x
puts x.eof?
```

If you run the script, then type some characters (but _NOT_ a newline),
then hit Ctrl-D twice, it will print the input string.  Unfortunately,
calling `eof?` will try to read from STDIN again causing us to need a
3rd Ctrl-D to exit the program.

Before introducing the EOF callback to Prism, the input loop looked
kind of like this:

```ruby
loop do
  str = STDIN.gets
  process(str)

  if str.nil?
    p :DONE
  end
end
```

Which required 3 Ctrl-D to exit.  If we naively changed it to something
like this:

```ruby
loop do
  str = STDIN.gets
  process(str)

  if STDIN.eof?
    p :DONE
  end
end
```

It would still require 3 Ctrl-D because `eof?` would block.  In this
patch, we're wrapping the IO object, checking the buffer for a newline
and length, and then using that to simulate a non-blocking eof? method.

This commit wraps STDIN and emulates a non-blocking `eof` function.

[Bug #21188]
This commit is contained in:
Aaron Patterson 2025-07-17 15:20:20 -07:00 committed by Aaron Patterson
parent 1c6b36af18
commit 89d89fa49d
5 changed files with 66 additions and 13 deletions

View file

@ -11492,6 +11492,18 @@ pm_parse_string(pm_parse_result_t *result, VALUE source, VALUE filepath, VALUE *
return pm_parse_process(result, node, script_lines);
}
struct rb_stdin_wrapper {
VALUE rb_stdin;
int eof_seen;
};
static int
pm_parse_stdin_eof(void *stream)
{
struct rb_stdin_wrapper * wrapped_stdin = (struct rb_stdin_wrapper *)stream;
return wrapped_stdin->eof_seen;
}
/**
* An implementation of fgets that is suitable for use with Ruby IO objects.
*/
@ -11500,7 +11512,9 @@ pm_parse_stdin_fgets(char *string, int size, void *stream)
{
RUBY_ASSERT(size > 0);
VALUE line = rb_funcall((VALUE) stream, rb_intern("gets"), 1, INT2FIX(size - 1));
struct rb_stdin_wrapper * wrapped_stdin = (struct rb_stdin_wrapper *)stream;
VALUE line = rb_funcall(wrapped_stdin->rb_stdin, rb_intern("gets"), 1, INT2FIX(size - 1));
if (NIL_P(line)) {
return NULL;
}
@ -11511,6 +11525,13 @@ pm_parse_stdin_fgets(char *string, int size, void *stream)
memcpy(string, cstr, length);
string[length] = '\0';
// We're reading strings from stdin via gets. We'll assume that if the
// string is smaller than the requested length, and doesn't end with a
// newline, that we hit EOF.
if (length < (size - 1) && string[length - 1] != '\n') {
wrapped_stdin->eof_seen = 1;
}
return string;
}
@ -11527,8 +11548,13 @@ pm_parse_stdin(pm_parse_result_t *result)
{
pm_options_frozen_string_literal_init(&result->options);
struct rb_stdin_wrapper wrapped_stdin = {
rb_stdin,
0
};
pm_buffer_t buffer;
pm_node_t *node = pm_parse_stream(&result->parser, &buffer, (void *) rb_stdin, pm_parse_stdin_fgets, &result->options);
pm_node_t *node = pm_parse_stream(&result->parser, &buffer, (void *) &wrapped_stdin, pm_parse_stdin_fgets, pm_parse_stdin_eof, &result->options);
// Copy the allocated buffer contents into the input string so that it gets
// freed. At this point we've handed over ownership, so we don't need to