summaryrefslogtreecommitdiff
path: root/util/ec3po/interpreter.py
blob: 8d21af247a9a1b6ce0102900b5471c6648097110 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# Copyright 2015 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""EC-3PO EC Interpreter

interpreter provides the interpretation layer between the EC UART and the user.
It receives commands through its command pipe, formats the commands for the EC,
and sends the command to the EC.  It also presents data from the EC to either be
displayed via the interactive console interface, or some other consumer.  It
additionally supports automatic command retrying if the EC drops a character in
a command.
"""

# Note: This is a py2/3 compatible file.

from __future__ import print_function

import binascii
import copy
import logging
import os
import select
import traceback

import six

COMMAND_RETRIES = 3  # Number of attempts to retry a command.
EC_MAX_READ = 1024  # Max bytes to read at a time from the EC.
EC_SYN = b"\xec"  # Byte indicating EC interrogation.
EC_ACK = b"\xc0"  # Byte representing correct EC response to interrogation.


class LoggerAdapter(logging.LoggerAdapter):
    """Class which provides a small adapter for the logger."""

    def process(self, msg, kwargs):
        """Prepends the served PTY to the beginning of the log message."""
        return "%s - %s" % (self.extra["pty"], msg), kwargs


class Interpreter(object):
    """Class which provides the interpretation layer between the EC and user.

    This class essentially performs all of the intepretation for the EC and the
    user.  It handles all of the automatic command retrying as well as the
    formation of commands for EC images which support that.

    Attributes:
      logger: A logger for this module.
      ec_uart_pty: An opened file object to the raw EC UART PTY.
      ec_uart_pty_name: A string containing the name of the raw EC UART PTY.
      cmd_pipe: A socket.socket or multiprocessing.Connection object which
        represents the Interpreter side of the command pipe.  This must be a
        bidirectional pipe.  Commands and responses will utilize this pipe.
      dbg_pipe: A socket.socket or multiprocessing.Connection object which
        represents the Interpreter side of the debug pipe. This must be a
        unidirectional pipe with write capabilities.  EC debug output will utilize
        this pipe.
      cmd_retries: An integer representing the number of attempts the console
        should retry commands if it receives an error.
      log_level: An integer representing the numeric value of the log level.
      inputs: A list of objects that the intpreter selects for reading.
        Initially, these are the EC UART and the command pipe.
      outputs: A list of objects that the interpreter selects for writing.
      ec_cmd_queue: A FIFO queue used for sending commands down to the EC UART.
      last_cmd: A string that represents the last command sent to the EC.  If an
        error is encountered, the interpreter will attempt to retry this command
        up to COMMAND_RETRIES.
      enhanced_ec: A boolean indicating if the EC image that we are currently
        communicating with is enhanced or not.  Enhanced EC images will support
        packed commands and host commands over the UART.  This defaults to False
        and is changed depending on the result of an interrogation.
      interrogating: A boolean indicating if we are in the middle of interrogating
        the EC.
      connected: A boolean indicating if the interpreter is actually connected to
        the UART and listening.
    """

    def __init__(
        self, ec_uart_pty, cmd_pipe, dbg_pipe, log_level=logging.INFO, name=None
    ):
        """Intializes an Interpreter object with the provided args.

        Args:
          ec_uart_pty: A string representing the EC UART to connect to.
          cmd_pipe: A socket.socket or multiprocessing.Connection object which
            represents the Interpreter side of the command pipe.  This must be a
            bidirectional pipe.  Commands and responses will utilize this pipe.
          dbg_pipe: A socket.socket or multiprocessing.Connection object which
            represents the Interpreter side of the debug pipe. This must be a
            unidirectional pipe with write capabilities.  EC debug output will
            utilize this pipe.
          cmd_retries: An integer representing the number of attempts the console
            should retry commands if it receives an error.
          log_level: An optional integer representing the numeric value of the log
            level.  By default, the log level will be logging.INFO (20).
          name: the console source name
        """
        # Create a unique logger based on the interpreter name
        interpreter_prefix = ("%s - " % name) if name else ""
        logger = logging.getLogger("%sEC3PO.Interpreter" % interpreter_prefix)
        self.logger = LoggerAdapter(logger, {"pty": ec_uart_pty})
        # TODO(https://crbug.com/1162189): revist the 2 TODOs below
        # TODO(https://bugs.python.org/issue27805, python3.7+): revert to ab+
        # TODO(https://bugs.python.org/issue20074): removing buffering=0 if/when
        # that gets fixed, or keep two pty: one for reading and one for writing
        self.ec_uart_pty = open(ec_uart_pty, "r+b", buffering=0)
        self.ec_uart_pty_name = ec_uart_pty
        self.cmd_pipe = cmd_pipe
        self.dbg_pipe = dbg_pipe
        self.cmd_retries = COMMAND_RETRIES
        self.log_level = log_level
        self.inputs = [self.ec_uart_pty, self.cmd_pipe]
        self.outputs = []
        self.ec_cmd_queue = six.moves.queue.Queue()
        self.last_cmd = b""
        self.enhanced_ec = False
        self.interrogating = False
        self.connected = True

    def __str__(self):
        """Show internal state of the Interpreter object.

        Returns:
          A string that shows the values of the attributes.
        """
        string = []
        string.append("%r" % self)
        string.append("ec_uart_pty: %s" % self.ec_uart_pty)
        string.append("cmd_pipe: %r" % self.cmd_pipe)
        string.append("dbg_pipe: %r" % self.dbg_pipe)
        string.append("cmd_retries: %d" % self.cmd_retries)
        string.append("log_level: %d" % self.log_level)
        string.append("inputs: %r" % self.inputs)
        string.append("outputs: %r" % self.outputs)
        string.append("ec_cmd_queue: %r" % self.ec_cmd_queue)
        string.append("last_cmd: '%s'" % self.last_cmd)
        string.append("enhanced_ec: %r" % self.enhanced_ec)
        string.append("interrogating: %r" % self.interrogating)
        return "\n".join(string)

    def EnqueueCmd(self, command):
        """Enqueue a command to be sent to the EC UART.

        Args:
          command: A string which contains the command to be sent.
        """
        self.ec_cmd_queue.put(command)
        self.logger.log(
            1, "Commands now in queue: %d", self.ec_cmd_queue.qsize()
        )

        # Add the EC UART as an output to be serviced.
        if self.connected and self.ec_uart_pty not in self.outputs:
            self.outputs.append(self.ec_uart_pty)

    def PackCommand(self, raw_cmd):
        r"""Packs a command for use with error checking.

        For error checking, we pack console commands in a particular format.  The
        format is as follows:

          &&[x][x][x][x]&{cmd}\n\n
          ^ ^    ^^    ^^  ^  ^-- 2 newlines.
          | |    ||    ||  |-- the raw console command.
          | |    ||    ||-- 1 ampersand.
          | |    ||____|--- 2 hex digits representing the CRC8 of cmd.
          | |____|-- 2 hex digits reprsenting the length of cmd.
          |-- 2 ampersands

        Args:
          raw_cmd: A pre-packed string which contains the raw command.

        Returns:
          A string which contains the packed command.
        """
        # Don't pack a single carriage return.
        if raw_cmd != b"\r":
            # The command format is as follows.
            # &&[x][x][x][x]&{cmd}\n\n
            packed_cmd = []
            packed_cmd.append(b"&&")
            # The first pair of hex digits are the length of the command.
            packed_cmd.append(b"%02x" % len(raw_cmd))
            # Then the CRC8 of cmd.
            packed_cmd.append(b"%02x" % Crc8(raw_cmd))
            packed_cmd.append(b"&")
            # Now, the raw command followed by 2 newlines.
            packed_cmd.append(raw_cmd)
            packed_cmd.append(b"\n\n")
            return b"".join(packed_cmd)
        else:
            return raw_cmd

    def ProcessCommand(self, command):
        """Captures the input determines what actions to take.

        Args:
          command: A string representing the command sent by the user.
        """
        if command == b"disconnect":
            if self.connected:
                self.logger.debug("UART disconnect request.")
                # Drop all pending commands if any.
                while not self.ec_cmd_queue.empty():
                    c = self.ec_cmd_queue.get()
                    self.logger.debug("dropped: '%s'", c)
                if self.enhanced_ec:
                    # Reset retry state.
                    self.cmd_retries = COMMAND_RETRIES
                    self.last_cmd = b""
                # Get the UART that the interpreter is attached to.
                fileobj = self.ec_uart_pty
                self.logger.debug("fileobj: %r", fileobj)
                # Remove the descriptor from the inputs and outputs.
                self.inputs.remove(fileobj)
                if fileobj in self.outputs:
                    self.outputs.remove(fileobj)
                self.logger.debug(
                    "Removed fileobj. Remaining inputs: %r", self.inputs
                )
                # Close the file.
                fileobj.close()
                # Mark the interpreter as disconnected now.
                self.connected = False
                self.logger.debug(
                    "Disconnected from %s.", self.ec_uart_pty_name
                )
            return

        elif command == b"reconnect":
            if not self.connected:
                self.logger.debug("UART reconnect request.")
                # Reopen the PTY.
                # TODO(https://bugs.python.org/issue27805, python3.7+): revert to ab+
                # TODO(https://bugs.python.org/issue20074): removing buffering=0 if/when
                # that gets fixed, or keep two pty: one for reading and one for writing
                fileobj = open(self.ec_uart_pty_name, "r+b", buffering=0)
                self.logger.debug("fileobj: %r", fileobj)
                self.ec_uart_pty = fileobj
                # Add the descriptor to the inputs.
                self.inputs.append(fileobj)
                self.logger.debug("fileobj added. curr inputs: %r", self.inputs)
                # Mark the interpreter as connected now.
                self.connected = True
                self.logger.debug("Connected to %s.", self.ec_uart_pty_name)
            return

        elif command.startswith(b"enhanced"):
            self.enhanced_ec = command.split(b" ")[1] == b"True"
            return

        # Ignore any other commands while in the disconnected state.
        self.logger.log(1, "command: '%s'", command)
        if not self.connected:
            self.logger.debug(
                "Ignoring command because currently disconnected."
            )
            return

        # Remove leading and trailing spaces only if this is an enhanced EC image.
        # For non-enhanced EC images, commands will be single characters at a time
        # and can be spaces.
        if self.enhanced_ec:
            command = command.strip(b" ")

        # There's nothing to do if the command is empty.
        if len(command) == 0:
            return

        # Handle log level change requests.
        if command.startswith(b"loglevel"):
            self.logger.debug("Log level change request.")
            new_log_level = int(command.split(b" ")[1])
            self.logger.logger.setLevel(new_log_level)
            self.logger.info("Log level changed to %d.", new_log_level)
            return

        # Check for interrogation command.
        if command == EC_SYN:
            # User is requesting interrogation.  Send SYN as is.
            self.logger.debug("User requesting interrogation.")
            self.interrogating = True
            # Assume the EC isn't enhanced until we get a response.
            self.enhanced_ec = False
        elif self.enhanced_ec:
            # Enhanced EC images require the plaintext commands to be packed.
            command = self.PackCommand(command)
            # TODO(aaboagye): Make a dict of commands and keys and eventually,
            # handle partial matching based on unique prefixes.

        self.EnqueueCmd(command)

    def HandleCmdRetries(self):
        """Attempts to retry commands if possible."""
        if self.cmd_retries > 0:
            # The EC encountered an error.  We'll have to retry again.
            self.logger.warning("Retrying command...")
            self.cmd_retries -= 1
            self.logger.warning("Retries remaining: %d", self.cmd_retries)
            # Retry the command and add the EC UART to the writers again.
            self.EnqueueCmd(self.last_cmd)
            self.outputs.append(self.ec_uart_pty)
        else:
            # We're out of retries, so just give up.
            self.logger.error("Command failed.  No retries left.")
            # Clear the command in progress.
            self.last_cmd = b""
            # Reset the retry count.
            self.cmd_retries = COMMAND_RETRIES

    def SendCmdToEC(self):
        """Sends a command to the EC."""
        # If we're retrying a command, just try to send it again.
        if self.cmd_retries < COMMAND_RETRIES:
            cmd = self.last_cmd
        else:
            # If we're not retrying, we should not be writing to the EC if we have no
            # items in our command queue.
            assert not self.ec_cmd_queue.empty()
            # Get the command to send.
            cmd = self.ec_cmd_queue.get()

        # Send the command.
        self.ec_uart_pty.write(cmd)
        self.ec_uart_pty.flush()
        self.logger.log(1, "Sent command to EC.")

        if self.enhanced_ec and cmd != EC_SYN:
            # Now, that we've sent the command, store the current command as the last
            # command sent.  If we encounter an error string, we will attempt to retry
            # this command.
            if cmd != self.last_cmd:
                self.last_cmd = cmd
                # Reset the retry count.
                self.cmd_retries = COMMAND_RETRIES

        # If no command is pending to be sent, then we can remove the EC UART from
        # writers.  Might need better checking for command retry logic in here.
        if self.ec_cmd_queue.empty():
            # Remove the EC UART from the writers while we wait for a response.
            self.logger.debug("Removing EC UART from writers.")
            self.outputs.remove(self.ec_uart_pty)

    def HandleECData(self):
        """Handle any debug prints from the EC."""
        self.logger.log(1, "EC has data")
        # Read what the EC sent us.
        data = os.read(self.ec_uart_pty.fileno(), EC_MAX_READ)
        self.logger.log(1, "got: '%s'", binascii.hexlify(data))
        if b"&E" in data and self.enhanced_ec:
            # We received an error, so we should retry it if possible.
            self.logger.warning("Error string found in data.")
            self.HandleCmdRetries()
            return

        # If we were interrogating, check the response and update our knowledge
        # of the current EC image.
        if self.interrogating:
            self.enhanced_ec = data == EC_ACK
            if self.enhanced_ec:
                self.logger.debug("The current EC image seems enhanced.")
            else:
                self.logger.debug(
                    "The current EC image does NOT seem enhanced."
                )
            # Done interrogating.
            self.interrogating = False
        # For now, just forward everything the EC sends us.
        self.logger.log(1, "Forwarding to user...")
        self.dbg_pipe.send(data)

    def HandleUserData(self):
        """Handle any incoming commands from the user.

        Raises:
          EOFError: Allowed to propagate through from self.cmd_pipe.recv().
        """
        self.logger.log(1, "Command data available.  Begin processing.")
        data = self.cmd_pipe.recv()
        # Process the command.
        self.ProcessCommand(data)


def Crc8(data):
    """Calculates the CRC8 of data.

    The generator polynomial used is: x^8 + x^2 + x + 1.
    This is the same implementation that is used in the EC.

    Args:
      data: A string of data that we wish to calculate the CRC8 on.

    Returns:
      crc >> 8: An integer representing the CRC8 value.
    """
    crc = 0
    for byte in six.iterbytes(data):
        crc ^= byte << 8
        for _ in range(8):
            if crc & 0x8000:
                crc ^= 0x1070 << 3
            crc <<= 1
    return crc >> 8


def StartLoop(interp, shutdown_pipe=None):
    """Starts an infinite loop of servicing the user and the EC.

    StartLoop checks to see if there are any commands to process, processing them
    if any, and forwards EC output to the user.

    When sending a command to the EC, we send the command once and check the
    response to see if the EC encountered an error when receiving the command.  An
    error condition is reported to the interpreter by a string with at least one
    '&' and 'E'.  The full string is actually '&&EE', however it's possible that
    the leading ampersand or trailing 'E' could be dropped.  If an error is
    encountered, the interpreter will retry up to the amount configured.

    Args:
      interp: An Interpreter object that has been properly initialised.
      shutdown_pipe: A file object for a pipe or equivalent that becomes readable
        (not blocked) to indicate that the loop should exit.  Can be None to never
        exit the loop.
    """
    try:
        # This is used instead of "break" to avoid exiting the loop in the middle of
        # an iteration.
        continue_looping = True

        while continue_looping:
            # The inputs list is created anew in each loop iteration because the
            # Interpreter class sometimes modifies the interp.inputs list.
            if shutdown_pipe is None:
                inputs = interp.inputs
            else:
                inputs = list(interp.inputs)
                inputs.append(shutdown_pipe)

            readable, writeable, _ = select.select(inputs, interp.outputs, [])

            for obj in readable:
                # Handle any debug prints from the EC.
                if obj is interp.ec_uart_pty:
                    interp.HandleECData()

                # Handle any commands from the user.
                elif obj is interp.cmd_pipe:
                    try:
                        interp.HandleUserData()
                    except EOFError:
                        interp.logger.debug(
                            "ec3po interpreter received EOF from cmd_pipe in "
                            "HandleUserData()"
                        )
                        continue_looping = False

                elif obj is shutdown_pipe:
                    interp.logger.debug(
                        "ec3po interpreter received shutdown pipe unblocked notification"
                    )
                    continue_looping = False

            for obj in writeable:
                # Send a command to the EC.
                if obj is interp.ec_uart_pty:
                    interp.SendCmdToEC()

    except KeyboardInterrupt:
        pass

    finally:
        interp.cmd_pipe.close()
        interp.dbg_pipe.close()
        interp.ec_uart_pty.close()
        if shutdown_pipe is not None:
            shutdown_pipe.close()
        interp.logger.debug(
            "Exit ec3po interpreter loop for %s", interp.ec_uart_pty_name
        )