def playback_log(log_path, file_like, show_esc=False): """ Plays back the log file at *log_path* by way of timely output to *file_like* which is expected to be any file-like object with write() and flush() methods. If *show_esc* is True, escape sequences and control characters will be escaped so they can be seen in the output. There will also be no delay between the output of frames (under the assumption that if you want to see the raw log you want it to output all at once so you can pipe it into some other app). """ prev_frame_time = None try: for count, frame in enumerate(get_frames(log_path)): frame_time = float(frame[:13]) # First 13 chars is the timestamp frame = frame[14:] # [14:] Skips the timestamp and the colon if count == 0: # Write it out immediately if show_esc: frame = raw(frame) file_like.write(frame) prev_frame_time = frame_time else: if show_esc: frame = raw(frame) else: # Wait until the time between the previous frame and now # has passed wait_time = (frame_time - prev_frame_time)/1000.0 sleep(wait_time) # frame times are in milliseconds file_like.write(frame) prev_frame_time = frame_time file_like.flush() except IOError: # Something wrong with the file return
def playback_log(log_path, file_like, show_esc=False): """ Plays back the log file at *log_path* by way of timely output to *file_like* which is expected to be any file-like object with write() and flush() methods. If *show_esc* is True, escape sequences and control characters will be escaped so they can be seen in the output. There will also be no delay between the output of frames (under the assumption that if you want to see the raw log you want it to output all at once so you can pipe it into some other app). """ prev_frame_time = None try: for count, frame in enumerate(get_frames(log_path)): frame_time = float(frame[:13]) # First 13 chars is the timestamp frame = frame[14:] # [14:] Skips the timestamp and the colon if count == 0: # Write it out immediately if show_esc: frame = raw(frame) file_like.write(frame) prev_frame_time = frame_time else: if show_esc: frame = raw(frame) else: # Wait until the time between the previous frame and now # has passed wait_time = (frame_time - prev_frame_time) / 1000.0 sleep(wait_time) # frame times are in milliseconds file_like.write(frame) prev_frame_time = frame_time file_like.flush() except IOError: # Something wrong with the file return
def flatten_log(log_path, file_like, preserve_renditions=True, show_esc=False): """ Given a log file at *log_path*, write a string of log lines contained within to *file_like*. Where *file_like* is expected to be any file-like object with write() and flush() methods. If *preserve_renditions* is True, CSI escape sequences for renditions will be preserved as-is (e.g. font color, background, etc). This is to make the output appear as close to how it was originally displayed as possible. Besides that, it looks really nice =) If *show_esc* is True, escape sequences and control characters will be visible in the output. Trailing whitespace and escape sequences will not be removed. ..note:: Converts our standard recording-based log format into something that can be used with grep and similar search/filter tools. """ from terminal import Terminal, SPECIAL metadata = get_log_metadata(log_path) rows = metadata.get('rows', 24) cols = metadata.get('columns', None) if not cols: # Try the old metadata format which used 'cols': cols = metadata.get('cols', 80) term = Terminal(rows=rows, cols=cols, em_dimensions=0) out_line = u"" cr = False # We skip the first frame, [1:] because it holds the recording metadata for count, frame in enumerate(get_frames(log_path)): if count == 0: # Skip the first frame (it's just JSON-encoded metadata) continue # First 13 chars is the timestamp: frame_time = float(frame.decode('UTF-8', 'ignore')[:13]) # Convert to datetime object frame_time = datetime.fromtimestamp(frame_time/1000) if show_esc: frame_time = frame_time.strftime(u'\x1b[0m%b %m %H:%M:%S') else: # Renditions preserved == I want pretty. Make the date bold: frame_time = frame_time.strftime(u'\x1b[0;1m%b %m %H:%M:%S\x1b[m') if not show_esc: term.write(frame[14:]) if term.capture: # Capturing a file... Keep feeding it frames until complete continue elif term.captured_files: for line in term.screen: # Find all the characters that come before/after the capture for char in line: if ord(char) >= SPECIAL: adjusted = escape_escape_seq(out_line, rstrip=True) adjusted = frame_time + u' %s\n' % adjusted file_like.write(adjusted.encode('utf-8')) out_line = u"" if char in term.captured_files: captured_file = term.captured_files[char].file_obj captured_file.seek(0) file_like.write(captured_file.read()) file_like.write(b'\n') del captured_file term.clear_screen() term.close_captured_fds() # Instant cleanup else: out_line += char if not out_line: continue adjusted = frame_time + u' %s\n' % out_line.strip() file_like.write(adjusted.encode('utf-8')) out_line = u"" continue else: term.clear_screen() frame = frame.decode('UTF-8', 'ignore') for char in frame[14:]: if '\x1b[H\x1b[2J' in out_line: # Clear screen sequence # Handle the clear screen (usually ctrl-l) by outputting # a new log entry line to avoid confusion regarding what # happened at this time. out_line += u"^L" # Clear screen is a ctrl-l or equivalent if show_esc: adjusted = raw(out_line) else: adjusted = escape_escape_seq(out_line, rstrip=True) adjusted = frame_time + u' %s\n' % adjusted file_like.write(adjusted.encode('utf-8')) out_line = u"" continue if char == u'\n': if show_esc: adjusted = raw(out_line) else: adjusted = escape_escape_seq(out_line, rstrip=True) if not adjusted: out_line = u"" # Skip empty lines continue adjusted = frame_time + u' %s\n' % adjusted file_like.write(adjusted.encode('utf-8')) out_line = u"" cr = False elif char == u'\r': # Carriage returns need special handling. Make a note of it cr = True else: # \r without \n means that characters were (likely) # overwritten. This usually happens when the user gets to # the end of the line (which would create a newline in the # terminal but not necessarily the log), erases their # current line (e.g. ctrl-u), or an escape sequence modified # the line in-place. To clearly indicate what happened we # insert a '^M' and start a new line so as to avoid # confusion over these events. if cr: out_line += "^M" file_like.write((frame_time + u' ').encode('utf-8')) if show_esc: adjusted = raw(out_line) else: adjusted = escape_escape_seq(out_line, rstrip=True) file_like.write((adjusted + u'\n').encode('utf-8')) out_line = u"" out_line += char cr = False file_like.flush() del term
def escape_escape_seq(text, preserve_renditions=True, rstrip=True): """ Escapes escape sequences so they don't muck with the terminal viewing *text* Also replaces special characters with unicode symbol equivalents (e.g. so you can see what they are without having them do anything to your running shell) If *preserve_renditions* is True, CSI escape sequences for renditions will be preserved as-is (e.g. font color, background, etc). If *rstrip* is true, trailing escape sequences and whitespace will be removed. """ esc_sequence = re.compile( r'\x1b(.*\x1b\\|[ABCDEFGHIJKLMNOQRSTUVWXYZa-z0-9=]|[()# %*+].)') csi_sequence = re.compile(r'\x1B\[([?A-Za-z0-9;@:\!]*)([A-Za-z@_])') #esc_rstrip = re.compile('[ \t]+\x1b.+$') out = u"" esc_buffer = u"" # If this seems confusing it is because text parsing is a black art! ARRR! for char in text: if not esc_buffer: if char == u'\x1b': # Start of an ESC sequence esc_buffer = char # TODO: Determine if we should bring this back: #elif ord(char) in replacement_map: #out += replacement_map[ord(char)] else: # Vanilla char. Booooring. out += raw(char) else: # Something interesting is going on esc_buffer += char if char == u'\x07' or esc_buffer.endswith(u'\x1b\\'): # Likely title esc_buffer = u'' # Nobody wants to see your naked ESC sequence continue elif esc_buffer.endswith('\x1b\\'): esc_buffer = u'' # Ignore continue # Nobody wants to see plain ESC sequences in the buf... match_obj = esc_sequence.match(esc_buffer) if match_obj: #seq_type = match_obj.group(1) esc_buffer = u'' # Just when you thought you've ESC'd... continue # CSI ESC sequences... These are worth a second look match_obj = csi_sequence.match(esc_buffer) if match_obj: csi_type = match_obj.group(2) if csi_type == 'm' and preserve_renditions: # mmmmmm! out += esc_buffer # Ooh, naked viewing of pretty things! elif csi_type == 'C': # Move cursor right (we want to do this) # Will be something like this: \x1b[208C num_spaces = match_obj.group(1) if not num_spaces: num_spaces = 1 spaces = int(num_spaces) out += u' ' * spaces # Add an equivalent amount of spaces esc_buffer = u'' # Make room for more! continue if rstrip: # Remove trailing whitespace + trailing ESC sequences return out.rstrip() else: # All these trailers better make for a good movie return out
def flatten_log(log_path, file_like, preserve_renditions=True, show_esc=False): """ Given a log file at *log_path*, write a string of log lines contained within to *file_like*. Where *file_like* is expected to be any file-like object with write() and flush() methods. If *preserve_renditions* is True, CSI escape sequences for renditions will be preserved as-is (e.g. font color, background, etc). This is to make the output appear as close to how it was originally displayed as possible. Besides that, it looks really nice =) If *show_esc* is True, escape sequences and control characters will be visible in the output. Trailing whitespace and escape sequences will not be removed. ..note:: Converts our standard recording-based log format into something that can be used with grep and similar search/filter tools. """ from terminal import Terminal, SPECIAL metadata = get_log_metadata(log_path) rows = metadata.get('rows', 24) cols = metadata.get('columns', None) if not cols: # Try the old metadata format which used 'cols': cols = metadata.get('cols', 80) term = Terminal(rows=rows, cols=cols, em_dimensions=0) out_line = u"" cr = False # We skip the first frame, [1:] because it holds the recording metadata for count, frame in enumerate(get_frames(log_path)): if count == 0: # Skip the first frame (it's just JSON-encoded metadata) continue # First 13 chars is the timestamp: frame_time = float(frame.decode('UTF-8', 'ignore')[:13]) # Convert to datetime object frame_time = datetime.fromtimestamp(frame_time / 1000) if show_esc: frame_time = frame_time.strftime(u'\x1b[0m%b %m %H:%M:%S') else: # Renditions preserved == I want pretty. Make the date bold: frame_time = frame_time.strftime(u'\x1b[0;1m%b %m %H:%M:%S\x1b[m') if not show_esc: term.write(frame[14:]) if term.capture: # Capturing a file... Keep feeding it frames until complete continue elif term.captured_files: for line in term.screen: # Find all the characters that come before/after the capture for char in line: if ord(char) >= SPECIAL: adjusted = escape_escape_seq(out_line, rstrip=True) adjusted = frame_time + u' %s\n' % adjusted file_like.write(adjusted.encode('utf-8')) out_line = u"" if char in term.captured_files: captured_file = term.captured_files[char].file_obj captured_file.seek(0) file_like.write(captured_file.read()) file_like.write(b'\n') del captured_file term.clear_screen() term.close_captured_fds() # Instant cleanup else: out_line += char if not out_line: continue adjusted = frame_time + u' %s\n' % out_line.strip() file_like.write(adjusted.encode('utf-8')) out_line = u"" continue else: term.clear_screen() frame = frame.decode('UTF-8', 'ignore') for char in frame[14:]: if '\x1b[H\x1b[2J' in out_line: # Clear screen sequence # Handle the clear screen (usually ctrl-l) by outputting # a new log entry line to avoid confusion regarding what # happened at this time. out_line += u"^L" # Clear screen is a ctrl-l or equivalent if show_esc: adjusted = raw(out_line) else: adjusted = escape_escape_seq(out_line, rstrip=True) adjusted = frame_time + u' %s\n' % adjusted file_like.write(adjusted.encode('utf-8')) out_line = u"" continue if char == u'\n': if show_esc: adjusted = raw(out_line) else: adjusted = escape_escape_seq(out_line, rstrip=True) if not adjusted: out_line = u"" # Skip empty lines continue adjusted = frame_time + u' %s\n' % adjusted file_like.write(adjusted.encode('utf-8')) out_line = u"" cr = False elif char == u'\r': # Carriage returns need special handling. Make a note of it cr = True else: # \r without \n means that characters were (likely) # overwritten. This usually happens when the user gets to # the end of the line (which would create a newline in the # terminal but not necessarily the log), erases their # current line (e.g. ctrl-u), or an escape sequence modified # the line in-place. To clearly indicate what happened we # insert a '^M' and start a new line so as to avoid # confusion over these events. if cr: out_line += "^M" file_like.write((frame_time + u' ').encode('utf-8')) if show_esc: adjusted = raw(out_line) else: adjusted = escape_escape_seq(out_line, rstrip=True) file_like.write((adjusted + u'\n').encode('utf-8')) out_line = u"" out_line += char cr = False file_like.flush() del term
def escape_escape_seq(text, preserve_renditions=True, rstrip=True): """ Escapes escape sequences so they don't muck with the terminal viewing *text* Also replaces special characters with unicode symbol equivalents (e.g. so you can see what they are without having them do anything to your running shell) If *preserve_renditions* is True, CSI escape sequences for renditions will be preserved as-is (e.g. font color, background, etc). If *rstrip* is true, trailing escape sequences and whitespace will be removed. """ esc_sequence = re.compile( r'\x1b(.*\x1b\\|[ABCDEFGHIJKLMNOQRSTUVWXYZa-z0-9=]|[()# %*+].)') csi_sequence = re.compile(r'\x1B\[([?A-Za-z0-9;@:\!]*)([A-Za-z@_])') #esc_rstrip = re.compile('[ \t]+\x1b.+$') out = u"" esc_buffer = u"" # If this seems confusing it is because text parsing is a black art! ARRR! for char in text: if not esc_buffer: if char == u'\x1b': # Start of an ESC sequence esc_buffer = char # TODO: Determine if we should bring this back: #elif ord(char) in replacement_map: #out += replacement_map[ord(char)] else: # Vanilla char. Booooring. out += raw(char) else: # Something interesting is going on esc_buffer += char if char == u'\x07' or esc_buffer.endswith( u'\x1b\\'): # Likely title esc_buffer = u'' # Nobody wants to see your naked ESC sequence continue elif esc_buffer.endswith('\x1b\\'): esc_buffer = u'' # Ignore continue # Nobody wants to see plain ESC sequences in the buf... match_obj = esc_sequence.match(esc_buffer) if match_obj: #seq_type = match_obj.group(1) esc_buffer = u'' # Just when you thought you've ESC'd... continue # CSI ESC sequences... These are worth a second look match_obj = csi_sequence.match(esc_buffer) if match_obj: csi_type = match_obj.group(2) if csi_type == 'm' and preserve_renditions: # mmmmmm! out += esc_buffer # Ooh, naked viewing of pretty things! elif csi_type == 'C': # Move cursor right (we want to do this) # Will be something like this: \x1b[208C num_spaces = match_obj.group(1) if not num_spaces: num_spaces = 1 spaces = int(num_spaces) out += u' ' * spaces # Add an equivalent amount of spaces esc_buffer = u'' # Make room for more! continue if rstrip: # Remove trailing whitespace + trailing ESC sequences return out.rstrip() else: # All these trailers better make for a good movie return out