From 64b4b205d2bbbe5a6a46c170f68bd785bc739cee Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 29 Dec 2020 05:30:41 -0500 Subject: [PATCH] add timeout to prompt --- base/util.jl | 58 +++++++++++++++++++++++++++++++++++++++++++++++++--- test/misc.jl | 19 +++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/base/util.jl b/base/util.jl index c489503d40133..090a6edf7310f 100644 --- a/base/util.jl +++ b/base/util.jl @@ -278,11 +278,33 @@ end getpass(prompt::AbstractString) = getpass(stdin, stdout, prompt) """ - prompt(message; default="") -> Union{String, Nothing} + prompt(message; default="", timeout=nothing) -> Union{String, Nothing} Displays the `message` then waits for user input. Input is terminated when a newline (\\n) is encountered or EOF (^D) character is entered on a blank line. If a `default` is provided -then the user can enter just a newline character to select the `default`. +then the user can enter just a newline character to select the `default`. A `timeout` in seconds +greater than 0 can be provided, after which the default will be returned. + +For instance, the user enters a value and hits `return`: +```julia +julia> Base.prompt("Proceed? y/n"; default="n", timeout=5) +Proceed? y/n [n] timeout 5 seconds: y +"y" +``` + +The user hits `return` alone to select the default: +```julia +julia> Base.prompt("Proceed? y/n"; default="n", timeout=5) +Proceed? y/n [n] timeout 5 seconds: +"n" +``` + +The user doesn't input and hit `return` before the timeout, so default returns: +```julia +julia> Base.prompt("Proceed? y/n"; default="n", timeout=5) +Proceed? y/n [n] timeout 5 seconds: timed out +"n" +``` See also `Base.getpass` and `Base.winprompt` for secure entry of passwords. """ @@ -294,10 +316,40 @@ function prompt(input::IO, output::IO, message::AbstractString; default::Abstrac uinput = chomp(uinput) isempty(uinput) ? default : uinput end +function prompt(input::LibuvStream, output::IO, message::AbstractString; default::AbstractString="", timeout::Union{Nothing, Real} = nothing) + in_stat_before = input.status + timeout_timer = if !isnothing(timeout) && timeout > 0 + plural = timeout == 1 ? "" : "s" + msg = !isempty(default) ? "$message [$default] timeout $timeout second$(plural): " : "$message: " + Timer(timeout) do t + lock(input.cond) + input.status = StatusEOF + notify(input.cond) + unlock(input.cond) + end + else + msg = !isempty(default) ? "$message [$default]: " : "$message: " + nothing + end + print(output, msg) + uinput = readline(input, keep=true) + if !isnothing(timeout_timer) + if isopen(timeout_timer) + close(timeout_timer) + else + println(output, "timed out") + input.status = in_stat_before + return default + end + end + isempty(uinput) && return nothing # Encountered an EOF + uinput = chomp(uinput) + isempty(uinput) ? default : uinput +end # allow new prompt methods to be defined if stdin has been # redirected to some custom stream, e.g. in IJulia. -prompt(message::AbstractString; default::AbstractString="") = prompt(stdin, stdout, message, default=default) +prompt(message::AbstractString; default::AbstractString="",timeout::Union{Nothing, Real} = nothing) = prompt(stdin, stdout, message, default=default, timeout=timeout) # Windows authentication prompt if Sys.iswindows() diff --git a/test/misc.jl b/test/misc.jl index 2305b78250ef2..dbe26fb19e9ee 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -518,6 +518,25 @@ let buf = IOBuffer() @test Base.prompt(IOBuffer("blah\n"), buf, "baz", default="foobar") == "blah" end +# stdin is unavailable on the workers. Run test on master. +remotecall_fetch(1) do + let buf = IOBuffer() + original_stdin = stdin + (rd, wr) = redirect_stdin() + @test Base.prompt(rd, buf, "baz", default="foobar", timeout = 1) == "foobar" + @test String(take!(buf)) == "baz [foobar] timeout 1 second: timed out\n" + @test Base.prompt(rd, buf, "baz", default="foobar", timeout = 2) == "foobar" + @test String(take!(buf)) == "baz [foobar] timeout 2 seconds: timed out\n" + write(wr, "foo\n") + @test Base.prompt(rd, buf, "baz", default="foobar", timeout = 1) == "foo" + @test String(take!(buf)) == "baz [foobar] timeout 1 second: " + write(wr, "\n") + @test Base.prompt(rd, buf, "baz", default="foobar", timeout = 1) == "foobar" + @test String(take!(buf)) == "baz [foobar] timeout 1 second: " + redirect_stdin(original_stdin) + end +end + # these tests are not in a test block so that they will compile separately @static if Sys.iswindows() SetLastError(code) = ccall(:SetLastError, stdcall, Cvoid, (UInt32,), code)