Don't crash on a malformed log entry. Move lazy parse of struct.

Austin asked to be resilient when the log has a truncated final entry.

The lazy parsing of struct part of an entry belongs in LogEntry, not
in the reader.

Change-Id: I81d76a116609ac73834863fdd15d2d657d24deaf
diff --git a/frc971/analysis/logentry.py b/frc971/analysis/logentry.py
index 5b9a2ed..3069af0 100644
--- a/frc971/analysis/logentry.py
+++ b/frc971/analysis/logentry.py
@@ -1,7 +1,6 @@
 #!/usr/bin/python3
 
 import re
-import sys
 
 """
 A regular expression to match the envelope part of the log entry.
@@ -32,10 +31,11 @@
 
   def __init__(self, line):
     """Populates a LogEntry from a line."""
+    self.line = line
     m = LOG_RE.match(line)
     if m is None:
         print("LOG_RE failed on", line)
-        sys.exit(1)
+        return
     self.name = m.group(1)
     self.pid_index = int(m.group(2))
     self.msg_index = int(m.group(3))
@@ -44,6 +44,7 @@
     self.filename = m.group(6)
     self.linenumber = m.group(7)
     self.msg = m.group(8)
+    self.struct_name = None
 
   def __str__(self):
     """Formats the data cleanly."""
@@ -56,6 +57,10 @@
     Returns:
       struct_name, struct_type, json dict.
     """
+    if self.struct_name:
+        # We've already parsed the structural part. Return the cached result
+        return (self.struct_name, self.struct_type, self.struct_json)
+
     struct_name_index = self.msg.find(':')
     struct_name = self.msg[0:struct_name_index]
 
@@ -109,6 +114,11 @@
     # Now that we have tokens, parse them.
     self.JsonizeTokens(json, tokens, 1)
 
+    # Cache the result to avoid having to reparse.
+    self.struct_name = struct_name
+    self.struct_type = struct_type
+    self.struct_json = json
+
     return (struct_name, struct_type, json)
 
   def JsonizeTokens(self, json, tokens, token_index):
diff --git a/frc971/analysis/logreader.py b/frc971/analysis/logreader.py
index 34f1b01..c59487b 100644
--- a/frc971/analysis/logreader.py
+++ b/frc971/analysis/logreader.py
@@ -47,7 +47,11 @@
     """
     with open(f, 'r') as fd:
       for line in fd:
-        self.HandleLine(line)
+        try:
+            self.HandleLine(line)
+        except Exception as ex:
+            # It's common for the last line of the file to be malformed.
+            print("Ignoring malformed log entry: ", line)
 
   def HandleLine(self, line):
     """
@@ -60,7 +64,6 @@
       None
     """
     pline = LogEntry(line)
-    pline_data = None
 
     for key in self.signal:
       value = self.signal[key]
@@ -79,12 +82,9 @@
       # Make sure that we're looking at the right binary structure instance.
       if binary == pline.name:
         if pline.msg.startswith(struct_instance_name + ': '):
-          # Parse the structure once.
-          if pline_data is None:
-              _, _, pline_data = pline.ParseStruct()
           # Traverse the structure as specified in `data_search_path`.
           # This lets the user access very deeply nested structures.
-          data = pline_data
+          _, _, data = pline.ParseStruct()
           for path in data_search_path:
             data = data[path]