Skip to content

Commit 2422042

Browse files
tenderloveXrXr
authored andcommitted
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. [Backport #21188]
1 parent c6614d4 commit 2422042

5 files changed

Lines changed: 70 additions & 17 deletions

File tree

lib/prism/ffi.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def self.load_exported_functions_from(header, *functions, callbacks)
8080
end
8181

8282
callback :pm_parse_stream_fgets_t, [:pointer, :int, :pointer], :pointer
83+
callback :pm_parse_stream_feof_t, [:pointer], :int
8384
enum :pm_string_init_result_t, %i[PM_STRING_INIT_SUCCESS PM_STRING_INIT_ERROR_GENERIC PM_STRING_INIT_ERROR_DIRECTORY]
8485
enum :pm_string_query_t, [:PM_STRING_QUERY_ERROR, -1, :PM_STRING_QUERY_FALSE, :PM_STRING_QUERY_TRUE]
8586

@@ -95,7 +96,7 @@ def self.load_exported_functions_from(header, *functions, callbacks)
9596
"pm_string_query_local",
9697
"pm_string_query_constant",
9798
"pm_string_query_method_name",
98-
[:pm_parse_stream_fgets_t]
99+
[:pm_parse_stream_fgets_t, :pm_parse_stream_feof_t]
99100
)
100101

101102
load_exported_functions_from(
@@ -273,13 +274,15 @@ def parse_stream(stream, **options)
273274
end
274275
}
275276

277+
eof_callback = -> (_) { stream.eof? }
278+
276279
# In the pm_serialize_parse_stream function it accepts a pointer to the
277280
# IO object as a void* and then passes it through to the callback as the
278281
# third argument, but it never touches it itself. As such, since we have
279282
# access to the IO object already through the closure of the lambda, we
280283
# can pass a null pointer here and not worry.
281-
LibRubyParser.pm_serialize_parse_stream(buffer.pointer, nil, callback, dump_options(options))
282-
Prism.load(source, buffer.read)
284+
LibRubyParser.pm_serialize_parse_stream(buffer.pointer, nil, callback, eof_callback, dump_options(options))
285+
Prism.load(source, buffer.read, options.fetch(:freeze, false))
283286
end
284287
end
285288

prism/extension.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,14 @@ profile_file(int argc, VALUE *argv, VALUE self) {
879879
return Qnil;
880880
}
881881

882+
static int
883+
parse_stream_eof(void *stream) {
884+
if (rb_funcall((VALUE) stream, rb_intern("eof?"), 0)) {
885+
return 1;
886+
}
887+
return 0;
888+
}
889+
882890
/**
883891
* An implementation of fgets that is suitable for use with Ruby IO objects.
884892
*/
@@ -919,7 +927,7 @@ parse_stream(int argc, VALUE *argv, VALUE self) {
919927
pm_parser_t parser;
920928
pm_buffer_t buffer;
921929

922-
pm_node_t *node = pm_parse_stream(&parser, &buffer, (void *) stream, parse_stream_fgets, &options);
930+
pm_node_t *node = pm_parse_stream(&parser, &buffer, (void *) stream, parse_stream_fgets, parse_stream_eof, &options);
923931
rb_encoding *encoding = rb_enc_find(parser.encoding->name);
924932

925933
VALUE source = pm_source_new(&parser, encoding);

prism/prism.c

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22624,11 +22624,11 @@ pm_parse(pm_parser_t *parser) {
2262422624
* otherwise return true.
2262522625
*/
2262622626
static bool
22627-
pm_parse_stream_read(pm_buffer_t *buffer, void *stream, pm_parse_stream_fgets_t *fgets) {
22627+
pm_parse_stream_read(pm_buffer_t *buffer, void *stream, pm_parse_stream_fgets_t *stream_fgets, pm_parse_stream_feof_t *stream_feof) {
2262822628
#define LINE_SIZE 4096
2262922629
char line[LINE_SIZE];
2263022630

22631-
while (memset(line, '\n', LINE_SIZE), fgets(line, LINE_SIZE, stream) != NULL) {
22631+
while (memset(line, '\n', LINE_SIZE), stream_fgets(line, LINE_SIZE, stream) != NULL) {
2263222632
size_t length = LINE_SIZE;
2263322633
while (length > 0 && line[length - 1] == '\n') length--;
2263422634

@@ -22660,6 +22660,12 @@ pm_parse_stream_read(pm_buffer_t *buffer, void *stream, pm_parse_stream_fgets_t
2266022660
if (strncmp(line, "__END__\r\n", 9) == 0) return false;
2266122661
break;
2266222662
}
22663+
22664+
// All data should be read via gets. If the string returned by gets
22665+
// _doesn't_ end with a newline, then we assume we hit EOF condition.
22666+
if (stream_feof(stream)) {
22667+
break;
22668+
}
2266322669
}
2266422670

2266522671
return true;
@@ -22695,16 +22701,17 @@ pm_parse_stream_unterminated_heredoc_p(pm_parser_t *parser) {
2269522701
* can stream stdin in to Ruby so we need to support a streaming API.
2269622702
*/
2269722703
PRISM_EXPORTED_FUNCTION pm_node_t *
22698-
pm_parse_stream(pm_parser_t *parser, pm_buffer_t *buffer, void *stream, pm_parse_stream_fgets_t *fgets, const pm_options_t *options) {
22704+
pm_parse_stream(pm_parser_t *parser, pm_buffer_t *buffer, void *stream, pm_parse_stream_fgets_t *stream_fgets, pm_parse_stream_feof_t *stream_feof, const pm_options_t *options) {
2269922705
pm_buffer_init(buffer);
2270022706

22701-
bool eof = pm_parse_stream_read(buffer, stream, fgets);
22707+
bool eof = pm_parse_stream_read(buffer, stream, stream_fgets, stream_feof);
22708+
2270222709
pm_parser_init(parser, (const uint8_t *) pm_buffer_value(buffer), pm_buffer_length(buffer), options);
2270322710
pm_node_t *node = pm_parse(parser);
2270422711

2270522712
while (!eof && parser->error_list.size > 0 && (parser->lex_modes.index > 0 || pm_parse_stream_unterminated_heredoc_p(parser))) {
2270622713
pm_node_destroy(parser, node);
22707-
eof = pm_parse_stream_read(buffer, stream, fgets);
22714+
eof = pm_parse_stream_read(buffer, stream, stream_fgets, stream_feof);
2270822715

2270922716
pm_parser_free(parser);
2271022717
pm_parser_init(parser, (const uint8_t *) pm_buffer_value(buffer), pm_buffer_length(buffer), options);
@@ -22796,13 +22803,13 @@ pm_serialize_parse(pm_buffer_t *buffer, const uint8_t *source, size_t size, cons
2279622803
* given stream into to the given buffer.
2279722804
*/
2279822805
PRISM_EXPORTED_FUNCTION void
22799-
pm_serialize_parse_stream(pm_buffer_t *buffer, void *stream, pm_parse_stream_fgets_t *fgets, const char *data) {
22806+
pm_serialize_parse_stream(pm_buffer_t *buffer, void *stream, pm_parse_stream_fgets_t *stream_fgets, pm_parse_stream_feof_t *stream_feof, const char *data) {
2280022807
pm_parser_t parser;
2280122808
pm_options_t options = { 0 };
2280222809
pm_options_read(&options, data);
2280322810

2280422811
pm_buffer_t parser_buffer;
22805-
pm_node_t *node = pm_parse_stream(&parser, &parser_buffer, stream, fgets, &options);
22812+
pm_node_t *node = pm_parse_stream(&parser, &parser_buffer, stream, stream_fgets, stream_feof, &options);
2280622813
pm_serialize_header(buffer);
2280722814
pm_serialize_content(&parser, node, buffer);
2280822815
pm_buffer_append_byte(buffer, '\0');

prism/prism.h

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,25 @@ PRISM_EXPORTED_FUNCTION pm_node_t * pm_parse(pm_parser_t *parser);
8787
*/
8888
typedef char * (pm_parse_stream_fgets_t)(char *string, int size, void *stream);
8989

90+
/**
91+
* This function is used in pm_parse_stream to check whether a stream is EOF.
92+
* It closely mirrors that of feof so that feof can be used as the
93+
* default implementation.
94+
*/
95+
typedef int (pm_parse_stream_feof_t)(void *stream);
96+
9097
/**
9198
* Parse a stream of Ruby source and return the tree.
9299
*
93100
* @param parser The parser to use.
94101
* @param buffer The buffer to use.
95102
* @param stream The stream to parse.
96-
* @param fgets The function to use to read from the stream.
103+
* @param stream_fgets The function to use to read from the stream.
104+
* @param stream_feof The function to use to determine if the stream has hit eof.
97105
* @param options The optional options to use when parsing.
98106
* @return The AST representing the source.
99107
*/
100-
PRISM_EXPORTED_FUNCTION pm_node_t * pm_parse_stream(pm_parser_t *parser, pm_buffer_t *buffer, void *stream, pm_parse_stream_fgets_t *fgets, const pm_options_t *options);
108+
PRISM_EXPORTED_FUNCTION pm_node_t * pm_parse_stream(pm_parser_t *parser, pm_buffer_t *buffer, void *stream, pm_parse_stream_fgets_t *stream_fgets, pm_parse_stream_feof_t *stream_feof, const pm_options_t *options);
101109

102110
// We optionally support serializing to a binary string. For systems that don't
103111
// want or need this functionality, it can be turned off with the
@@ -110,10 +118,11 @@ PRISM_EXPORTED_FUNCTION pm_node_t * pm_parse_stream(pm_parser_t *parser, pm_buff
110118
*
111119
* @param buffer The buffer to serialize to.
112120
* @param stream The stream to parse.
113-
* @param fgets The function to use to read from the stream.
121+
* @param stream_fgets The function to use to read from the stream.
122+
* @param stream_feof The function to use to tell if the stream has hit eof.
114123
* @param data The optional data to pass to the parser.
115124
*/
116-
PRISM_EXPORTED_FUNCTION void pm_serialize_parse_stream(pm_buffer_t *buffer, void *stream, pm_parse_stream_fgets_t *fgets, const char *data);
125+
PRISM_EXPORTED_FUNCTION void pm_serialize_parse_stream(pm_buffer_t *buffer, void *stream, pm_parse_stream_fgets_t *stream_fgets, pm_parse_stream_feof_t *stream_feof, const char *data);
117126

118127
/**
119128
* Serialize the given list of comments to the given buffer.

prism_compile.c

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11414,6 +11414,18 @@ pm_parse_string(pm_parse_result_t *result, VALUE source, VALUE filepath, VALUE *
1141411414
return pm_parse_process(result, node, script_lines);
1141511415
}
1141611416

11417+
struct rb_stdin_wrapper {
11418+
VALUE rb_stdin;
11419+
int eof_seen;
11420+
};
11421+
11422+
static int
11423+
pm_parse_stdin_eof(void *stream)
11424+
{
11425+
struct rb_stdin_wrapper * wrapped_stdin = (struct rb_stdin_wrapper *)stream;
11426+
return wrapped_stdin->eof_seen;
11427+
}
11428+
1141711429
/**
1141811430
* An implementation of fgets that is suitable for use with Ruby IO objects.
1141911431
*/
@@ -11422,7 +11434,9 @@ pm_parse_stdin_fgets(char *string, int size, void *stream)
1142211434
{
1142311435
RUBY_ASSERT(size > 0);
1142411436

11425-
VALUE line = rb_funcall((VALUE) stream, rb_intern("gets"), 1, INT2FIX(size - 1));
11437+
struct rb_stdin_wrapper * wrapped_stdin = (struct rb_stdin_wrapper *)stream;
11438+
11439+
VALUE line = rb_funcall(wrapped_stdin->rb_stdin, rb_intern("gets"), 1, INT2FIX(size - 1));
1142611440
if (NIL_P(line)) {
1142711441
return NULL;
1142811442
}
@@ -11433,6 +11447,13 @@ pm_parse_stdin_fgets(char *string, int size, void *stream)
1143311447
memcpy(string, cstr, length);
1143411448
string[length] = '\0';
1143511449

11450+
// We're reading strings from stdin via gets. We'll assume that if the
11451+
// string is smaller than the requested length, and doesn't end with a
11452+
// newline, that we hit EOF.
11453+
if (length < (size - 1) && string[length - 1] != '\n') {
11454+
wrapped_stdin->eof_seen = 1;
11455+
}
11456+
1143611457
return string;
1143711458
}
1143811459

@@ -11449,8 +11470,13 @@ pm_parse_stdin(pm_parse_result_t *result)
1144911470
{
1145011471
pm_options_frozen_string_literal_init(&result->options);
1145111472

11473+
struct rb_stdin_wrapper wrapped_stdin = {
11474+
rb_stdin,
11475+
0
11476+
};
11477+
1145211478
pm_buffer_t buffer;
11453-
pm_node_t *node = pm_parse_stream(&result->parser, &buffer, (void *) rb_stdin, pm_parse_stdin_fgets, &result->options);
11479+
pm_node_t *node = pm_parse_stream(&result->parser, &buffer, (void *) &wrapped_stdin, pm_parse_stdin_fgets, pm_parse_stdin_eof, &result->options);
1145411480

1145511481
// Copy the allocated buffer contents into the input string so that it gets
1145611482
// freed. At this point we've handed over ownership, so we don't need to

0 commit comments

Comments
 (0)