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)