Skip to content

Commit 7801e85

Browse files
committed
Add interruptible console input timeout
Add a `timeout` option to `IO#console_input_events` so callers can wait for input while remaining responsive to Ruby interrupts.
1 parent 9c7cb6d commit 7801e85

2 files changed

Lines changed: 106 additions & 25 deletions

File tree

ext/io/console/console.c

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ getattr(int fd, conmode *t)
8888

8989
#define CSI "\x1b\x5b"
9090

91-
static ID id_getc, id_close;
91+
static ID id_getc, id_close, id_timeout;
9292
static ID id_gets, id_flush, id_chomp_bang;
9393

9494
#ifndef HAVE_RB_INTERNED_STR_CSTR
@@ -1058,22 +1058,48 @@ console_set_winsize(VALUE io, VALUE size)
10581058
#endif
10591059

10601060
#ifdef _WIN32
1061+
enum console_input_handle_index {
1062+
console_input_handle,
1063+
console_input_wakeup,
1064+
console_input_handle_count
1065+
};
1066+
10611067
typedef struct {
1062-
HANDLE handle;
1068+
HANDLE handles[console_input_handle_count];
10631069
INPUT_RECORD *records;
10641070
DWORD length;
10651071
DWORD count;
1072+
DWORD timeout;
1073+
DWORD wait_result;
1074+
DWORD error;
10661075
BOOL result;
10671076
} read_console_input_args_t;
10681077

10691078
static void *
10701079
nogvl_read_console_input(void *ptr)
10711080
{
10721081
read_console_input_args_t *args = ptr;
1073-
args->result = ReadConsoleInputW(args->handle, args->records, args->length, &args->count);
1082+
1083+
args->wait_result = WaitForMultipleObjects(console_input_handle_count,
1084+
args->handles, FALSE, args->timeout);
1085+
if (args->wait_result == WAIT_OBJECT_0 + console_input_handle) {
1086+
args->result = ReadConsoleInputW(args->handles[console_input_handle],
1087+
args->records, args->length, &args->count);
1088+
if (!args->result) args->error = GetLastError();
1089+
}
1090+
else if (args->wait_result == WAIT_FAILED) {
1091+
args->error = GetLastError();
1092+
}
10741093
return 0;
10751094
}
10761095

1096+
static void
1097+
ubf_console_input(void *ptr)
1098+
{
1099+
read_console_input_args_t *args = ptr;
1100+
SetEvent(args->handles[console_input_wakeup]);
1101+
}
1102+
10771103
static void
10781104
console_input_event_set(VALUE event, const char *name, VALUE value)
10791105
{
@@ -1126,12 +1152,45 @@ console_input_event(const INPUT_RECORD *record)
11261152
return event;
11271153
}
11281154

1155+
static VALUE
1156+
console_input_events_read(VALUE vargs)
1157+
{
1158+
read_console_input_args_t *args = (read_console_input_args_t *)vargs;
1159+
VALUE events;
1160+
DWORD i;
1161+
1162+
rb_thread_call_without_gvl(nogvl_read_console_input, args,
1163+
ubf_console_input, args);
1164+
if (args->wait_result == WAIT_TIMEOUT) return rb_ary_new();
1165+
if (args->wait_result != WAIT_OBJECT_0 + console_input_handle ||
1166+
!args->result) {
1167+
rb_syserr_fail(rb_w32_map_errno(args->error), 0);
1168+
}
1169+
1170+
events = rb_ary_new_capa(args->count);
1171+
for (i = 0; i < args->count; ++i) {
1172+
rb_ary_push(events, console_input_event(&args->records[i]));
1173+
}
1174+
return events;
1175+
}
1176+
1177+
static VALUE
1178+
console_input_events_ensure(VALUE vargs)
1179+
{
1180+
read_console_input_args_t *args = (read_console_input_args_t *)vargs;
1181+
1182+
CloseHandle(args->handles[console_input_wakeup]);
1183+
xfree(args->records);
1184+
return Qnil;
1185+
}
1186+
11291187
/*
11301188
* call-seq:
1131-
* io.console_input_events([max_events]) -> array
1189+
* io.console_input_events([max_events], timeout: nil) -> array
11321190
*
11331191
* Reads up to +max_events+ console input events, preserving their order.
1134-
* The default is one event. Blocks until at least one event is available.
1192+
* The default is one event. Blocks until at least one event is available,
1193+
* or for +timeout+ seconds if specified. Returns an empty Array on timeout.
11351194
*
11361195
* Each event is returned as a Hash. The +:type+ and remaining keys are:
11371196
*
@@ -1150,37 +1209,45 @@ console_input_event(const INPUT_RECORD *record)
11501209
static VALUE
11511210
console_input_events(int argc, VALUE *argv, VALUE io)
11521211
{
1153-
VALUE vmax;
1212+
VALUE vmax = Qnil, vopts = Qnil, vtimeout = Qundef;
1213+
VALUE values[1];
1214+
ID keywords[1] = {id_timeout};
11541215
DWORD max_events = 1;
11551216
read_console_input_args_t args;
1156-
VALUE event_buffer = 0;
1157-
VALUE events;
1158-
DWORD i;
11591217

1160-
rb_scan_args(argc, argv, "01", &vmax);
1218+
rb_scan_args(argc, argv, "01:", &vmax, &vopts);
1219+
if (rb_get_kwargs(vopts, keywords, 0, 1, values)) {
1220+
vtimeout = values[0];
1221+
}
11611222
if (!NIL_P(vmax)) {
11621223
max_events = NUM2UINT(vmax);
11631224
if (max_events == 0) rb_raise(rb_eArgError, "max_events must be positive");
11641225
}
11651226

1166-
args.handle = (HANDLE)rb_w32_get_osfhandle(GetReadFD(io));
1167-
args.records = ALLOCV_N(INPUT_RECORD, event_buffer, max_events);
1168-
args.length = max_events;
1169-
args.count = 0;
1170-
args.result = FALSE;
1171-
rb_thread_call_without_gvl(nogvl_read_console_input, &args, RUBY_UBF_IO, 0);
1172-
if (!args.result) {
1173-
int error = LAST_ERROR;
1174-
ALLOCV_END(event_buffer);
1175-
rb_syserr_fail(error, 0);
1227+
args.timeout = INFINITE;
1228+
if (!NIL_OR_UNDEF_P(vtimeout)) {
1229+
struct timeval timeout = rb_time_interval(vtimeout);
1230+
uint64_t milliseconds = (uint64_t)timeout.tv_sec * 1000;
1231+
milliseconds += ((uint64_t)timeout.tv_usec + 999) / 1000;
1232+
args.timeout = milliseconds < INFINITE ? (DWORD)milliseconds : INFINITE - 1;
11761233
}
11771234

1178-
events = rb_ary_new_capa(args.count);
1179-
for (i = 0; i < args.count; ++i) {
1180-
rb_ary_push(events, console_input_event(&args.records[i]));
1235+
args.handles[console_input_handle] =
1236+
(HANDLE)rb_w32_get_osfhandle(GetReadFD(io));
1237+
args.records = ALLOC_N(INPUT_RECORD, max_events);
1238+
args.handles[console_input_wakeup] = CreateEvent(NULL, FALSE, FALSE, NULL);
1239+
if (!args.handles[console_input_wakeup]) {
1240+
int error = LAST_ERROR;
1241+
xfree(args.records);
1242+
rb_syserr_fail(error, 0);
11811243
}
1182-
ALLOCV_END(event_buffer);
1183-
return events;
1244+
args.length = max_events;
1245+
args.count = 0;
1246+
args.wait_result = WAIT_FAILED;
1247+
args.error = ERROR_SUCCESS;
1248+
args.result = FALSE;
1249+
return rb_ensure(console_input_events_read, (VALUE)&args,
1250+
console_input_events_ensure, (VALUE)&args);
11841251
}
11851252

11861253
/*
@@ -2324,6 +2391,7 @@ Init_console(void)
23242391
id_flush = rb_intern("flush");
23252392
id_chomp_bang = rb_intern("chomp!");
23262393
id_close = rb_intern("close");
2394+
id_timeout = rb_intern("timeout");
23272395
#define init_rawmode_opt_id(name) \
23282396
rawmode_opt_ids[kwd_##name] = rb_intern(#name)
23292397
init_rawmode_opt_id(min);

test/io/console/test_io_console.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,19 @@ def test_console_input_events
859859
events[index, 5],
860860
)
861861
assert_raise(ArgumentError) {IO.console.console_input_events(0)}
862+
assert_raise(ArgumentError) {IO.console.console_input_events(timeout: -1)}
863+
864+
assert_equal([], IO.console.console_input_events(128, timeout: 0.01))
865+
started = Queue.new
866+
thread = Thread.new do
867+
Thread.current.report_on_exception = false
868+
started << true
869+
IO.console.console_input_events(128, timeout: 100)
870+
end
871+
started.pop
872+
sleep 0.1
873+
thread.raise(Interrupt)
874+
assert_raise(Interrupt) {thread.value}
862875
end
863876

864877
def test_check_winsize_changed_deprecated

0 commit comments

Comments
 (0)