Skip to content

Commit a9d21ac

Browse files
authored
Add on_connect_hook to serve_repl for customized sessions (#33)
For now, this allows the server to set a custom module for the client to evaluate commands in. This can be useful if you want - The client to always start evaluating code in the server's module, for debugging - Some minimal level of isolation between clients (a new anonymous module could be created for each client)
1 parent ba605b8 commit a9d21ac

File tree

3 files changed

+76
-44
lines changed

3 files changed

+76
-44
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "RemoteREPL"
22
uuid = "1bd9f7bb-701c-4338-bec7-ac987af7c555"
33
authors = ["Chris Foster <[email protected]> and contributors"]
4-
version = "0.2.11"
4+
version = "0.2.12"
55

66
[deps]
77
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"

src/server.jl

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ using REPL
44
using Logging
55

66
mutable struct ServerSideSession
7+
socket
78
display_properties::Dict
89
in_module::Module
910
end
1011

12+
Base.isopen(session::ServerSideSession) = isopen(session.socket)
13+
Base.close(session::ServerSideSession) = close(session.socket)
14+
1115
function send_header(io, ser_version=Serialization.ser_version)
1216
write(io, PROTOCOL_MAGIC, PROTOCOL_VERSION)
1317
write(io, UInt32(ser_version))
@@ -93,9 +97,7 @@ function eval_message(session, messageid, messagebody)
9397
end
9498
end
9599

96-
function evaluate_requests(request_chan, response_chan)
97-
session = ServerSideSession(Dict(), Main)
98-
100+
function evaluate_requests(session, request_chan, response_chan)
99101
while true
100102
try
101103
request = take!(request_chan)
@@ -174,15 +176,16 @@ function serialize_responses(socket, response_chan)
174176
end
175177
end
176178

177-
# Serve a remote REPL session to a single client over `socket`.
178-
function serve_repl_session(socket)
179+
# Serve a remote REPL session to a single client
180+
function serve_repl_session(session)
181+
socket = session.socket
179182
send_header(socket)
180183
@sync begin
181184
request_chan = Channel(1)
182185
response_chan = Channel(1)
183186

184187
repl_backend = @async try
185-
evaluate_requests(request_chan, response_chan)
188+
evaluate_requests(session, request_chan, response_chan)
186189
catch exc
187190
@error "RemoteREPL backend crashed" exception=exc,catch_backtrace()
188191
finally
@@ -210,14 +213,18 @@ function serve_repl_session(socket)
210213
end
211214

212215
"""
213-
serve_repl([address=Sockets.localhost,] port=$DEFAULT_PORT)
216+
serve_repl([address=Sockets.localhost,] port=$DEFAULT_PORT; [on_client_connect=nothing])
214217
serve_repl(server)
215218
216219
Start a REPL server listening on interface `address` and `port`. In normal
217220
operation `serve_repl()` serves REPL clients indefinitely (ie., it does not
218221
return), so you will generally want to launch it using `@async serve_repl()` to
219222
do other useful work at the same time.
220223
224+
The hook `on_client_connect` may be supplied to modify the `ServerSideSession`
225+
for a client after each client connects. This can be used to define the default
226+
module in which the client evaluates commands.
227+
221228
If you want to be able to stop the server you can pass an already-listening
222229
`server` object (the result of `Sockets.listen()`). The server can then be
223230
cancelled from another task using `close(server)` as necessary to control the
@@ -230,34 +237,38 @@ be used on open networks or multi-user machines where other users aren't
230237
trusted. For open networks, use the default `address=Sockets.localhost` and the
231238
automatic ssh tunnel support provided by the client-side `connect_repl()`.
232239
"""
233-
function serve_repl(address=Sockets.localhost, port::Integer=DEFAULT_PORT)
240+
function serve_repl(address=Sockets.localhost, port::Integer=DEFAULT_PORT; kws...)
234241
server = listen(address, port)
235242
try
236-
serve_repl(server)
243+
serve_repl(server; kws...)
237244
finally
238245
close(server)
239246
end
240247
end
241-
serve_repl(port::Integer) = serve_repl(Sockets.localhost, port)
248+
serve_repl(port::Integer; kws...) = serve_repl(Sockets.localhost, port; kws...)
242249

243-
function serve_repl(server::Base.IOServer)
244-
open_sockets = Set()
250+
function serve_repl(server::Base.IOServer; on_client_connect=nothing)
251+
open_sessions = Set{ServerSideSession}()
245252
@sync try
246253
while isopen(server)
247254
socket = accept(server)
248-
push!(open_sockets, socket)
249-
peer=getpeername(socket)
255+
session = ServerSideSession(socket, Dict(), Main)
256+
push!(open_sessions, session)
257+
peer = getpeername(socket)
250258
@async try
251-
serve_repl_session(socket)
259+
if !isnothing(on_client_connect)
260+
on_client_connect(session)
261+
end
262+
serve_repl_session(session)
252263
catch exc
253-
if !(exc isa EOFError && !isopen(socket))
264+
if !(exc isa EOFError && !isopen(session))
254265
@warn "Something went wrong evaluating client command" #=
255266
=# exception=exc,catch_backtrace()
256267
end
257268
finally
258269
@info "REPL client exited" peer
259-
close(socket)
260-
pop!(open_sockets, socket)
270+
close(session)
271+
pop!(open_sessions, session)
261272
end
262273
@info "REPL client opened a connection" peer
263274
end
@@ -269,8 +280,8 @@ function serve_repl(server::Base.IOServer)
269280
@error "Unexpected server failure" isopen(server) exception=exc,catch_backtrace()
270281
rethrow()
271282
finally
272-
for socket in open_sockets
273-
close(socket)
283+
for session in open_sessions
284+
close(session)
274285
end
275286
end
276287
end

test/runtests.jl

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,28 @@ end
4747
@test repl_prompt_text(fake_conn("ABC", DEFAULT_PORT, is_open=false)) == "julia@ABC [disconnected]> "
4848
end
4949

50+
function wait_conn(host, port, use_ssh; max_tries=4)
51+
for i=1:max_tries
52+
try
53+
return RemoteREPL.Connection(host=host, port=port,
54+
tunnel=use_ssh ? :ssh : :none,
55+
ssh_opts=`-o StrictHostKeyChecking=no`)
56+
catch exc
57+
if i == max_tries
58+
rethrow()
59+
end
60+
# Server not yet started - continue waiting
61+
sleep(2)
62+
end
63+
end
64+
end
65+
66+
function runcommand_unwrap(conn, cmdstr)
67+
result = RemoteREPL.run_remote_repl_command(conn, IOBuffer(), cmdstr)
68+
# Unwrap Text for testing purposes
69+
return result isa Text ? result.content : result
70+
end
71+
5072
# Connect to a non-default loopback address to test SSH integration
5173
test_interface = ip"127.111.111.111"
5274

@@ -56,7 +78,7 @@ use_ssh = if "use_ssh=true" in ARGS
5678
elseif "use_ssh=false" in ARGS
5779
false
5880
else
59-
# Autodetct
81+
# Autodetect
6082
try
6183
socket = Sockets.connect(test_interface, 22)
6284
# https://tools.ietf.org/html/rfc4253#section-4.2
@@ -78,34 +100,15 @@ server_proc = run(`$(Base.julia_cmd()) -e "using Sockets; using RemoteREPL; serv
78100

79101
try
80102

81-
@testset "RemoteREPL.jl" begin
82-
local conn = nothing
83-
max_tries = 4
84-
for i=1:max_tries
85-
try
86-
conn = RemoteREPL.Connection(host=test_interface, port=test_port,
87-
tunnel=use_ssh ? :ssh : :none,
88-
ssh_opts=`-o StrictHostKeyChecking=no`)
89-
break
90-
catch exc
91-
if i == max_tries
92-
rethrown()
93-
end
94-
# Server not yet started - continue waiting
95-
sleep(2)
96-
end
97-
end
103+
@testset "Client server tests" begin
104+
conn = wait_conn(test_interface, test_port, use_ssh)
98105
@assert isopen(conn)
99106

100107
# Some basic tests of the transport and server side and partial client side.
101108
#
102109
# More full testing of the client code would requires some tricky mocking
103110
# of the REPL environment.
104-
function runcommand(cmdstr)
105-
result = RemoteREPL.run_remote_repl_command(conn, IOBuffer(), cmdstr)
106-
# Unwrap Text for testing purposes
107-
return result isa Text ? result.content : result
108-
end
111+
runcommand(cmdstr) = runcommand_unwrap(conn, cmdstr)
109112

110113
@test runcommand("asdf = 42") == "42"
111114
@test runcommand("Main.asdf") == "42"
@@ -262,3 +265,21 @@ end
262265
finally
263266
kill(server_proc)
264267
end
268+
269+
270+
test_port = RemoteREPL.find_free_port(Sockets.localhost)
271+
server_proc = run(```$(Base.julia_cmd()) -e "using Sockets; using RemoteREPL; module EvalInMod ; end;
272+
serve_repl($test_port, on_client_connect=sess->sess.in_module=EvalInMod)"```, wait=false)
273+
try
274+
275+
@testset "on_client_connect" begin
276+
conn = wait_conn(test_interface, test_port, use_ssh)
277+
278+
runcommand(cmdstr) = runcommand_unwrap(conn, cmdstr)
279+
280+
@test runcommand("@__MODULE__") == "Main.EvalInMod"
281+
end
282+
283+
finally
284+
kill(server_proc)
285+
end

0 commit comments

Comments
 (0)