commander.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. # Copyright 2024 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from __future__ import absolute_import
  15. import re
  16. import threading
  17. import tokenize
  18. import types
  19. from pebble import pulse2
  20. from . import apps
  21. class Pulse2ConnectionAdapter(object):
  22. '''An adapter for the pulse2 API to look enough like pulse.Connection
  23. to make PebbleCommander work...ish.
  24. Prompt will break spectacularly if the firmware reboots or the link
  25. state otherwise changes. Commander itself needs to be modified to be
  26. link-state aware.
  27. '''
  28. def __init__(self, interface):
  29. self.interface = interface
  30. self.logging = apps.StreamingLogs(interface)
  31. link = interface.get_link()
  32. self.prompt = apps.Prompt(link)
  33. self.flash = apps.FlashImaging(link)
  34. def close(self):
  35. self.interface.close()
  36. class PebbleCommander(object):
  37. """ Pebble Commander.
  38. Implements everything for interfacing with PULSE things.
  39. """
  40. def __init__(self, tty=None, interactive=False, capfile=None):
  41. if capfile is not None:
  42. interface = pulse2.Interface.open_dbgserial(
  43. url=tty, capture_stream=open(capfile, 'wb'))
  44. else:
  45. interface = pulse2.Interface.open_dbgserial(url=tty)
  46. try:
  47. self.connection = Pulse2ConnectionAdapter(interface)
  48. except:
  49. interface.close()
  50. raise
  51. self.interactive = interactive
  52. self.log_listeners_lock = threading.Lock()
  53. self.log_listeners = []
  54. # Start the logging thread
  55. self.log_thread = threading.Thread(target=self._start_logging)
  56. self.log_thread.daemon = True
  57. self.log_thread.start()
  58. def __enter__(self):
  59. return self
  60. def __exit__(self, exc_type, exc_value, traceback):
  61. self.close()
  62. @classmethod
  63. def command(cls, name=None):
  64. """ Registers a command.
  65. `name` is the command name. If `name` is unspecified, name will be the function name
  66. with underscores converted to hyphens.
  67. The convention for `name` is to separate words with a hyphen. The function name
  68. will be the same as `name` with hyphens replaced with underscores.
  69. Example: `click-short` will result in a PebbleCommander.click_short function existing.
  70. `fn` should return an array of strings (or None), and take the current
  71. `PebbleCommander` as the first argument, and the rest of the argument strings
  72. as subsequent arguments. For errors, `fn` should throw an exception.
  73. # TODO: Probably make the return something structured instead of stringly typed.
  74. """
  75. def decorator(fn):
  76. # Story time:
  77. # <cory> Things are fine as long as you only read from `name`, but assigning to `name`
  78. # creates a new local which shadows the outer scope's variable, even though it's
  79. # only assigned later on in the block
  80. # <cory> You could work around this by doing something like `name_ = name` and using
  81. # `name_` in the `decorator` scope
  82. cmdname = name
  83. if not cmdname:
  84. cmdname = fn.__name__.replace('_', '-')
  85. funcname = cmdname.replace('-', '_')
  86. if not re.match(tokenize.Name + '$', funcname):
  87. raise ValueError("command name %s isn't a valid name" % funcname)
  88. if hasattr(cls, funcname):
  89. raise ValueError('function name %s clashes with existing attribute' % funcname)
  90. fn.is_command = True
  91. fn.name = cmdname
  92. method = types.MethodType(fn, cls)
  93. setattr(cls, funcname, method)
  94. return fn
  95. return decorator
  96. def close(self):
  97. self.connection.close()
  98. def _start_logging(self):
  99. """ Thread to handle logging messages.
  100. """
  101. while True:
  102. try:
  103. msg = self.connection.logging.receive()
  104. except pulse2.exceptions.SocketClosed:
  105. break
  106. with self.log_listeners_lock:
  107. # TODO: Buffer log messages if no listeners attached?
  108. for listener in self.log_listeners:
  109. try:
  110. listener(msg)
  111. except:
  112. pass
  113. def attach_log_listener(self, listener):
  114. """ Attaches a listener for log messages.
  115. Function takes message and returns are ignored.
  116. """
  117. with self.log_listeners_lock:
  118. self.log_listeners.append(listener)
  119. def detach_log_listener(self, listener):
  120. """ Removes a listener that was added with `attach_log_listener`
  121. """
  122. with self.log_listeners_lock:
  123. self.log_listeners.remove(listener)
  124. def send_prompt_command(self, cmd):
  125. """ Send a prompt command string.
  126. Unfortunately this is indeed stringly typed, a better solution is necessary.
  127. """
  128. return self.connection.prompt.command_and_response(cmd)
  129. def get_command(self, command):
  130. try:
  131. fn = getattr(self, command.replace('-', '_'))
  132. if fn.is_command:
  133. return fn
  134. except AttributeError:
  135. # Method doesn't exist, or isn't a command.
  136. pass
  137. return None