-
-
Notifications
You must be signed in to change notification settings - Fork 407
/
stdio.jl
248 lines (228 loc) · 8.68 KB
/
stdio.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# IJulia redirects STDOUT and STDERR into "stream" messages sent to the
# Jupyter front-end.
# create a wrapper type around redirected stdio streams,
# both for overloading things like `flush` and so that we
# can set properties like `color`.
immutable IJuliaStdio{IO_t <: IO} <: Base.AbstractPipe
io::IOContext{IO_t}
end
IJuliaStdio(io::IO, stream::AbstractString="unknown") =
IJuliaStdio{typeof(io)}(IOContext(io, :color=>Base.have_color,
:jupyter_stream=>stream,
:displaysize=>displaysize()))
Base.pipe_reader(io::IJuliaStdio) = io.io.io
Base.pipe_writer(io::IJuliaStdio) = io.io.io
Base.lock(io::IJuliaStdio) = lock(io.io.io)
Base.unlock(io::IJuliaStdio) = unlock(io.io.io)
Base.in(key_value::Pair, io::IJuliaStdio) = in(key_value, io.io)
Base.haskey(io::IJuliaStdio, key) = haskey(io.io, key)
Base.getindex(io::IJuliaStdio, key) = getindex(io.io, key)
Base.get(io::IJuliaStdio, key, default) = get(io.io, key, default)
Base.displaysize(io::IJuliaStdio) = displaysize(io.io)
if VERSION >= v"0.7.0-DEV.1472" # Julia PR #23271
Base.unwrapcontext(io::IJuliaStdio) = Base.unwrapcontext(io.io)
end
# logging in verbose mode goes to original stdio streams. Use macros
# so that we do not even evaluate the arguments in no-verbose modes
function get_log_preface()
t = now()
taskname = get(task_local_storage(), :IJulia_task, "")
@sprintf("%02d:%02d:%02d(%s): ", Dates.hour(t),Dates.minute(t),Dates.second(t),taskname)
end
macro vprintln(x...)
quote
if verbose::Bool
println(orig_STDOUT[], get_log_preface(), $(map(esc, x)...))
end
end
end
macro verror_show(e, bt)
quote
if verbose::Bool
showerror(orig_STDERR[], $(esc(e)), $(esc(bt)))
end
end
end
#name=>iobuffer for each stream ("stdout","stderr") so they can be sent in flush
const bufs = Dict{String,IOBuffer}()
const stream_interval = 0.1
# maximum number of bytes in libuv/os buffer before emptying
const max_bytes = 10*1024
# max output per code cell is 512 kb by default
const max_output_per_request = Ref(1 << 19)
"""
Continually read from (size limited) Libuv/OS buffer into an `IObuffer` to avoid problems when
the Libuv/OS buffer gets full (https://github.com/JuliaLang/julia/issues/8789). Send data immediately
when buffer contains more than `max_bytes` bytes. Otherwise, if data is available it will be sent every
`stream_interval` seconds (see the Timers set up in watch_stdio). Truncate the output to `max_output_per_request`
bytes per execution request since excessive output can bring browsers to a grinding halt.
"""
function watch_stream(rd::IO, name::AbstractString)
task_local_storage(:IJulia_task, "read $name task")
try
buf = IOBuffer()
bufs[name] = buf
while !eof(rd) # blocks until something is available
nb = nb_available(rd)
if nb > 0
stdio_bytes[] += nb
# if this stream has surpassed the maximum output limit then ignore future bytes
if stdio_bytes[] >= max_output_per_request[]
read(rd, nb) # read from libuv/os buffer and discard
if stdio_bytes[] - nb < max_output_per_request[]
send_ipython(publish[], msg_pub(execute_msg, "stream",
Dict("name" => "stderr", "text" => "Excessive output truncated after $(stdio_bytes[]) bytes.")))
end
else
write(buf, read(rd, nb))
end
end
if buf.size > 0
if buf.size >= max_bytes
#send immediately
send_stream(name)
end
end
end
catch e
# the IPython manager may send us a SIGINT if the user
# chooses to interrupt the kernel; don't crash on this
if isa(e, InterruptException)
watch_stream(rd, name)
else
rethrow()
end
end
end
function send_stdio(name)
if verbose::Bool && !haskey(task_local_storage(), :IJulia_task)
task_local_storage(:IJulia_task, "send $name task")
end
send_stream(name)
end
send_stdout(t::Timer) = send_stdio("stdout")
send_stderr(t::Timer) = send_stdio("stderr")
"""
Jupyter associates cells with message headers. Once a cell's execution state has
been set as to idle, it will silently drop stream messages (i.e. output to
stdout and stderr) - see https://github.com/jupyter/notebook/issues/518.
When using Interact, and a widget's state changes, a new
message header is sent to the IJulia kernel, and while Reactive
is updating Signal graph state, it's execution state is busy, meaning Jupyter
will not drop stream messages if Interact can set the header message under which
the stream messages will be sent. Hence the need for this function.
"""
function set_cur_msg(msg)
global execute_msg = msg
end
function send_stream(name::AbstractString)
buf = bufs[name]
if buf.size > 0
d = take!(buf)
n = num_utf8_trailing(d)
dextra = d[end-(n-1):end]
resize!(d, length(d) - n)
s = String(d)
if isvalid(String, s)
write(buf, dextra) # assume that the rest of the string will be written later
length(d) == 0 && return
else
# fallback: base64-encode non-UTF8 binary data
sbuf = IOBuffer()
print(sbuf, "base64 binary data: ")
b64 = Base64EncodePipe(sbuf)
write(b64, d)
write(b64, dextra)
close(b64)
print(sbuf, '\n')
s = String(take!(sbuf))
end
send_ipython(publish[],
msg_pub(execute_msg, "stream",
Dict("name" => name, "text" => s)))
end
end
"""
If `d` ends with an incomplete UTF8-encoded character, return the number of trailing incomplete bytes.
Otherwise, return `0`.
"""
function num_utf8_trailing(d::Vector{UInt8})
i = length(d)
# find last non-continuation byte in d:
while i >= 1 && ((d[i] & 0xc0) == 0x80)
i -= 1
end
i < 1 && return 0
c = d[i]
# compute number of expected UTF-8 bytes starting at i:
n = c <= 0x7f ? 1 : c < 0xe0 ? 2 : c < 0xf0 ? 3 : 4
nend = length(d) + 1 - i # num bytes from i to end
return nend == n ? 0 : nend
end
"""
readprompt(prompt::AbstractString; password::Bool=false)
Display the `prompt` string, request user input,
and return the string entered by the user. If `password`
is `true`, the user's input is not displayed during typing.
"""
function readprompt(prompt::AbstractString; password::Bool=false)
if !execute_msg.content["allow_stdin"]
error("IJulia: this front-end does not implement stdin")
end
send_ipython(raw_input[],
msg_reply(execute_msg, "input_request",
Dict("prompt"=>prompt, "password"=>password)))
while true
msg = recv_ipython(raw_input[])
if msg.header["msg_type"] == "input_reply"
return msg.content["value"]
else
error("IJulia error: unknown stdin reply")
end
end
end
# IJulia issue #42: there doesn't seem to be a good way to make a task
# that blocks until there is a read request from STDIN ... this makes
# it very hard to properly redirect all reads from STDIN to pyin messages.
# In the meantime, however, we can just hack it so that readline works:
import Base.readline
function readline(io::IJuliaStdio)
if get(io,:jupyter_stream,"unknown") == "stdin"
return readprompt("STDIN> ")
else
readline(io.io)
end
end
function watch_stdio()
task_local_storage(:IJulia_task, "init task")
if capture_stdout
read_task = @async watch_stream(read_stdout[], "stdout")
#send STDOUT stream msgs every stream_interval secs (if there is output to send)
Timer(send_stdout, stream_interval, stream_interval)
end
if capture_stderr
readerr_task = @async watch_stream(read_stderr[], "stderr")
#send STDERR stream msgs every stream_interval secs (if there is output to send)
Timer(send_stderr, stream_interval, stream_interval)
end
end
function flush_all()
flush_cstdio() # flush writes to stdout/stderr by external C code
flush(STDOUT)
flush(STDERR)
end
function oslibuv_flush()
#refs: https://github.com/JuliaLang/IJulia.jl/issues/347#issuecomment-144505862
# https://github.com/JuliaLang/IJulia.jl/issues/347#issuecomment-144605024
@static if Compat.Sys.iswindows()
ccall(:SwitchToThread, stdcall, Void, ())
end
yield()
yield()
end
import Base.flush
function flush(io::IJuliaStdio)
flush(io.io)
oslibuv_flush()
send_stream(get(io,:jupyter_stream,"unknown"))
end