-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Finish implementing REPL server/client #56
Comments
The macro-enabled REPL console belongs in
|
As of today, EDIT: ...aaand done. |
A possible future direction: Detachable sessions Like screen. But there's no "inject asynchronous exception and resume in background", so we can't do much with SIGTSTP (Ctrl+Z) even if we caught it... Right now, injecting new background computations into a live Python process is not the primary goal of this feature. But if needed, this is already possible in the REPL: import threading
import queue
q = queue.Queue() # for results
def worker():
...
q.push(...)
t = threading.Thread(target=worker, daemon=True)
t.start() and then just disconnect the session normally. The results should appear in import threading
from ..misc import namelambda
out = {}
def bg(thunk):
@namelambda(thunk.__name__)
def worker():
try:
result = thunk()
except Exception as err:
out[t.ident] = err
else:
out[t.ident] = result
t = threading.Thread(target=worker, daemon=True)
t.start()
return t and then just expose that to the client side. Usage example: from unpythonic import primes, islice
t = bg(lambda: islice(primes())[:100]) # --> thread object
# later, to read the result
print(out.get(t.ident, None))
# clean up if you care about that sort of thing
t.join() # should return immediately if the result is already available
del t This is like We have |
With a208e78, everything necessary for a first release of the REPL feature should be complete. |
12 February 2020: An updated version of this text is now in
doc/repl.md
.Hot-patch a running Python process! With macros in the REPL! Inspired by Swank in Common Lisp.
As of now, a complete implementation is in place, waiting for a few important final touches.
To try it right now, with the latest code from git:
python3 -m unpythonic.net.server
.imacropy
installed, you will getimacropy.console.MacroConsole
. (Recommended.)macropy.core.console.MacroConsole
.code.InteractiveConsole
, and macro support will not be enabled.from unpythonic.net import server; server.start(locals=globals())
. This is all the preparation your app code needs to do to provide a REPL server that has access to the running process.server.stop()
manually; this is automatically registered as anatexit
handler.start
specifies the top-level namespace of REPL sessions served by the server. If this is one of your modules' global namespace, you can directly write to that namespace in the REPL simply by assigning to variables. E.g.x = 42
will actually domymod.x = 42
.server.start(locals={})
.import sys; sys.modules['myothermod'].x
.python3 -m unpythonic.net.client 127.0.0.1:1337
. This opens a REPL session, where:readline
) is available, with history and remote tab completion (when you use tab completion, the client queries for completions from the server).KeyboardInterrupt
to the remote.KeyboardInterrupt
asynchronous exception into the thread running that particular session. Any other threads in the process running the server are unaffected.ctypes.pythonapi
makes that possible.unpythonic.misc.async_raise
and Push PyPy3 compatibility to 100% #58 for details.Ctrl+C
just once. Hammering the key combo may raise aKeyboardInterrupt
locally in the code that is trying to send the remoteKeyboardInterrupt
(or in code waiting for the server's response), thus forcibly terminating the client. Starting immediately after the server has responded, remote Ctrl+C is available again. (The server indicates this by sending the textKeyboardInterrupt
, possibly with a stack trace, and then giving a new prompt, just like a standard interactive Python session does.)print()
is available as usual, but output is properly redirected to the client only in the REPL session's main thread.sys.stdout
in the REPL session's main thread. After the REPL server has been started, it's actually aShim
that holds the underlying stream in aThreadLocalBox
, so you can get the stream from there if you really need to. For any thread that hasn't sent a value into that box, the box will return the default, which is the original stdin/stdout/stderr of the server process.help(obj)
does not work, hangs the client. Known issue. Use the customdoc(obj)
instead. It just prints the docstring without paging, while emulatinghelp
's dedenting. It's not a perfect solution, but should work well enough to view docstrings of live objects in a live Python process.select.select
is not called on an fd that is not a socket.DANGER:
A REPL server is essentially an opt-in back door. While the intended use is for allowing hot-patching in your app, by its very nature, the server gives access not only to your app, but also to anything that can be imported, including
os
andsys
. It is trivial to use it as a shell that just happens to use Python as the command language, or to obtain traditional shell access (e.g.bash
) via it.This particular REPL server has no authentication support whatsoever. Any user logged in to the local machine can connect. There is no encryption for network traffic, either. Therefore, to remain secure:
In both cases, access control and encrypted connections (SSH) are then provided by the OS itself. Note this is exactly the same level of security (i.e. none whatsoever) as provided by the Python REPL itself. If you have access to
python
, you have access to the system (with the privileges thepython
process itself runs under).Why a custom REPL server/client
Macro support, right there in the console of a REPL-in-a-live-Python-process. This is why this feature is included in
unpythonic
, instead of just recommending Manhole, socketserverREPL, or similar existing solutions.Furthermore, the focus is different from most similar projects; this server is primarily intended for hot-patching, not so much for debugging. So we don't care about debugger hooks, or instantly embedding a REPL into a particular local scope (to give the full Python user experience for examining program state), pausing the thread that spawned the REPL. We care about running the REPL server in the background (listening for connections as part of normal operation of your app), and making write access to module globals easy.
A hot-patching REPL server is also useful in oldschool style scientific scripts that run directly via
python3 mysolver.py
orpython3 -m mysolver
(no Jupyter notebook there), because it reduces the burden of planning ahead. Seeing the first plots from a new study often raises new questions. Experience has shown it would often be useful to re-plot the same data (that took two hours to compute) in alternative ways... while the script doesn't yet have the code to save anything to disk, because the current run was supposed to be just for testing. You know that when you close that last figure window, the process will terminate, and all that delicious data will be gone. But provided the data can be accessed from module scope, an embedded REPL server can still save the day. You just open a REPL session to your live process, and save what it turns out you needed, before closing that last figure and letting the process terminate. It's all about having a different kind of conversation with your scientific problem. (Cf. Paul Graham on software development in On Lisp; original quotation.)Future directions
Authentication and encryption
SSH with key-based authentication is the primary future direction of interest. It would enable security, making actual remote access feasible.
This may be added in an eventual v2.0 (using Paramiko), but right now it's not on the immediate roadmap. This would allow a client to be sure the server is who it claims to be, as well as letting users log in based on an
authorized_keys
file. It would also make it possible to audit who has connected and when.There are a lot of Paramiko client examples on the internet (oddly, with a focus mainly on security testing), but demo_server.py in the distribution seems to be the only server example, and leaves unclear important issues such as how to set up a session and a shell. Reading paramiko/server.py as well as paramiko/transport.py didn't make me much wiser.
(What we want is to essentially treat our Python REPL as the shell for the SSH session.) So for this first version, right now I'm not going to bother with SSH support.
What we needed to get macro support
Drop-in replacing
code.InteractiveConsole
inunpythonic.net.server
withmacropy.core.console.MacroConsole
gave rudimentary macro support.However, to have the same semantics as in the
imacropy
IPython extension, a custom console was needed. This was added toimacropy
asimacropy.console.MacroConsole
.For historical interest, refer to and compare imacropy/iconsole.py and macropy/core/console.py. The result is the new imacropy/console.py.
DONE:
Robustify socket data handling. Do it properly, no optimistic single reads and writes, since TCP doesn't do datagrams.Now we have a simplistic message protocol (seeunpythonic.net.msg
) that runs over TCP, so we can use that for the control channel. But to remain simple andnetcat
compatible, the primary channel cannot be message-based. So we still need a prompt detector.Improve presentation of line editing (needs prompt detection at the client side; refer to repl_tool.py by Ivor Wanders).Done in 2658ede.Add remote Ctrl+C support. Requires a control channel, like in IPython. (There is already a rudimentary control channel for tab completion requests; just generalize this.)First cut of remote Ctrl+C support added in 9b68f95.The comments suggest the server is going to inject itself to the calling module's globals namespace, but perhaps it's more pythonic to let the user specify the namespace to run in. You can easily passglobals()
as the namespace in the call tounpythonic.net.server.start
if that's what you want.Done,start()
now takes a mandatorylocals
argument; now just need to update the comments.PyPy3 doesn't support remote Ctrl+C due to lack ofDone.PyThreadState_SetAsyncExc
incpyext
(PyPy's partial emulation of CPython'sctypes.pythonapi
). Disable this feature when running on PyPy, for now, to get this thing out of the door.Figure out and fix bug with remote Ctrl+C: output appears one prompt late after Ctrl+C'ing a computation.Hacked around in c3f67c2. A real fix seems more difficult, and I want to release 0.14.2 sooner rather than later.for _ in range(int(1e9)): pass
.KeyboardInterrupt
and a new prompt.doc(doc)
. Observe no output.Not sure if we should wrap the REPL session in a PTY or not?Keeping the PTY wrapper for now.code.InteractiveConsole
running within it (e.g. ANSI color codes should work - test this!), but some things are not available, because the client runs a second input prompt (that holds the local TTY), separate from the one running on the server.print("\033[32m")
in a remote REPL turns the foreground color dark green, andprint("\033[39m")
resets it to the default. Compulsory links for the curious: [1], [2].Improve/update comments/docstrings.0ed68f5.help()
can't work in the current implementation.unpythonic.net
code.localhost 1337 8128
(address and two ports; omitting ports uses default values).doc
tohelp
(in the REPL sessions)? Then again, least surprise; maybe not. Yes, let's not do that. Better exportdoc
as-is.unpythonic.net
is almost a separate project, so this should have its owndoc/repl.md
. The material for the documentation is practically already here and in the docstrings.unpythonic.net
. We don't want the leaves to fall while an operation is pending...The text was updated successfully, but these errors were encountered: