1193 lines
36 KiB
Julia
1193 lines
36 KiB
Julia
# This file is a part of Julia. License is MIT: https://julialang.org/license
|
|
|
|
import .Libc: RawFD, dup
|
|
if is_windows()
|
|
import .Libc: WindowsRawSocket
|
|
end
|
|
|
|
## types ##
|
|
abstract type IOServer end
|
|
abstract type LibuvServer <: IOServer end
|
|
abstract type LibuvStream <: IO end
|
|
|
|
# IO
|
|
# +- AbstractIOBuffer{T<:AbstractArray{UInt8,1}} (not exported)
|
|
# +- AbstractPipe (not exported)
|
|
# . +- Pipe
|
|
# . +- Process (not exported)
|
|
# . +- ProcessChain (not exported)
|
|
# +- Base64DecodePipe
|
|
# +- Base64EncodePipe
|
|
# +- BufferStream
|
|
# +- DevNullStream (not exported)
|
|
# +- Filesystem.File
|
|
# +- LibuvStream (not exported)
|
|
# . +- PipeEndpoint (not exported)
|
|
# . +- TCPSocket
|
|
# . +- TTY (not exported)
|
|
# . +- UDPSocket
|
|
# +- IOBuffer = Base.AbstractIOBuffer{Array{UInt8,1}}
|
|
# +- IOStream
|
|
|
|
# IOServer
|
|
# +- LibuvServer
|
|
# . +- PipeServer
|
|
# . +- TCPServer
|
|
|
|
# Redirectable = Union{IO, FileRedirect, Libc.RawFD} (not exported)
|
|
|
|
function stream_wait(x, c...) # for x::LibuvObject
|
|
preserve_handle(x)
|
|
try
|
|
return wait(c...)
|
|
finally
|
|
unpreserve_handle(x)
|
|
end
|
|
end
|
|
|
|
nb_available(s::LibuvStream) = nb_available(s.buffer)
|
|
|
|
function eof(s::LibuvStream)
|
|
if isopen(s) # fast path
|
|
nb_available(s) > 0 && return false
|
|
else
|
|
return nb_available(s) <= 0
|
|
end
|
|
wait_readnb(s,1)
|
|
return !isopen(s) && nb_available(s) <= 0
|
|
end
|
|
|
|
const DEFAULT_READ_BUFFER_SZ = 10485760 # 10 MB
|
|
|
|
const StatusUninit = 0 # handle is allocated, but not initialized
|
|
const StatusInit = 1 # handle is valid, but not connected/active
|
|
const StatusConnecting = 2 # handle is in process of connecting
|
|
const StatusOpen = 3 # handle is usable
|
|
const StatusActive = 4 # handle is listening for read/write/connect events
|
|
const StatusClosing = 5 # handle is closing / being closed
|
|
const StatusClosed = 6 # handle is closed
|
|
const StatusEOF = 7 # handle is a TTY that has seen an EOF event
|
|
const StatusPaused = 8 # handle is Active, but not consuming events, and will transition to Open if it receives an event
|
|
function uv_status_string(x)
|
|
s = x.status
|
|
if x.handle == C_NULL
|
|
if s == StatusClosed
|
|
return "closed"
|
|
elseif s == StatusUninit
|
|
return "null"
|
|
end
|
|
return "invalid status"
|
|
elseif s == StatusUninit
|
|
return "uninit"
|
|
elseif s == StatusInit
|
|
return "init"
|
|
elseif s == StatusConnecting
|
|
return "connecting"
|
|
elseif s == StatusOpen
|
|
return "open"
|
|
elseif s == StatusActive
|
|
return "active"
|
|
elseif s == StatusPaused
|
|
return "paused"
|
|
elseif s == StatusClosing
|
|
return "closing"
|
|
elseif s == StatusClosed
|
|
return "closed"
|
|
elseif s == StatusEOF
|
|
return "eof"
|
|
end
|
|
return "invalid status"
|
|
end
|
|
|
|
mutable struct PipeEndpoint <: LibuvStream
|
|
handle::Ptr{Void}
|
|
status::Int
|
|
buffer::IOBuffer
|
|
readnotify::Condition
|
|
connectnotify::Condition
|
|
closenotify::Condition
|
|
sendbuf::Nullable{IOBuffer}
|
|
lock::ReentrantLock
|
|
throttle::Int
|
|
|
|
PipeEndpoint() = PipeEndpoint(Libc.malloc(_sizeof_uv_named_pipe), StatusUninit)
|
|
function PipeEndpoint(handle::Ptr{Void}, status)
|
|
p = new(handle,
|
|
status,
|
|
PipeBuffer(),
|
|
Condition(),
|
|
Condition(),
|
|
Condition(),
|
|
nothing,
|
|
ReentrantLock(),
|
|
DEFAULT_READ_BUFFER_SZ)
|
|
associate_julia_struct(handle, p)
|
|
finalizer(p, uvfinalize)
|
|
return p
|
|
end
|
|
end
|
|
|
|
mutable struct PipeServer <: LibuvServer
|
|
handle::Ptr{Void}
|
|
status::Int
|
|
connectnotify::Condition
|
|
closenotify::Condition
|
|
function PipeServer(handle::Ptr{Void}, status)
|
|
p = new(handle,
|
|
status,
|
|
Condition(),
|
|
Condition())
|
|
associate_julia_struct(p.handle, p)
|
|
finalizer(p, uvfinalize)
|
|
return p
|
|
end
|
|
end
|
|
|
|
const LibuvPipe = Union{PipeEndpoint, PipeServer}
|
|
|
|
function PipeServer()
|
|
p = PipeServer(Libc.malloc(_sizeof_uv_named_pipe), StatusUninit)
|
|
return init_pipe!(p; readable=true)
|
|
end
|
|
|
|
mutable struct TTY <: LibuvStream
|
|
handle::Ptr{Void}
|
|
status::Int
|
|
buffer::IOBuffer
|
|
readnotify::Condition
|
|
closenotify::Condition
|
|
sendbuf::Nullable{IOBuffer}
|
|
lock::ReentrantLock
|
|
throttle::Int
|
|
@static if is_windows(); ispty::Bool; end
|
|
TTY() = TTY(Libc.malloc(_sizeof_uv_tty), StatusUninit)
|
|
function TTY(handle::Ptr{Void}, status)
|
|
tty = new(
|
|
handle,
|
|
status,
|
|
PipeBuffer(),
|
|
Condition(),
|
|
Condition(),
|
|
nothing, ReentrantLock(),
|
|
DEFAULT_READ_BUFFER_SZ)
|
|
associate_julia_struct(handle, tty)
|
|
finalizer(tty, uvfinalize)
|
|
@static if is_windows()
|
|
tty.ispty = ccall(:jl_ispty, Cint, (Ptr{Void},), handle) != 0
|
|
end
|
|
return tty
|
|
end
|
|
end
|
|
|
|
function TTY(fd::RawFD; readable::Bool = false)
|
|
tty = TTY()
|
|
# This needs to go after associate_julia_struct so that there
|
|
# is no garbage in the ->data field
|
|
err = ccall(:uv_tty_init, Int32, (Ptr{Void}, Ptr{Void}, Int32, Int32),
|
|
eventloop(), tty.handle, fd.fd, readable)
|
|
uv_error("TTY", err)
|
|
tty.status = StatusOpen
|
|
return tty
|
|
end
|
|
|
|
show(io::IO, stream::LibuvServer) = print(io, typeof(stream), "(",
|
|
_fd(stream), " ",
|
|
uv_status_string(stream), ")")
|
|
show(io::IO, stream::LibuvStream) = print(io, typeof(stream), "(",
|
|
_fd(stream), " ",
|
|
uv_status_string(stream), ", ",
|
|
nb_available(stream.buffer)," bytes waiting)")
|
|
|
|
# Shared LibuvStream object interface
|
|
|
|
function isreadable(io::LibuvStream)
|
|
nb_available(io) > 0 && return true
|
|
isopen(io) || return false
|
|
return ccall(:uv_is_readable, Cint, (Ptr{Void},), io.handle) != 0
|
|
end
|
|
|
|
function iswritable(io::LibuvStream)
|
|
isopen(io) || return false
|
|
io.status == StatusClosing && return false
|
|
return ccall(:uv_is_writable, Cint, (Ptr{Void},), io.handle) != 0
|
|
end
|
|
|
|
lock(s::LibuvStream) = lock(s.lock)
|
|
unlock(s::LibuvStream) = unlock(s.lock)
|
|
|
|
uvtype(::LibuvStream) = UV_STREAM
|
|
uvhandle(stream::LibuvStream) = stream.handle
|
|
unsafe_convert(::Type{Ptr{Void}}, s::Union{LibuvStream, LibuvServer}) = s.handle
|
|
|
|
function init_stdio(handle::Ptr{Void})
|
|
t = ccall(:jl_uv_handle_type, Int32, (Ptr{Void},), handle)
|
|
if t == UV_FILE
|
|
return fdio(ccall(:jl_uv_file_handle, Int32, (Ptr{Void},), handle))
|
|
# Replace ios.c file with libuv file?
|
|
# return File(RawFD(ccall(:jl_uv_file_handle,Int32,(Ptr{Void},),handle)))
|
|
else
|
|
if t == UV_TTY
|
|
ret = TTY(handle, StatusOpen)
|
|
elseif t == UV_TCP
|
|
ret = TCPSocket(handle, StatusOpen)
|
|
elseif t == UV_NAMED_PIPE
|
|
ret = PipeEndpoint(handle, StatusOpen)
|
|
else
|
|
throw(ArgumentError("invalid stdio type: $t"))
|
|
end
|
|
return ret
|
|
end
|
|
end
|
|
|
|
function isopen(x::Union{LibuvStream, LibuvServer})
|
|
if x.status == StatusUninit || x.status == StatusInit
|
|
throw(ArgumentError("$x is not initialized"))
|
|
end
|
|
x.status != StatusClosed && x.status != StatusEOF
|
|
end
|
|
|
|
function check_open(x::Union{LibuvStream, LibuvServer})
|
|
if !isopen(x) || x.status == StatusClosing
|
|
throw(ArgumentError("stream is closed or unusable"))
|
|
end
|
|
end
|
|
|
|
function wait_connected(x::Union{LibuvStream, LibuvServer})
|
|
check_open(x)
|
|
while x.status == StatusConnecting
|
|
stream_wait(x, x.connectnotify)
|
|
check_open(x)
|
|
end
|
|
end
|
|
|
|
function wait_readbyte(x::LibuvStream, c::UInt8)
|
|
if isopen(x) # fast path
|
|
search(x.buffer, c) > 0 && return
|
|
else
|
|
return
|
|
end
|
|
preserve_handle(x)
|
|
try
|
|
while isopen(x) && search(x.buffer, c) <= 0
|
|
start_reading(x) # ensure we are reading
|
|
wait(x.readnotify)
|
|
end
|
|
finally
|
|
if isempty(x.readnotify.waitq)
|
|
stop_reading(x) # stop reading iff there are currently no other read clients of the stream
|
|
end
|
|
unpreserve_handle(x)
|
|
end
|
|
nothing
|
|
end
|
|
|
|
function wait_readnb(x::LibuvStream, nb::Int)
|
|
if isopen(x) # fast path
|
|
nb_available(x.buffer) >= nb && return
|
|
else
|
|
return
|
|
end
|
|
oldthrottle = x.throttle
|
|
preserve_handle(x)
|
|
try
|
|
while isopen(x) && nb_available(x.buffer) < nb
|
|
x.throttle = max(nb, x.throttle)
|
|
start_reading(x) # ensure we are reading
|
|
wait(x.readnotify)
|
|
end
|
|
finally
|
|
if isempty(x.readnotify.waitq)
|
|
stop_reading(x) # stop reading iff there are currently no other read clients of the stream
|
|
end
|
|
if oldthrottle <= x.throttle <= nb
|
|
x.throttle = oldthrottle
|
|
end
|
|
unpreserve_handle(x)
|
|
end
|
|
nothing
|
|
end
|
|
|
|
function wait_close(x::Union{LibuvStream, LibuvServer})
|
|
if isopen(x)
|
|
stream_wait(x, x.closenotify)
|
|
end
|
|
nothing
|
|
end
|
|
|
|
function close(stream::Union{LibuvStream, LibuvServer})
|
|
if stream.status == StatusInit
|
|
ccall(:jl_forceclose_uv, Void, (Ptr{Void},), stream.handle)
|
|
elseif isopen(stream)
|
|
if stream.status != StatusClosing
|
|
ccall(:jl_close_uv, Void, (Ptr{Void},), stream.handle)
|
|
stream.status = StatusClosing
|
|
end
|
|
if uv_handle_data(stream) != C_NULL
|
|
stream_wait(stream, stream.closenotify)
|
|
end
|
|
end
|
|
nothing
|
|
end
|
|
|
|
function uvfinalize(uv::Union{LibuvStream, LibuvServer})
|
|
if uv.handle != C_NULL
|
|
disassociate_julia_struct(uv.handle) # not going to call the usual close hooks
|
|
if uv.status != StatusUninit
|
|
close(uv)
|
|
else
|
|
Libc.free(uv.handle)
|
|
end
|
|
uv.status = StatusClosed
|
|
uv.handle = C_NULL
|
|
end
|
|
nothing
|
|
end
|
|
|
|
if is_windows()
|
|
ispty(s::TTY) = s.ispty
|
|
ispty(s::IO) = false
|
|
end
|
|
|
|
" displaysize(io) -> (lines, columns)
|
|
Return the nominal size of the screen that may be used for rendering output to this io object"
|
|
displaysize(io::IO) = displaysize()
|
|
displaysize() = (parse(Int, get(ENV, "LINES", "24")),
|
|
parse(Int, get(ENV, "COLUMNS", "80")))::Tuple{Int, Int}
|
|
|
|
function displaysize(io::TTY)
|
|
local h::Int, w::Int
|
|
default_size = displaysize()
|
|
|
|
@static if is_windows()
|
|
if ispty(io)
|
|
# io is actually a libuv pipe but a cygwin/msys2 pty
|
|
try
|
|
h, w = map(x -> parse(Int, x), split(readstring(open(Base.Cmd(String["stty", "size"]), "r", io)[1])))
|
|
h > 0 || (h = default_size[1])
|
|
w > 0 || (w = default_size[2])
|
|
return h, w
|
|
catch
|
|
return default_size
|
|
end
|
|
end
|
|
end
|
|
|
|
s1 = Ref{Int32}(0)
|
|
s2 = Ref{Int32}(0)
|
|
Base.uv_error("size (TTY)", ccall(:uv_tty_get_winsize,
|
|
Int32, (Ptr{Void}, Ptr{Int32}, Ptr{Int32}),
|
|
io, s1, s2) != 0)
|
|
w, h = s1[], s2[]
|
|
h > 0 || (h = default_size[1])
|
|
w > 0 || (w = default_size[2])
|
|
return h, w
|
|
end
|
|
|
|
|
|
### Libuv callbacks ###
|
|
|
|
#from `connect`
|
|
function uv_connectcb(conn::Ptr{Void}, status::Cint)
|
|
hand = ccall(:jl_uv_connect_handle, Ptr{Void}, (Ptr{Void},), conn)
|
|
sock = @handle_as hand LibuvStream
|
|
@assert sock.status == StatusConnecting
|
|
if status >= 0
|
|
sock.status = StatusOpen
|
|
notify(sock.connectnotify)
|
|
else
|
|
sock.status = StatusInit
|
|
err = UVError("connect", status)
|
|
notify_error(sock.connectnotify, err)
|
|
end
|
|
Libc.free(conn)
|
|
nothing
|
|
end
|
|
|
|
# from `listen`
|
|
function uv_connectioncb(stream::Ptr{Void}, status::Cint)
|
|
sock = @handle_as stream LibuvServer
|
|
if status >= 0
|
|
notify(sock.connectnotify)
|
|
else
|
|
err = UVError("connection", status)
|
|
notify_error(sock.connectnotify, err)
|
|
end
|
|
nothing
|
|
end
|
|
|
|
## BUFFER ##
|
|
## Allocate space in buffer (for immediate use)
|
|
function alloc_request(buffer::IOBuffer, recommended_size::UInt)
|
|
ensureroom(buffer, Int(recommended_size))
|
|
ptr = buffer.append ? buffer.size + 1 : buffer.ptr
|
|
nb = length(buffer.data) - ptr + 1
|
|
return (pointer(buffer.data, ptr), nb)
|
|
end
|
|
|
|
notify_filled(buffer::IOBuffer, nread::Int, base::Ptr{Void}, len::UInt) = notify_filled(buffer, nread)
|
|
|
|
function notify_filled(buffer::IOBuffer, nread::Int)
|
|
if buffer.append
|
|
buffer.size += nread
|
|
else
|
|
buffer.ptr += nread
|
|
end
|
|
end
|
|
|
|
alloc_buf_hook(stream::LibuvStream, size::UInt) = alloc_request(stream.buffer, UInt(size))
|
|
|
|
function uv_alloc_buf(handle::Ptr{Void}, size::Csize_t, buf::Ptr{Void})
|
|
hd = uv_handle_data(handle)
|
|
if hd == C_NULL
|
|
ccall(:jl_uv_buf_set_len, Void, (Ptr{Void}, Csize_t), buf, 0)
|
|
return nothing
|
|
end
|
|
stream = unsafe_pointer_to_objref(hd)::LibuvStream
|
|
|
|
local data::Ptr{Void}, newsize::Csize_t
|
|
if stream.status != StatusActive
|
|
data = C_NULL
|
|
newsize = 0
|
|
else
|
|
(data, newsize) = alloc_buf_hook(stream, UInt(size))
|
|
if data == C_NULL
|
|
newsize = 0
|
|
end
|
|
end
|
|
|
|
ccall(:jl_uv_buf_set_base, Void, (Ptr{Void}, Ptr{Void}), buf, data)
|
|
ccall(:jl_uv_buf_set_len, Void, (Ptr{Void}, Csize_t), buf, newsize)
|
|
nothing
|
|
end
|
|
|
|
function uv_readcb(handle::Ptr{Void}, nread::Cssize_t, buf::Ptr{Void})
|
|
stream_unknown_type = @handle_as handle LibuvStream
|
|
nrequested = ccall(:jl_uv_buf_len, Csize_t, (Ptr{Void},), buf)
|
|
function readcb_specialized(stream::LibuvStream, nread::Int, nrequested::UInt)
|
|
if nread < 0
|
|
if nread == UV_ENOBUFS && nrequested == 0
|
|
# remind the client that stream.buffer is full
|
|
notify(stream.readnotify)
|
|
elseif nread == UV_EOF
|
|
if isa(stream, TTY)
|
|
stream.status = StatusEOF # libuv called uv_stop_reading already
|
|
notify(stream.readnotify)
|
|
notify(stream.closenotify)
|
|
elseif stream.status != StatusClosing
|
|
# begin shutdown of the stream
|
|
ccall(:jl_close_uv, Void, (Ptr{Void},), stream.handle)
|
|
stream.status = StatusClosing
|
|
end
|
|
else
|
|
# This is a fatal connection error. Shutdown requests as per the usual
|
|
# close function won't work and libuv will fail with an assertion failure
|
|
ccall(:jl_forceclose_uv, Void, (Ptr{Void},), stream)
|
|
notify_error(stream.readnotify, UVError("read", nread))
|
|
end
|
|
else
|
|
notify_filled(stream.buffer, nread)
|
|
notify(stream.readnotify)
|
|
end
|
|
|
|
# Stop background reading when
|
|
# 1) there's nobody paying attention to the data we are reading
|
|
# 2) we have accumulated a lot of unread data OR
|
|
# 3) we have an alternate buffer that has reached its limit.
|
|
if stream.status == StatusPaused ||
|
|
(stream.status == StatusActive &&
|
|
((nb_available(stream.buffer) >= stream.throttle) ||
|
|
(nb_available(stream.buffer) >= stream.buffer.maxsize)))
|
|
# save cycles by stopping kernel notifications from arriving
|
|
ccall(:uv_read_stop, Cint, (Ptr{Void},), stream)
|
|
stream.status = StatusOpen
|
|
end
|
|
nothing
|
|
end
|
|
readcb_specialized(stream_unknown_type, Int(nread), UInt(nrequested))
|
|
end
|
|
|
|
function reseteof(x::TTY)
|
|
if x.status == StatusEOF
|
|
x.status = StatusOpen
|
|
end
|
|
nothing
|
|
end
|
|
|
|
function _uv_hook_close(uv::Union{LibuvStream, LibuvServer})
|
|
uv.handle = C_NULL
|
|
uv.status = StatusClosed
|
|
# notify any listeners that exist on this libuv stream type
|
|
notify(uv.closenotify)
|
|
isdefined(uv, :readnotify) && notify(uv.readnotify)
|
|
isdefined(uv, :connectnotify) && notify(uv.connectnotify)
|
|
nothing
|
|
end
|
|
|
|
|
|
##########################################
|
|
# Pipe Abstraction
|
|
# (composed of two half-pipes: .in and .out)
|
|
##########################################
|
|
|
|
mutable struct Pipe <: AbstractPipe
|
|
in::PipeEndpoint # writable
|
|
out::PipeEndpoint # readable
|
|
end
|
|
Pipe() = Pipe(PipeEndpoint(), PipeEndpoint())
|
|
pipe_reader(p::Pipe) = p.out
|
|
pipe_writer(p::Pipe) = p.in
|
|
|
|
function link_pipe(pipe::Pipe;
|
|
julia_only_read = false,
|
|
julia_only_write = false)
|
|
link_pipe(pipe.out, julia_only_read, pipe.in, julia_only_write)
|
|
end
|
|
|
|
show(io::IO, stream::Pipe) = print(io,
|
|
"Pipe(",
|
|
_fd(stream.in), " ",
|
|
uv_status_string(stream.in), " => ",
|
|
_fd(stream.out), " ",
|
|
uv_status_string(stream.out), ", ",
|
|
nb_available(stream), " bytes waiting)")
|
|
|
|
|
|
## Functions for PipeEndpoint and PipeServer ##
|
|
|
|
function init_pipe!(pipe::LibuvPipe;
|
|
readable::Bool = false,
|
|
writable::Bool = false,
|
|
julia_only::Bool = true)
|
|
if pipe.status != StatusUninit
|
|
error("pipe is already initialized")
|
|
end
|
|
err = ccall(:jl_init_pipe, Cint,
|
|
(Ptr{Void}, Int32, Int32, Int32),
|
|
pipe.handle, writable, readable, julia_only)
|
|
uv_error(
|
|
if readable && writable
|
|
"init_pipe(ipc)"
|
|
elseif readable
|
|
"init_pipe(read)"
|
|
elseif writable
|
|
"init_pipe(write)"
|
|
else
|
|
"init_pipe(none)"
|
|
end, err)
|
|
pipe.status = StatusInit
|
|
return pipe
|
|
end
|
|
|
|
function _link_pipe(read_end::Ptr{Void}, write_end::Ptr{Void})
|
|
uv_error("pipe_link",
|
|
ccall(:uv_pipe_link, Int32, (Ptr{Void}, Ptr{Void}), read_end, write_end))
|
|
nothing
|
|
end
|
|
|
|
function link_pipe(read_end::Ptr{Void}, readable_julia_only::Bool,
|
|
write_end::Ptr{Void}, writable_julia_only::Bool,
|
|
readpipe::PipeEndpoint, writepipe::PipeEndpoint)
|
|
#make the pipe an unbuffered stream for now
|
|
#TODO: this is probably not freeing memory properly after errors
|
|
uv_error("init_pipe(read)",
|
|
ccall(:jl_init_pipe, Cint, (Ptr{Void},Int32,Int32,Int32), read_end, 0, 1, readable_julia_only))
|
|
uv_error("init_pipe(write)",
|
|
ccall(:jl_init_pipe, Cint, (Ptr{Void},Int32,Int32,Int32), write_end, 1, 0, writable_julia_only))
|
|
_link_pipe(read_end, write_end)
|
|
nothing
|
|
end
|
|
|
|
function link_pipe(read_end::Ptr{Void}, readable_julia_only::Bool,
|
|
write_end::Ptr{Void}, writable_julia_only::Bool)
|
|
uv_error("init_pipe(read)",
|
|
ccall(:jl_init_pipe, Cint, (Ptr{Void},Int32,Int32,Int32), read_end, 0, 1, readable_julia_only))
|
|
uv_error("init_pipe(write)",
|
|
ccall(:jl_init_pipe, Cint, (Ptr{Void},Int32,Int32,Int32), write_end, 1, 0, writable_julia_only))
|
|
_link_pipe(read_end,write_end)
|
|
nothing
|
|
end
|
|
|
|
function link_pipe(read_end::PipeEndpoint, readable_julia_only::Bool,
|
|
write_end::Ptr{Void}, writable_julia_only::Bool)
|
|
init_pipe!(read_end;
|
|
readable = true, writable = false, julia_only = readable_julia_only)
|
|
uv_error("init_pipe",
|
|
ccall(:jl_init_pipe, Cint, (Ptr{Void},Int32,Int32,Int32), write_end, 1, 0, writable_julia_only))
|
|
_link_pipe(read_end.handle, write_end)
|
|
read_end.status = StatusOpen
|
|
nothing
|
|
end
|
|
|
|
function link_pipe(read_end::Ptr{Void}, readable_julia_only::Bool,
|
|
write_end::PipeEndpoint, writable_julia_only::Bool)
|
|
uv_error("init_pipe",
|
|
ccall(:jl_init_pipe, Cint, (Ptr{Void},Int32,Int32,Int32), read_end, 0, 1, readable_julia_only))
|
|
init_pipe!(write_end;
|
|
readable = false, writable = true, julia_only = writable_julia_only)
|
|
_link_pipe(read_end, write_end.handle)
|
|
write_end.status = StatusOpen
|
|
nothing
|
|
end
|
|
|
|
function link_pipe(read_end::PipeEndpoint, readable_julia_only::Bool,
|
|
write_end::PipeEndpoint, writable_julia_only::Bool)
|
|
init_pipe!(read_end;
|
|
readable = true, writable = false, julia_only = readable_julia_only)
|
|
init_pipe!(write_end;
|
|
readable = false, writable = true, julia_only = writable_julia_only)
|
|
_link_pipe(read_end.handle, write_end.handle)
|
|
write_end.status = StatusOpen
|
|
read_end.status = StatusOpen
|
|
nothing
|
|
end
|
|
|
|
function close_pipe_sync(p::PipeEndpoint)
|
|
ccall(:uv_pipe_close_sync, Void, (Ptr{Void},), p.handle)
|
|
p.status = StatusClosed
|
|
nothing
|
|
end
|
|
|
|
function close_pipe_sync(handle::Ptr{Void})
|
|
return ccall(:uv_pipe_close_sync, Void, (Ptr{Void},), handle)
|
|
end
|
|
|
|
## Functions for any LibuvStream ##
|
|
|
|
# flow control
|
|
|
|
function start_reading(stream::LibuvStream)
|
|
if stream.status == StatusOpen
|
|
if !isreadable(stream)
|
|
error("tried to read a stream that is not readable")
|
|
end
|
|
# libuv may call the alloc callback immediately
|
|
# for a TTY on Windows, so ensure the status is set first
|
|
stream.status = StatusActive
|
|
ret = ccall(:uv_read_start, Cint, (Ptr{Void}, Ptr{Void}, Ptr{Void}),
|
|
stream, uv_jl_alloc_buf::Ptr{Void}, uv_jl_readcb::Ptr{Void})
|
|
return ret
|
|
elseif stream.status == StatusPaused
|
|
stream.status = StatusActive
|
|
return Int32(0)
|
|
elseif stream.status == StatusActive
|
|
return Int32(0)
|
|
else
|
|
return Int32(-1)
|
|
end
|
|
end
|
|
|
|
if is_windows()
|
|
# the low performance version of stop_reading is required
|
|
# on Windows due to a NT kernel bug that we can't use a blocking
|
|
# stream for non-blocking (overlapped) calls,
|
|
# and a ReadFile call blocking on one thread
|
|
# causes all other operations on that stream to lockup
|
|
function stop_reading(stream::LibuvStream)
|
|
if stream.status == StatusActive
|
|
stream.status = StatusOpen
|
|
ccall(:uv_read_stop, Cint, (Ptr{Void},), stream)
|
|
end
|
|
nothing
|
|
end
|
|
else
|
|
function stop_reading(stream::LibuvStream)
|
|
if stream.status == StatusActive
|
|
stream.status = StatusPaused
|
|
end
|
|
nothing
|
|
end
|
|
end
|
|
|
|
# bulk read / write
|
|
|
|
readbytes!(s::LibuvStream, a::Vector{UInt8}, nb = length(a)) = readbytes!(s, a, Int(nb))
|
|
function readbytes!(s::LibuvStream, a::Vector{UInt8}, nb::Int)
|
|
sbuf = s.buffer
|
|
@assert sbuf.seekable == false
|
|
@assert sbuf.maxsize >= nb
|
|
|
|
if nb_available(sbuf) >= nb
|
|
return readbytes!(sbuf, a, nb)
|
|
end
|
|
|
|
if nb <= SZ_UNBUFFERED_IO # Under this limit we are OK with copying the array from the stream's buffer
|
|
wait_readnb(s, nb)
|
|
return readbytes!(sbuf, a, nb)
|
|
else
|
|
try
|
|
stop_reading(s) # Just playing it safe, since we are going to switch buffers.
|
|
newbuf = PipeBuffer(a, #=maxsize=# nb)
|
|
newbuf.size = 0 # reset the write pointer to the beginning
|
|
s.buffer = newbuf
|
|
write(newbuf, sbuf)
|
|
wait_readnb(s, Int(nb))
|
|
compact(newbuf)
|
|
return nb_available(newbuf)
|
|
finally
|
|
s.buffer = sbuf
|
|
if !isempty(s.readnotify.waitq)
|
|
start_reading(s) # resume reading iff there are currently other read clients of the stream
|
|
end
|
|
end
|
|
end
|
|
@assert false # unreachable
|
|
end
|
|
|
|
function read(stream::LibuvStream)
|
|
wait_readnb(stream, typemax(Int))
|
|
return take!(stream.buffer)
|
|
end
|
|
|
|
function unsafe_read(s::LibuvStream, p::Ptr{UInt8}, nb::UInt)
|
|
sbuf = s.buffer
|
|
@assert sbuf.seekable == false
|
|
@assert sbuf.maxsize >= nb
|
|
|
|
if nb_available(sbuf) >= nb
|
|
return unsafe_read(sbuf, p, nb)
|
|
end
|
|
|
|
if nb <= SZ_UNBUFFERED_IO # Under this limit we are OK with copying the array from the stream's buffer
|
|
wait_readnb(s, Int(nb))
|
|
unsafe_read(sbuf, p, nb)
|
|
else
|
|
try
|
|
stop_reading(s) # Just playing it safe, since we are going to switch buffers.
|
|
newbuf = PipeBuffer(unsafe_wrap(Array, p, nb), #=maxsize=# Int(nb))
|
|
newbuf.size = 0 # reset the write pointer to the beginning
|
|
s.buffer = newbuf
|
|
write(newbuf, sbuf)
|
|
wait_readnb(s, Int(nb))
|
|
nb == nb_available(newbuf) || throw(EOFError())
|
|
finally
|
|
s.buffer = sbuf
|
|
if !isempty(s.readnotify.waitq)
|
|
start_reading(s) # resume reading iff there are currently other read clients of the stream
|
|
end
|
|
end
|
|
end
|
|
nothing
|
|
end
|
|
|
|
function read(this::LibuvStream, ::Type{UInt8})
|
|
wait_readnb(this, 1)
|
|
buf = this.buffer
|
|
@assert buf.seekable == false
|
|
return read(buf, UInt8)
|
|
end
|
|
|
|
function readavailable(this::LibuvStream)
|
|
wait_readnb(this, 1)
|
|
buf = this.buffer
|
|
@assert buf.seekable == false
|
|
return take!(buf)
|
|
end
|
|
|
|
function readuntil(this::LibuvStream, c::UInt8)
|
|
wait_readbyte(this, c)
|
|
buf = this.buffer
|
|
@assert buf.seekable == false
|
|
return readuntil(buf, c)
|
|
end
|
|
|
|
uv_write(s::LibuvStream, p::Vector{UInt8}) = uv_write(s, pointer(p), UInt(sizeof(p)))
|
|
function uv_write(s::LibuvStream, p::Ptr{UInt8}, n::UInt)
|
|
check_open(s)
|
|
uvw = Libc.malloc(_sizeof_uv_write)
|
|
uv_req_set_data(uvw, C_NULL) # in case we get interrupted before arriving at the wait call
|
|
err = ccall(:jl_uv_write,
|
|
Int32,
|
|
(Ptr{Void}, Ptr{Void}, UInt, Ptr{Void}, Ptr{Void}),
|
|
s, p, n, uvw,
|
|
uv_jl_writecb_task::Ptr{Void})
|
|
if err < 0
|
|
Libc.free(uvw)
|
|
uv_error("write", err)
|
|
end
|
|
ct = current_task()
|
|
preserve_handle(ct)
|
|
try
|
|
uv_req_set_data(uvw, ct)
|
|
wait()
|
|
finally
|
|
if uv_req_data(uvw) != C_NULL
|
|
# uvw is still alive,
|
|
# so make sure we don't get spurious notifications later
|
|
uv_req_set_data(uvw, C_NULL)
|
|
else
|
|
# done with uvw
|
|
Libc.free(uvw)
|
|
end
|
|
unpreserve_handle(ct)
|
|
end
|
|
return Int(n)
|
|
end
|
|
|
|
# Optimized send
|
|
# - smaller writes are buffered, final uv write on flush or when buffer full
|
|
# - large isbits arrays are unbuffered and written directly
|
|
|
|
function unsafe_write(s::LibuvStream, p::Ptr{UInt8}, n::UInt)
|
|
if isnull(s.sendbuf)
|
|
return uv_write(s, p, UInt(n))
|
|
end
|
|
|
|
buf = get(s.sendbuf)
|
|
totb = nb_available(buf) + n
|
|
if totb < buf.maxsize
|
|
nb = unsafe_write(buf, p, n)
|
|
else
|
|
flush(s)
|
|
if n > buf.maxsize
|
|
nb = uv_write(s, p, n)
|
|
else
|
|
nb = unsafe_write(buf, p, n)
|
|
end
|
|
end
|
|
return nb
|
|
end
|
|
|
|
function flush(s::LibuvStream)
|
|
if isnull(s.sendbuf)
|
|
return
|
|
end
|
|
buf = get(s.sendbuf)
|
|
if nb_available(buf) > 0
|
|
arr = take!(buf) # Array of UInt8s
|
|
uv_write(s, arr)
|
|
end
|
|
return
|
|
end
|
|
|
|
buffer_writes(s::LibuvStream, bufsize) = (s.sendbuf=PipeBuffer(bufsize); s)
|
|
|
|
## low-level calls to libuv ##
|
|
|
|
function write(s::LibuvStream, b::UInt8)
|
|
if !isnull(s.sendbuf)
|
|
buf = get(s.sendbuf)
|
|
if nb_available(buf) + 1 < buf.maxsize
|
|
return write(buf, b)
|
|
end
|
|
end
|
|
return write(s, Ref{UInt8}(b))
|
|
end
|
|
|
|
function uv_writecb_task(req::Ptr{Void}, status::Cint)
|
|
d = uv_req_data(req)
|
|
if d != C_NULL
|
|
uv_req_set_data(req, C_NULL)
|
|
if status < 0
|
|
err = UVError("write", status)
|
|
schedule(unsafe_pointer_to_objref(d)::Task, err, error=true)
|
|
else
|
|
schedule(unsafe_pointer_to_objref(d)::Task)
|
|
end
|
|
else
|
|
# no owner for this req, safe to just free it
|
|
Libc.free(req)
|
|
end
|
|
nothing
|
|
end
|
|
|
|
## server functions ##
|
|
|
|
function accept_nonblock(server::PipeServer,client::PipeEndpoint)
|
|
if client.status != StatusInit
|
|
error(client.status == StatusUninit ?
|
|
"client is not initialized" :
|
|
"client is already in use or has been closed")
|
|
end
|
|
err = ccall(:uv_accept, Int32, (Ptr{Void}, Ptr{Void}), server.handle, client.handle)
|
|
if err == 0
|
|
client.status = StatusOpen
|
|
end
|
|
return err
|
|
end
|
|
|
|
function accept_nonblock(server::PipeServer)
|
|
client = init_pipe!(PipeEndpoint(); readable=true, writable=true, julia_only=true)
|
|
uv_error("accept", accept_nonblock(server, client) != 0)
|
|
return client
|
|
end
|
|
|
|
function accept(server::LibuvServer, client::LibuvStream)
|
|
if server.status != StatusActive
|
|
throw(ArgumentError("server not connected, make sure \"listen\" has been called"))
|
|
end
|
|
while isopen(server)
|
|
err = accept_nonblock(server, client)
|
|
if err == 0
|
|
return client
|
|
elseif err != UV_EAGAIN
|
|
uv_error("accept", err)
|
|
end
|
|
stream_wait(server, server.connectnotify)
|
|
end
|
|
uv_error("accept", UV_ECONNABORTED)
|
|
end
|
|
|
|
const BACKLOG_DEFAULT = 511
|
|
|
|
function listen(sock::LibuvServer; backlog::Integer=BACKLOG_DEFAULT)
|
|
uv_error("listen", trylisten(sock))
|
|
return sock
|
|
end
|
|
|
|
function trylisten(sock::LibuvServer; backlog::Integer=BACKLOG_DEFAULT)
|
|
check_open(sock)
|
|
err = ccall(:uv_listen, Cint, (Ptr{Void}, Cint, Ptr{Void}),
|
|
sock, backlog, uv_jl_connectioncb::Ptr{Void})
|
|
sock.status = StatusActive
|
|
return err
|
|
end
|
|
|
|
function bind(server::PipeServer, name::AbstractString)
|
|
@assert server.status == StatusInit
|
|
err = ccall(:uv_pipe_bind, Int32, (Ptr{Void}, Cstring),
|
|
server, name)
|
|
if err != 0
|
|
if err != UV_EADDRINUSE && err != UV_EACCES
|
|
#TODO: this codepath is currently not tested
|
|
throw(UVError("bind",err))
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
server.status = StatusOpen
|
|
return true
|
|
end
|
|
|
|
"""
|
|
listen(path::AbstractString) -> PipeServer
|
|
|
|
Create and listen on a named pipe / UNIX domain socket.
|
|
"""
|
|
function listen(path::AbstractString)
|
|
sock = PipeServer()
|
|
bind(sock, path) || throw(ArgumentError("could not listen on path $path"))
|
|
return listen(sock)
|
|
end
|
|
|
|
function connect!(sock::PipeEndpoint, path::AbstractString)
|
|
@assert sock.status == StatusInit
|
|
req = Libc.malloc(_sizeof_uv_connect)
|
|
uv_req_set_data(req, C_NULL)
|
|
ccall(:uv_pipe_connect, Void, (Ptr{Void}, Ptr{Void}, Cstring, Ptr{Void}), req, sock.handle, path, uv_jl_connectcb::Ptr{Void})
|
|
sock.status = StatusConnecting
|
|
return sock
|
|
end
|
|
|
|
function connect(sock::LibuvStream, args...)
|
|
connect!(sock, args...)
|
|
wait_connected(sock)
|
|
return sock
|
|
end
|
|
|
|
# Libuv will internally reset read/writability, which is uses to
|
|
# mark that this is an invalid pipe.
|
|
|
|
"""
|
|
connect(path::AbstractString) -> PipeEndpoint
|
|
|
|
Connect to the named pipe / UNIX domain socket at `path`.
|
|
"""
|
|
connect(path::AbstractString) = connect(init_pipe!(PipeEndpoint(); readable=false, writable=false, julia_only=true),path)
|
|
|
|
const OS_HANDLE = is_windows() ? WindowsRawSocket : RawFD
|
|
const INVALID_OS_HANDLE = is_windows() ? WindowsRawSocket(Ptr{Void}(-1)) : RawFD(-1)
|
|
_fd(x::IOStream) = RawFD(fd(x))
|
|
function _fd(x::Union{LibuvStream, LibuvServer})
|
|
fd = Ref{OS_HANDLE}(INVALID_OS_HANDLE)
|
|
if x.status != StatusUninit && x.status != StatusClosed
|
|
err = ccall(:uv_fileno, Int32, (Ptr{Void}, Ptr{OS_HANDLE}), x.handle, fd)
|
|
# handle errors by returning INVALID_OS_HANDLE
|
|
end
|
|
return fd[]
|
|
end
|
|
|
|
for (x, writable, unix_fd, c_symbol) in
|
|
((:STDIN, false, 0, :jl_uv_stdin),
|
|
(:STDOUT, true, 1, :jl_uv_stdout),
|
|
(:STDERR, true, 2, :jl_uv_stderr))
|
|
f = Symbol("redirect_",lowercase(string(x)))
|
|
_f = Symbol("_",f)
|
|
@eval begin
|
|
function ($_f)(stream)
|
|
global $x
|
|
posix_fd = _fd(stream)
|
|
@static if is_windows()
|
|
ccall(:SetStdHandle, stdcall, Int32, (Int32, Ptr{Void}),
|
|
$(-10 - unix_fd), Libc._get_osfhandle(posix_fd).handle)
|
|
end
|
|
dup(posix_fd, RawFD($unix_fd))
|
|
$x = stream
|
|
nothing
|
|
end
|
|
function ($f)(handle::Union{LibuvStream,IOStream})
|
|
$(_f)(handle)
|
|
unsafe_store!(cglobal($(Expr(:quote,c_symbol)),Ptr{Void}),
|
|
handle.handle)
|
|
return handle
|
|
end
|
|
function ($f)()
|
|
read, write = (PipeEndpoint(), PipeEndpoint())
|
|
link_pipe(read, $(writable), write, $(!writable))
|
|
($f)($(writable ? :write : :read))
|
|
return (read, write)
|
|
end
|
|
end
|
|
end
|
|
|
|
"""
|
|
redirect_stdout([stream]) -> (rd, wr)
|
|
|
|
Create a pipe to which all C and Julia level [`STDOUT`](@ref) output
|
|
will be redirected.
|
|
Returns a tuple `(rd, wr)` representing the pipe ends.
|
|
Data written to [`STDOUT`](@ref) may now be read from the `rd` end of
|
|
the pipe. The `wr` end is given for convenience in case the old
|
|
[`STDOUT`](@ref) object was cached by the user and needs to be replaced
|
|
elsewhere.
|
|
|
|
!!! note
|
|
`stream` must be a `TTY`, a `Pipe`, or a `TCPSocket`.
|
|
"""
|
|
redirect_stdout
|
|
|
|
"""
|
|
redirect_stderr([stream]) -> (rd, wr)
|
|
|
|
Like [`redirect_stdout`](@ref), but for [`STDERR`](@ref).
|
|
|
|
!!! note
|
|
`stream` must be a `TTY`, a `Pipe`, or a `TCPSocket`.
|
|
"""
|
|
redirect_stderr
|
|
|
|
"""
|
|
redirect_stdin([stream]) -> (rd, wr)
|
|
|
|
Like [`redirect_stdout`](@ref), but for [`STDIN`](@ref).
|
|
Note that the order of the return tuple is still `(rd, wr)`,
|
|
i.e. data to be read from [`STDIN`](@ref) may be written to `wr`.
|
|
|
|
!!! note
|
|
`stream` must be a `TTY`, a `Pipe`, or a `TCPSocket`.
|
|
"""
|
|
redirect_stdin
|
|
|
|
for (F,S) in ((:redirect_stdin, :STDIN), (:redirect_stdout, :STDOUT), (:redirect_stderr, :STDERR))
|
|
@eval function $F(f::Function, stream)
|
|
STDOLD = $S
|
|
local ret
|
|
$F(stream)
|
|
try
|
|
ret = f()
|
|
finally
|
|
$F(STDOLD)
|
|
end
|
|
ret
|
|
end
|
|
end
|
|
|
|
"""
|
|
redirect_stdout(f::Function, stream)
|
|
|
|
Run the function `f` while redirecting [`STDOUT`](@ref) to `stream`.
|
|
Upon completion, [`STDOUT`](@ref) is restored to its prior setting.
|
|
|
|
!!! note
|
|
`stream` must be a `TTY`, a `Pipe`, or a `TCPSocket`.
|
|
"""
|
|
redirect_stdout(f::Function, stream)
|
|
|
|
"""
|
|
redirect_stderr(f::Function, stream)
|
|
|
|
Run the function `f` while redirecting [`STDERR`](@ref) to `stream`.
|
|
Upon completion, [`STDERR`](@ref) is restored to its prior setting.
|
|
|
|
!!! note
|
|
`stream` must be a `TTY`, a `Pipe`, or a `TCPSocket`.
|
|
"""
|
|
redirect_stderr(f::Function, stream)
|
|
|
|
"""
|
|
redirect_stdin(f::Function, stream)
|
|
|
|
Run the function `f` while redirecting [`STDIN`](@ref) to `stream`.
|
|
Upon completion, [`STDIN`](@ref) is restored to its prior setting.
|
|
|
|
!!! note
|
|
`stream` must be a `TTY`, a `Pipe`, or a `TCPSocket`.
|
|
"""
|
|
redirect_stdin(f::Function, stream)
|
|
|
|
mark(x::LibuvStream) = mark(x.buffer)
|
|
unmark(x::LibuvStream) = unmark(x.buffer)
|
|
reset(x::LibuvStream) = reset(x.buffer)
|
|
ismarked(x::LibuvStream) = ismarked(x.buffer)
|
|
|
|
# BufferStream's are non-OS streams, backed by a regular IOBuffer
|
|
mutable struct BufferStream <: LibuvStream
|
|
buffer::IOBuffer
|
|
r_c::Condition
|
|
close_c::Condition
|
|
is_open::Bool
|
|
buffer_writes::Bool
|
|
lock::ReentrantLock
|
|
|
|
BufferStream() = new(PipeBuffer(), Condition(), Condition(), true, false, ReentrantLock())
|
|
end
|
|
|
|
isopen(s::BufferStream) = s.is_open
|
|
function close(s::BufferStream)
|
|
s.is_open = false
|
|
notify(s.r_c)
|
|
notify(s.close_c)
|
|
nothing
|
|
end
|
|
uvfinalize(s::BufferStream) = nothing
|
|
|
|
read(s::BufferStream, ::Type{UInt8}) = (wait_readnb(s, 1); read(s.buffer, UInt8))
|
|
unsafe_read(s::BufferStream, a::Ptr{UInt8}, nb::UInt) = (wait_readnb(s, Int(nb)); unsafe_read(s.buffer, a, nb))
|
|
nb_available(s::BufferStream) = nb_available(s.buffer)
|
|
|
|
isreadable(s::BufferStream) = s.buffer.readable
|
|
iswritable(s::BufferStream) = s.buffer.writable
|
|
|
|
function wait_readnb(s::BufferStream, nb::Int)
|
|
while isopen(s) && nb_available(s.buffer) < nb
|
|
wait(s.r_c)
|
|
end
|
|
end
|
|
|
|
show(io::IO, s::BufferStream) = print(io,"BufferStream() bytes waiting:",nb_available(s.buffer),", isopen:", s.is_open)
|
|
|
|
function wait_readbyte(s::BufferStream, c::UInt8)
|
|
while isopen(s) && search(s.buffer,c) <= 0
|
|
wait(s.r_c)
|
|
end
|
|
end
|
|
|
|
wait_close(s::BufferStream) = if isopen(s); wait(s.close_c); end
|
|
start_reading(s::BufferStream) = Int32(0)
|
|
stop_reading(s::BufferStream) = nothing
|
|
|
|
write(s::BufferStream, b::UInt8) = write(s, Ref{UInt8}(b))
|
|
function unsafe_write(s::BufferStream, p::Ptr{UInt8}, nb::UInt)
|
|
rv = unsafe_write(s.buffer, p, nb)
|
|
!(s.buffer_writes) && notify(s.r_c)
|
|
return rv
|
|
end
|
|
|
|
function eof(s::BufferStream)
|
|
wait_readnb(s, 1)
|
|
return !isopen(s) && nb_available(s)<=0
|
|
end
|
|
|
|
# If buffer_writes is called, it will delay notifying waiters till a flush is called.
|
|
buffer_writes(s::BufferStream, bufsize=0) = (s.buffer_writes=true; s)
|
|
flush(s::BufferStream) = (notify(s.r_c); nothing)
|