202 lines
9.0 KiB
Python
202 lines
9.0 KiB
Python
from __future__ import print_function, unicode_literals
|
|
import os, traceback
|
|
from sys import stderr
|
|
try:
|
|
import readline
|
|
except ImportError:
|
|
readline = None
|
|
from six.moves import input
|
|
from attr import attrs, attrib
|
|
from twisted.internet.defer import inlineCallbacks, returnValue
|
|
from twisted.internet.threads import deferToThread, blockingCallFromThread
|
|
from .errors import KeyFormatError, AlreadyInputNameplateError
|
|
|
|
errf = None
|
|
if 0:
|
|
errf = open("err", "w") if os.path.exists("err") else None
|
|
def debug(*args, **kwargs):
|
|
if errf:
|
|
print(*args, file=errf, **kwargs)
|
|
errf.flush()
|
|
|
|
@attrs
|
|
class CodeInputter(object):
|
|
_input_helper = attrib()
|
|
_reactor = attrib()
|
|
def __attrs_post_init__(self):
|
|
self.used_completion = False
|
|
self._matches = None
|
|
# once we've claimed the nameplate, we can't go back
|
|
self._committed_nameplate = None # or string
|
|
|
|
def bcft(self, f, *a, **kw):
|
|
return blockingCallFromThread(self._reactor, f, *a, **kw)
|
|
|
|
def completer(self, text, state):
|
|
try:
|
|
return self._wrapped_completer(text, state)
|
|
except Exception as e:
|
|
# completer exceptions are normally silently discarded, which
|
|
# makes debugging challenging
|
|
print("completer exception: %s" % e)
|
|
traceback.print_exc()
|
|
raise e
|
|
|
|
def _wrapped_completer(self, text, state):
|
|
self.used_completion = True
|
|
# if we get here, then readline must be active
|
|
ct = readline.get_completion_type()
|
|
if state == 0:
|
|
debug("completer starting (%s) (state=0) (ct=%d)" % (text, ct))
|
|
self._matches = self._commit_and_build_completions(text)
|
|
debug(" matches:", " ".join(["'%s'" % m for m in self._matches]))
|
|
else:
|
|
debug(" s%d t'%s' ct=%d" % (state, text, ct))
|
|
|
|
if state >= len(self._matches):
|
|
debug(" returning None")
|
|
return None
|
|
debug(" returning '%s'" % self._matches[state])
|
|
return self._matches[state]
|
|
|
|
def _commit_and_build_completions(self, text):
|
|
ih = self._input_helper
|
|
if "-" in text:
|
|
got_nameplate = True
|
|
nameplate, words = text.split("-", 1)
|
|
else:
|
|
got_nameplate = False
|
|
nameplate = text # partial
|
|
|
|
# 'text' is one of these categories:
|
|
# "" or "12": complete on nameplates (all that match, maybe just one)
|
|
|
|
# "123-": if we haven't already committed to a nameplate, commit and
|
|
# wait for the wordlist. Then (either way) return the whole wordlist.
|
|
|
|
# "123-supp": if we haven't already committed to a nameplate, commit
|
|
# and wait for the wordlist. Then (either way) return all current
|
|
# matches.
|
|
|
|
if self._committed_nameplate:
|
|
if not got_nameplate or nameplate != self._committed_nameplate:
|
|
# they deleted past the committment point: we can't use
|
|
# this. For now, bail, but in the future let's find a
|
|
# gentler way to encourage them to not do that.
|
|
raise AlreadyInputNameplateError("nameplate (%s-) already entered, cannot go back" % self._committed_nameplate)
|
|
if not got_nameplate:
|
|
# we're completing on nameplates: "" or "12" or "123"
|
|
self.bcft(ih.refresh_nameplates) # results arrive later
|
|
debug(" getting nameplates")
|
|
completions = self.bcft(ih.get_nameplate_completions, nameplate)
|
|
else: # "123-" or "123-supp"
|
|
# time to commit to this nameplate, if they haven't already
|
|
if not self._committed_nameplate:
|
|
debug(" choose_nameplate(%s)" % nameplate)
|
|
self.bcft(ih.choose_nameplate, nameplate)
|
|
self._committed_nameplate = nameplate
|
|
|
|
# Now we want to wait for the wordlist to be available. If
|
|
# the user just typed "12-supp TAB", we'll claim "12" but
|
|
# will need a server roundtrip to discover that "supportive"
|
|
# is the only match. If we don't block, we'd return an empty
|
|
# wordlist to readline (which will beep and show no
|
|
# completions). *Then* when the user hits TAB again a moment
|
|
# later (after the wordlist has arrived, but the user hasn't
|
|
# modified the input line since the previous empty response),
|
|
# readline would show one match but not complete anything.
|
|
|
|
# In general we want to avoid returning empty lists to
|
|
# readline. If the user hits TAB when typing in the nameplate
|
|
# (before the sender has established one, or before we're
|
|
# heard about it from the server), it can't be helped. But
|
|
# for the rest of the code, a simple wait-for-wordlist will
|
|
# improve the user experience.
|
|
self.bcft(ih.when_wordlist_is_available) # blocks on CLAIM
|
|
# and we're completing on words now
|
|
debug(" getting words (%s)" % (words,))
|
|
completions = [nameplate+"-"+c
|
|
for c in self.bcft(ih.get_word_completions, words)]
|
|
|
|
# rlcompleter wants full strings
|
|
return sorted(completions)
|
|
|
|
def finish(self, text):
|
|
if "-" not in text:
|
|
raise KeyFormatError("incomplete wormhole code")
|
|
nameplate, words = text.split("-", 1)
|
|
|
|
if self._committed_nameplate:
|
|
if nameplate != self._committed_nameplate:
|
|
# they deleted past the committment point: we can't use
|
|
# this. For now, bail, but in the future let's find a
|
|
# gentler way to encourage them to not do that.
|
|
raise AlreadyInputNameplateError("nameplate (%s-) already entered, cannot go back" % self._committed_nameplate)
|
|
else:
|
|
debug(" choose_nameplate(%s)" % nameplate)
|
|
self._input_helper.choose_nameplate(nameplate)
|
|
debug(" choose_words(%s)" % words)
|
|
self._input_helper.choose_words(words)
|
|
|
|
def _input_code_with_completion(prompt, input_helper, reactor):
|
|
c = CodeInputter(input_helper, reactor)
|
|
if readline is not None:
|
|
if readline.__doc__ and "libedit" in readline.__doc__:
|
|
readline.parse_and_bind("bind ^I rl_complete")
|
|
else:
|
|
readline.parse_and_bind("tab: complete")
|
|
readline.set_completer(c.completer)
|
|
readline.set_completer_delims("")
|
|
debug("==== readline-based completion is prepared")
|
|
else:
|
|
debug("==== unable to import readline, disabling completion")
|
|
pass
|
|
code = input(prompt)
|
|
# Code is str(bytes) on py2, and str(unicode) on py3. We want unicode.
|
|
if isinstance(code, bytes):
|
|
code = code.decode("utf-8")
|
|
c.finish(code)
|
|
return c.used_completion
|
|
|
|
def warn_readline():
|
|
# When our process receives a SIGINT, Twisted's SIGINT handler will
|
|
# stop the reactor and wait for all threads to terminate before the
|
|
# process exits. However, if we were waiting for
|
|
# input_code_with_completion() when SIGINT happened, the readline
|
|
# thread will be blocked waiting for something on stdin. Trick the
|
|
# user into satisfying the blocking read so we can exit.
|
|
print("\nCommand interrupted: please press Return to quit", file=stderr)
|
|
|
|
# Other potential approaches to this problem:
|
|
# * hard-terminate our process with os._exit(1), but make sure the
|
|
# tty gets reset to a normal mode ("cooked"?) first, so that the
|
|
# next shell command the user types is echoed correctly
|
|
# * track down the thread (t.p.threadable.getThreadID from inside the
|
|
# thread), get a cffi binding to pthread_kill, deliver SIGINT to it
|
|
# * allocate a pty pair (pty.openpty), replace sys.stdin with the
|
|
# slave, build a pty bridge that copies bytes (and other PTY
|
|
# things) from the real stdin to the master, then close the slave
|
|
# at shutdown, so readline sees EOF
|
|
# * write tab-completion and basic editing (TTY raw mode,
|
|
# backspace-is-erase) without readline, probably with curses or
|
|
# twisted.conch.insults
|
|
# * write a separate program to get codes (maybe just "wormhole
|
|
# --internal-get-code"), run it as a subprocess, let it inherit
|
|
# stdin/stdout, send it SIGINT when we receive SIGINT ourselves. It
|
|
# needs an RPC mechanism (over some extra file descriptors) to ask
|
|
# us to fetch the current nameplate_id list.
|
|
#
|
|
# Note that hard-terminating our process with os.kill(os.getpid(),
|
|
# signal.SIGKILL), or SIGTERM, doesn't seem to work: the thread
|
|
# doesn't see the signal, and we must still wait for stdin to make
|
|
# readline finish.
|
|
|
|
@inlineCallbacks
|
|
def input_with_completion(prompt, input_helper, reactor):
|
|
t = reactor.addSystemEventTrigger("before", "shutdown", warn_readline)
|
|
#input_helper.refresh_nameplates()
|
|
used_completion = yield deferToThread(_input_code_with_completion,
|
|
prompt, input_helper, reactor)
|
|
reactor.removeSystemEventTrigger(t)
|
|
returnValue(used_completion)
|