magic-wormhole/src/wormhole/_input.py

341 lines
10 KiB
Python
Raw Normal View History

2018-04-21 07:30:08 +00:00
from __future__ import absolute_import, print_function, unicode_literals
# We use 'threading' defensively here, to detect if we're being called from a
# non-main thread. _rlcompleter.py is the only internal Wormhole code that
# deliberately creates a new thread.
import threading
2018-04-21 07:30:08 +00:00
from attr import attrib, attrs
from attr.validators import provides
from automat import MethodicalMachine
2018-04-21 07:30:08 +00:00
from twisted.internet import defer
from zope.interface import implementer
from . import _interfaces, errors
from ._nameplate import validate_nameplate
2018-04-21 07:30:08 +00:00
def first(outputs):
return list(outputs)[0]
2018-04-21 07:30:08 +00:00
@attrs
@implementer(_interfaces.IInput)
class Input(object):
_timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine()
2018-04-21 07:30:08 +00:00
set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def __attrs_post_init__(self):
self._all_nameplates = set()
self._nameplate = None
self._wordlist = None
self._wordlist_waiters = []
self._trace = None
def set_debug(self, f):
self._trace = f
2018-04-21 07:30:08 +00:00
def _debug(self, what): # pragma: no cover
if self._trace:
self._trace(old_state="", input=what, new_state="")
def wire(self, code, lister):
self._C = _interfaces.ICode(code)
self._L = _interfaces.ILister(lister)
def when_wordlist_is_available(self):
if self._wordlist:
return defer.succeed(None)
d = defer.Deferred()
self._wordlist_waiters.append(d)
return d
@m.state(initial=True)
2018-04-21 07:30:08 +00:00
def S0_idle(self):
pass # pragma: no cover
@m.state()
2018-04-21 07:30:08 +00:00
def S1_typing_nameplate(self):
pass # pragma: no cover
@m.state()
2018-04-21 07:30:08 +00:00
def S2_typing_code_no_wordlist(self):
pass # pragma: no cover
@m.state()
2018-04-21 07:30:08 +00:00
def S3_typing_code_yes_wordlist(self):
pass # pragma: no cover
@m.state(terminal=True)
2018-04-21 07:30:08 +00:00
def S4_done(self):
pass # pragma: no cover
# from Code
@m.input()
2018-04-21 07:30:08 +00:00
def start(self):
pass
# from Lister
@m.input()
2018-04-21 07:30:08 +00:00
def got_nameplates(self, all_nameplates):
pass
2017-03-17 23:50:37 +00:00
# from Nameplate
@m.input()
2018-04-21 07:30:08 +00:00
def got_wordlist(self, wordlist):
pass
2017-03-15 07:43:25 +00:00
# API provided to app as ICodeInputHelper
@m.input()
2018-04-21 07:30:08 +00:00
def refresh_nameplates(self):
pass
@m.input()
2018-04-21 07:30:08 +00:00
def get_nameplate_completions(self, prefix):
pass
def choose_nameplate(self, nameplate):
2018-04-21 07:30:08 +00:00
validate_nameplate(nameplate) # can raise KeyFormatError
self._choose_nameplate(nameplate)
2018-04-21 07:30:08 +00:00
@m.input()
2018-04-21 07:30:08 +00:00
def _choose_nameplate(self, nameplate):
pass
@m.input()
2018-04-21 07:30:08 +00:00
def get_word_completions(self, prefix):
pass
@m.input()
2018-04-21 07:30:08 +00:00
def choose_words(self, words):
pass
@m.output()
def do_start(self):
self._start_timing = self._timing.add("input code", waiting="user")
self._L.refresh()
return Helper(self)
2018-04-21 07:30:08 +00:00
@m.output()
2017-03-17 23:50:37 +00:00
def do_refresh(self):
self._L.refresh()
2018-04-21 07:30:08 +00:00
@m.output()
def record_nameplates(self, all_nameplates):
# we get a set of nameplate id strings
self._all_nameplates = all_nameplates
2018-04-21 07:30:08 +00:00
@m.output()
def _get_nameplate_completions(self, prefix):
completions = set()
for nameplate in self._all_nameplates:
if nameplate.startswith(prefix):
# TODO: it's a little weird that Input is responsible for the
# hyphen on nameplates, but WordList owns it for words
2018-04-21 07:30:08 +00:00
completions.add(nameplate + "-")
return completions
2018-04-21 07:30:08 +00:00
@m.output()
def record_all_nameplates(self, nameplate):
self._nameplate = nameplate
self._C.got_nameplate(nameplate)
2018-04-21 07:30:08 +00:00
2017-03-17 23:50:37 +00:00
@m.output()
def record_wordlist(self, wordlist):
2017-03-22 23:10:02 +00:00
from ._rlcompleter import debug
debug(" -record_wordlist")
2017-03-17 23:50:37 +00:00
self._wordlist = wordlist
2018-04-21 07:30:08 +00:00
@m.output()
def notify_wordlist_waiters(self, wordlist):
while self._wordlist_waiters:
d = self._wordlist_waiters.pop()
d.callback(None)
@m.output()
def no_word_completions(self, prefix):
return set()
2018-04-21 07:30:08 +00:00
@m.output()
def _get_word_completions(self, prefix):
assert self._wordlist
return self._wordlist.get_completions(prefix)
@m.output()
def raise_must_choose_nameplate1(self, prefix):
raise errors.MustChooseNameplateFirstError()
2018-04-21 07:30:08 +00:00
@m.output()
def raise_must_choose_nameplate2(self, words):
raise errors.MustChooseNameplateFirstError()
2018-04-21 07:30:08 +00:00
@m.output()
def raise_already_chose_nameplate1(self):
raise errors.AlreadyChoseNameplateError()
2018-04-21 07:30:08 +00:00
@m.output()
def raise_already_chose_nameplate2(self, prefix):
raise errors.AlreadyChoseNameplateError()
2018-04-21 07:30:08 +00:00
@m.output()
def raise_already_chose_nameplate3(self, nameplate):
raise errors.AlreadyChoseNameplateError()
2018-04-21 07:30:08 +00:00
@m.output()
def raise_already_chose_words1(self, prefix):
raise errors.AlreadyChoseWordsError()
2018-04-21 07:30:08 +00:00
@m.output()
def raise_already_chose_words2(self, words):
raise errors.AlreadyChoseWordsError()
@m.output()
2017-03-17 23:50:37 +00:00
def do_words(self, words):
code = self._nameplate + "-" + words
self._start_timing.finish()
self._C.finished_input(code)
2018-04-21 07:30:08 +00:00
S0_idle.upon(
start, enter=S1_typing_nameplate, outputs=[do_start], collector=first)
2017-03-19 22:23:25 +00:00
# wormholes that don't use input_code (i.e. they use allocate_code or
# generate_code) will never start() us, but Nameplate will give us a
# wordlist anyways (as soon as the nameplate is claimed), so handle it.
2018-04-21 07:30:08 +00:00
S0_idle.upon(
got_wordlist,
enter=S0_idle,
outputs=[record_wordlist, notify_wordlist_waiters])
S1_typing_nameplate.upon(
got_nameplates, enter=S1_typing_nameplate, outputs=[record_nameplates])
2017-03-19 22:23:25 +00:00
# but wormholes that *do* use input_code should not get got_wordlist
# until after we tell Code that we got_nameplate, which is the earliest
# it can be claimed
2018-04-21 07:30:08 +00:00
S1_typing_nameplate.upon(
refresh_nameplates, enter=S1_typing_nameplate, outputs=[do_refresh])
S1_typing_nameplate.upon(
get_nameplate_completions,
enter=S1_typing_nameplate,
outputs=[_get_nameplate_completions],
collector=first)
S1_typing_nameplate.upon(
_choose_nameplate,
enter=S2_typing_code_no_wordlist,
outputs=[record_all_nameplates])
S1_typing_nameplate.upon(
get_word_completions,
enter=S1_typing_nameplate,
outputs=[raise_must_choose_nameplate1])
S1_typing_nameplate.upon(
choose_words,
enter=S1_typing_nameplate,
outputs=[raise_must_choose_nameplate2])
S2_typing_code_no_wordlist.upon(
got_nameplates, enter=S2_typing_code_no_wordlist, outputs=[])
S2_typing_code_no_wordlist.upon(
got_wordlist,
enter=S3_typing_code_yes_wordlist,
outputs=[record_wordlist, notify_wordlist_waiters])
S2_typing_code_no_wordlist.upon(
refresh_nameplates,
enter=S2_typing_code_no_wordlist,
outputs=[raise_already_chose_nameplate1])
S2_typing_code_no_wordlist.upon(
get_nameplate_completions,
enter=S2_typing_code_no_wordlist,
outputs=[raise_already_chose_nameplate2])
S2_typing_code_no_wordlist.upon(
_choose_nameplate,
enter=S2_typing_code_no_wordlist,
outputs=[raise_already_chose_nameplate3])
S2_typing_code_no_wordlist.upon(
get_word_completions,
enter=S2_typing_code_no_wordlist,
outputs=[no_word_completions],
collector=first)
S2_typing_code_no_wordlist.upon(
choose_words, enter=S4_done, outputs=[do_words])
S3_typing_code_yes_wordlist.upon(
got_nameplates, enter=S3_typing_code_yes_wordlist, outputs=[])
# got_wordlist: should never happen
2018-04-21 07:30:08 +00:00
S3_typing_code_yes_wordlist.upon(
refresh_nameplates,
enter=S3_typing_code_yes_wordlist,
outputs=[raise_already_chose_nameplate1])
S3_typing_code_yes_wordlist.upon(
get_nameplate_completions,
enter=S3_typing_code_yes_wordlist,
outputs=[raise_already_chose_nameplate2])
S3_typing_code_yes_wordlist.upon(
_choose_nameplate,
enter=S3_typing_code_yes_wordlist,
outputs=[raise_already_chose_nameplate3])
S3_typing_code_yes_wordlist.upon(
get_word_completions,
enter=S3_typing_code_yes_wordlist,
outputs=[_get_word_completions],
collector=first)
S3_typing_code_yes_wordlist.upon(
choose_words, enter=S4_done, outputs=[do_words])
2017-03-17 23:50:37 +00:00
S4_done.upon(got_nameplates, enter=S4_done, outputs=[])
S4_done.upon(got_wordlist, enter=S4_done, outputs=[])
2018-04-21 07:30:08 +00:00
S4_done.upon(
refresh_nameplates,
enter=S4_done,
outputs=[raise_already_chose_nameplate1])
S4_done.upon(
get_nameplate_completions,
enter=S4_done,
outputs=[raise_already_chose_nameplate2])
S4_done.upon(
_choose_nameplate,
enter=S4_done,
outputs=[raise_already_chose_nameplate3])
S4_done.upon(
get_word_completions,
enter=S4_done,
outputs=[raise_already_chose_words1])
S4_done.upon(
choose_words, enter=S4_done, outputs=[raise_already_chose_words2])
# we only expose the Helper to application code, not _Input
@attrs
2017-06-26 11:21:00 +00:00
@implementer(_interfaces.IInputHelper)
class Helper(object):
_input = attrib()
def __attrs_post_init__(self):
self._main_thread = threading.current_thread().ident
def refresh_nameplates(self):
assert threading.current_thread().ident == self._main_thread
self._input.refresh_nameplates()
2018-04-21 07:30:08 +00:00
def get_nameplate_completions(self, prefix):
assert threading.current_thread().ident == self._main_thread
return self._input.get_nameplate_completions(prefix)
2018-04-21 07:30:08 +00:00
def choose_nameplate(self, nameplate):
assert threading.current_thread().ident == self._main_thread
self._input._debug("I.choose_nameplate")
self._input.choose_nameplate(nameplate)
self._input._debug("I.choose_nameplate finished")
2018-04-21 07:30:08 +00:00
def when_wordlist_is_available(self):
assert threading.current_thread().ident == self._main_thread
return self._input.when_wordlist_is_available()
2018-04-21 07:30:08 +00:00
def get_word_completions(self, prefix):
assert threading.current_thread().ident == self._main_thread
return self._input.get_word_completions(prefix)
2018-04-21 07:30:08 +00:00
def choose_words(self, words):
assert threading.current_thread().ident == self._main_thread
self._input._debug("I.choose_words")
self._input.choose_words(words)
self._input._debug("I.choose_words finished")