summaryrefslogtreecommitdiffstats
path: root/scripts/automation/trex_control_plane/stl/console/trex_tui.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/automation/trex_control_plane/stl/console/trex_tui.py')
-rw-r--r--scripts/automation/trex_control_plane/stl/console/trex_tui.py663
1 files changed, 609 insertions, 54 deletions
diff --git a/scripts/automation/trex_control_plane/stl/console/trex_tui.py b/scripts/automation/trex_control_plane/stl/console/trex_tui.py
index d3be4435..a69c4165 100644
--- a/scripts/automation/trex_control_plane/stl/console/trex_tui.py
+++ b/scripts/automation/trex_control_plane/stl/console/trex_tui.py
@@ -4,6 +4,8 @@ import os
import time
from collections import OrderedDict, deque
import datetime
+import readline
+from texttable import ansi_len
if sys.version_info > (3,0):
from io import StringIO
@@ -15,9 +17,23 @@ from trex_stl_lib.utils import text_tables
from trex_stl_lib import trex_stl_stats
from trex_stl_lib.utils.filters import ToggleFilter
+class TUIQuit(Exception):
+ pass
+
+
# for STL exceptions
from trex_stl_lib.api import *
+def ascii_split (s):
+ output = []
+
+ lines = s.split('\n')
+ for elem in lines:
+ if ansi_len(elem) > 0:
+ output.append(elem)
+
+ return output
+
class SimpleBar(object):
def __init__ (self, desc, pattern):
self.desc = desc
@@ -210,7 +226,7 @@ class TrexTUILatencyStats(TrexTUIPanel):
super(TrexTUILatencyStats, self).__init__(mng, "lstats")
self.key_actions = OrderedDict()
self.key_actions['c'] = {'action': self.action_clear, 'legend': 'clear', 'show': True}
- self.key_actions['t'] = {'action': self.action_toggle_histogram, 'legend': 'toggle histogram', 'show': True}
+ self.key_actions['h'] = {'action': self.action_toggle_histogram, 'legend': 'histogram toggle', 'show': True}
self.is_histogram = False
@@ -238,6 +254,23 @@ class TrexTUILatencyStats(TrexTUIPanel):
self.stateless_client.latency_stats.clear_stats()
return ""
+
+# utilization stats
+class TrexTUIUtilizationStats(TrexTUIPanel):
+ def __init__ (self, mng):
+ super(TrexTUIUtilizationStats, self).__init__(mng, "ustats")
+ self.key_actions = {}
+
+ def show (self):
+ stats = self.stateless_client._get_formatted_stats(port_id_list = None, stats_mask = trex_stl_stats.UT_COMPAT)
+ # print stats to screen
+ for stat_type, stat_data in stats.items():
+ text_tables.print_table_with_header(stat_data.text_table, stat_type)
+
+ def get_key_actions (self):
+ return self.key_actions
+
+
# log
class TrexTUILog():
def __init__ (self):
@@ -258,24 +291,42 @@ class TrexTUILog():
print(msg)
+# a predicate to wrap function as a bool
+class Predicate(object):
+ def __init__ (self, func):
+ self.func = func
+
+ def __nonzero__ (self):
+ return True if self.func() else False
+ def __bool__ (self):
+ return True if self.func() else False
+
+
# Panels manager (contains server panels)
class TrexTUIPanelManager():
def __init__ (self, tui):
self.tui = tui
self.stateless_client = tui.stateless_client
self.ports = self.stateless_client.get_all_ports()
-
+ self.locked = False
self.panels = {}
self.panels['dashboard'] = TrexTUIDashBoard(self)
self.panels['sstats'] = TrexTUIStreamsStats(self)
self.panels['lstats'] = TrexTUILatencyStats(self)
+ self.panels['ustats'] = TrexTUIUtilizationStats(self)
self.key_actions = OrderedDict()
- self.key_actions['q'] = {'action': self.action_quit, 'legend': 'quit', 'show': True}
+
+ # we allow console only when ports are acquired
+ self.key_actions['ESC'] = {'action': self.action_none, 'legend': 'console', 'show': Predicate(lambda : not self.locked)}
+
+ self.key_actions['q'] = {'action': self.action_none, 'legend': 'quit', 'show': True}
self.key_actions['d'] = {'action': self.action_show_dash, 'legend': 'dashboard', 'show': True}
self.key_actions['s'] = {'action': self.action_show_sstats, 'legend': 'streams', 'show': True}
self.key_actions['l'] = {'action': self.action_show_lstats, 'legend': 'latency', 'show': True}
+ self.key_actions['u'] = {'action': self.action_show_ustats, 'legend': 'util', 'show': True}
+
# start with dashboard
self.main_panel = self.panels['dashboard']
@@ -290,7 +341,6 @@ class TrexTUIPanelManager():
self.show_log = False
-
def generate_legend (self):
self.legend = "\n{:<12}".format("browse:")
@@ -327,14 +377,18 @@ class TrexTUIPanelManager():
# on window switch or turn on / off of the TUI we call this
- def init (self, show_log = False):
+ def init (self, show_log = False, locked = False):
self.show_log = show_log
+ self.locked = locked
self.generate_legend()
- def show (self):
+ def show (self, show_legend):
self.main_panel.show()
self.print_connection_status()
- self.print_legend()
+
+ if show_legend:
+ self.generate_legend()
+ self.print_legend()
if self.show_log:
self.log.show()
@@ -350,21 +404,22 @@ class TrexTUIPanelManager():
msg = self.main_panel.get_key_actions()[ch]['action']()
else:
- msg = ""
+ return False
self.generate_legend()
-
- if msg == None:
- return False
- else:
- if msg:
- self.log.add_event(msg)
- return True
+ return True
+
+ #if msg == None:
+ # return False
+ #else:
+ # if msg:
+ # self.log.add_event(msg)
+ # return True
# actions
- def action_quit (self):
+ def action_none (self):
return None
def action_show_dash (self):
@@ -392,6 +447,11 @@ class TrexTUIPanelManager():
self.init(self.show_log)
return ""
+ def action_show_ustats(self):
+ self.main_panel = self.panels['ustats']
+ self.init(self.show_log)
+ return ""
+
# shows a textual top style window
class TrexTUI():
@@ -400,52 +460,71 @@ class TrexTUI():
STATE_RECONNECT = 2
is_graph = False
+ MIN_ROWS = 50
+ MIN_COLS = 111
+
+ class ScreenSizeException(Exception):
+ def __init__ (self, cols, rows):
+ msg = "TUI requires console screen size of at least {0}x{1}, current is {2}x{3}".format(TrexTUI.MIN_COLS,
+ TrexTUI.MIN_ROWS,
+ cols,
+ rows)
+ super(TrexTUI.ScreenSizeException, self).__init__(msg)
+
def __init__ (self, stateless_client):
self.stateless_client = stateless_client
self.pm = TrexTUIPanelManager(self)
+
+ def clear_screen (self, lines = 50):
+ # reposition the cursor
+ sys.stdout.write("\x1b[0;0H")
+ # clear all lines
+ for i in range(lines):
+ sys.stdout.write("\x1b[0K")
+ if i < (lines - 1):
+ sys.stdout.write("\n")
- def handle_key_input (self):
- # try to read a single key
- ch = os.read(sys.stdin.fileno(), 1).decode()
- if ch != None and len(ch) > 0:
- return (self.pm.handle_key(ch), True)
+ # reposition the cursor
+ sys.stdout.write("\x1b[0;0H")
- else:
- return (True, False)
-
+ #sys.stdout.write("\x1b[2J\x1b[H")
- def clear_screen (self):
- #os.system('clear')
- # maybe this is faster ?
- sys.stdout.write("\x1b[2J\x1b[H")
+ def show (self, client, save_console_history, show_log = False, locked = False):
+
+ rows, cols = os.popen('stty size', 'r').read().split()
+ if (int(rows) < TrexTUI.MIN_ROWS) or (int(cols) < TrexTUI.MIN_COLS):
+ raise self.ScreenSizeException(rows = rows, cols = cols)
+ with AsyncKeys(client, save_console_history, locked) as async_keys:
+ sys.stdout.write("\x1bc")
+ self.async_keys = async_keys
+ self.show_internal(show_log, locked)
- def show (self, show_log = False):
- # init termios
- old_settings = termios.tcgetattr(sys.stdin)
- new_settings = termios.tcgetattr(sys.stdin)
- new_settings[3] = new_settings[3] & ~(termios.ECHO | termios.ICANON) # lflags
- new_settings[6][termios.VMIN] = 0 # cc
- new_settings[6][termios.VTIME] = 0 # cc
- termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
- self.pm.init(show_log)
+
+ def show_internal (self, show_log, locked):
+
+ self.pm.init(show_log, locked)
self.state = self.STATE_ACTIVE
- self.draw_policer = 0
+ self.last_redraw_ts = 0
try:
while True:
# draw and handle user input
- cont, force_draw = self.handle_key_input()
- self.draw_screen(force_draw)
- if not cont:
- break
- time.sleep(0.1)
+ status = self.async_keys.tick(self.pm)
+
+ self.draw_screen(status)
+
+ # speedup for keys, slower for no keys
+ if status == AsyncKeys.STATUS_NONE:
+ time.sleep(0.01)
+ else:
+ time.sleep(0.001)
# regular state
if self.state == self.STATE_ACTIVE:
@@ -473,34 +552,510 @@ class TrexTUI():
self.state = self.STATE_LOST_CONT
- finally:
- # restore
- termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
+ except TUIQuit:
+ print("\nExiting TUI...")
print("")
# draw once
- def draw_screen (self, force_draw = False):
-
- if (self.draw_policer >= 5) or (force_draw):
+ def draw_screen (self, status):
+ t = time.time() - self.last_redraw_ts
+ redraw = (t >= 0.5) or (status == AsyncKeys.STATUS_REDRAW_ALL)
+ if redraw:
# capture stdout to a string
old_stdout = sys.stdout
sys.stdout = mystdout = StringIO()
- self.pm.show()
+ self.pm.show(show_legend = self.async_keys.is_legend_mode())
+ self.last_snap = mystdout.getvalue()
+
+ self.async_keys.draw()
sys.stdout = old_stdout
self.clear_screen()
- print(mystdout.getvalue())
+ sys.stdout.write(mystdout.getvalue())
+
+ sys.stdout.flush()
+ self.last_redraw_ts = time.time()
+
+ elif status == AsyncKeys.STATUS_REDRAW_KEYS:
+ sys.stdout.write("\x1b[4A")
+ self.async_keys.draw()
sys.stdout.flush()
- self.draw_policer = 0
- else:
- self.draw_policer += 1
+ return
+
+
def get_state (self):
return self.state
+
+
+
+
+# handles async IO
+class AsyncKeys:
+
+ MODE_LEGEND = 1
+ MODE_CONSOLE = 2
+
+ STATUS_NONE = 0
+ STATUS_REDRAW_KEYS = 1
+ STATUS_REDRAW_ALL = 2
+
+ def __init__ (self, client, save_console_history, locked = False):
+ self.engine_console = AsyncKeysEngineConsole(self, client, save_console_history)
+ self.engine_legend = AsyncKeysEngineLegend(self)
+ self.locked = locked
+
+ if locked:
+ self.engine = self.engine_legend
+ self.locked = True
+ else:
+ self.engine = self.engine_console
+ self.locked = False
+
+ def __enter__ (self):
+ # init termios
+ self.old_settings = termios.tcgetattr(sys.stdin)
+ new_settings = termios.tcgetattr(sys.stdin)
+ new_settings[3] = new_settings[3] & ~(termios.ECHO | termios.ICANON) # lflags
+ new_settings[6][termios.VMIN] = 0 # cc
+ new_settings[6][termios.VTIME] = 0 # cc
+ termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
+
+ # huge buffer - no print without flush
+ sys.stdout = open('/dev/stdout', 'w', TrexTUI.MIN_COLS * TrexTUI.MIN_COLS * 2)
+ return self
+
+ def __exit__ (self, type, value, traceback):
+ termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)
+
+ # restore sys.stdout
+ sys.stdout.close()
+ sys.stdout = sys.__stdout__
+
+
+ def is_legend_mode (self):
+ return self.engine.get_type() == AsyncKeys.MODE_LEGEND
+
+ def is_console_mode (self):
+ return self.engine.get_type == AsyncKeys.MODE_CONSOLE
+
+ def switch (self):
+ if self.is_legend_mode():
+ self.engine = self.engine_console
+ else:
+ self.engine = self.engine_legend
+
+
+ def tick (self, pm):
+ seq = ''
+ # drain all chars
+ while True:
+ ch = os.read(sys.stdin.fileno(), 1).decode()
+ if not ch:
+ break
+ seq += ch
+
+ if not seq:
+ return self.STATUS_NONE
+
+ # ESC for switch
+ if seq == '\x1b':
+ if not self.locked:
+ self.switch()
+ return self.STATUS_REDRAW_ALL
+
+ # EOF (ctrl + D)
+ if seq == '\x04':
+ raise TUIQuit()
+
+ # pass tick to engine
+ return self.engine.tick(seq, pm)
+
+
+ def draw (self):
+ self.engine.draw()
+
+
+
+# Legend engine
+class AsyncKeysEngineLegend:
+ def __init__ (self, async):
+ self.async = async
+
+ def get_type (self):
+ return self.async.MODE_LEGEND
+
+ def tick (self, seq, pm):
+
+ if seq == 'q':
+ raise TUIQuit()
+
+ # ignore escapes
+ if len(seq) > 1:
+ return AsyncKeys.STATUS_NONE
+
+ rc = pm.handle_key(seq)
+ return AsyncKeys.STATUS_REDRAW_ALL if rc else AsyncKeys.STATUS_NONE
+
+ def draw (self):
+ pass
+
+
+# console engine
+class AsyncKeysEngineConsole:
+ def __init__ (self, async, client, save_console_history):
+ self.async = async
+ self.lines = deque(maxlen = 100)
+
+ self.generate_prompt = client.generate_prompt
+ self.save_console_history = save_console_history
+
+ self.ac = {'start' : client.start_line,
+ 'stop' : client.stop_line,
+ 'pause' : client.pause_line,
+ 'push' : client.push_line,
+ 'resume' : client.resume_line,
+ 'update' : client.update_line,
+ 'connect' : client.connect_line,
+ 'disconnect' : client.disconnect_line,
+ 'acquire' : client.acquire_line,
+ 'release' : client.release_line,
+ 'quit' : self.action_quit,
+ 'q' : self.action_quit,
+ 'exit' : self.action_quit,
+ 'help' : self.action_help,
+ '?' : self.action_help}
+
+ # fetch readline history and add relevants
+ for i in range(0, readline.get_current_history_length()):
+ cmd = readline.get_history_item(i)
+ if cmd and cmd.split()[0] in self.ac:
+ self.lines.appendleft(CmdLine(cmd))
+
+ # new line
+ self.lines.appendleft(CmdLine(''))
+ self.line_index = 0
+ self.last_status = ''
+
+ def action_quit (self, _):
+ raise TUIQuit()
+
+ def action_help (self, _):
+ return ' '.join([format_text(cmd, 'bold') for cmd in self.ac.keys()])
+
+ def get_type (self):
+ return self.async.MODE_CONSOLE
+
+
+ def handle_escape_char (self, seq):
+ # up
+ if seq == '\x1b[A':
+ self.line_index = min(self.line_index + 1, len(self.lines) - 1)
+
+ # down
+ elif seq == '\x1b[B':
+ self.line_index = max(self.line_index - 1, 0)
+
+ # left
+ elif seq == '\x1b[D':
+ self.lines[self.line_index].go_left()
+
+ # right
+ elif seq == '\x1b[C':
+ self.lines[self.line_index].go_right()
+
+ # del
+ elif seq == '\x1b[3~':
+ self.lines[self.line_index].del_key()
+
+ # home
+ elif seq == '\x1b[H':
+ self.lines[self.line_index].home_key()
+
+ # end
+ elif seq == '\x1b[F':
+ self.lines[self.line_index].end_key()
+ return True
+
+ # unknown key
+ else:
+ return AsyncKeys.STATUS_NONE
+
+ return AsyncKeys.STATUS_REDRAW_KEYS
+
+
+ def tick (self, seq, _):
+
+ # handle escape chars
+ if len(seq) > 1:
+ return self.handle_escape_char(seq)
+
+ # handle each char
+ for ch in seq:
+ return self.handle_single_key(ch)
+
+
+
+ def handle_single_key (self, ch):
+
+ # newline
+ if ch == '\n':
+ self.handle_cmd()
+
+ # backspace
+ elif ch == '\x7f':
+ self.lines[self.line_index].backspace()
+
+ # TAB
+ elif ch == '\t':
+ tokens = self.lines[self.line_index].get().split()
+ if not tokens:
+ return
+
+ if len(tokens) == 1:
+ self.handle_tab_names(tokens[0])
+ else:
+ self.handle_tab_files(tokens)
+
+
+ # simple char
+ else:
+ self.lines[self.line_index] += ch
+
+ return AsyncKeys.STATUS_REDRAW_KEYS
+
+
+ # handle TAB key for completing function names
+ def handle_tab_names (self, cur):
+ matching_cmds = [x for x in self.ac if x.startswith(cur)]
+
+ common = os.path.commonprefix([x for x in self.ac if x.startswith(cur)])
+ if common:
+ if len(matching_cmds) == 1:
+ self.lines[self.line_index].set(common + ' ')
+ self.last_status = ''
+ else:
+ self.lines[self.line_index].set(common)
+ self.last_status = 'ambigious: '+ ' '.join([format_text(cmd, 'bold') for cmd in matching_cmds])
+
+
+ # handle TAB for completing filenames
+ def handle_tab_files (self, tokens):
+
+ # only commands with files
+ if tokens[0] not in {'start', 'push'}:
+ return
+
+ # '-f' with no paramters - no partial and use current dir
+ if tokens[-1] == '-f':
+ partial = ''
+ d = '.'
+
+ # got a partial path
+ elif tokens[-2] == '-f':
+ partial = tokens.pop()
+
+ # check for dirs
+ dirname, basename = os.path.dirname(partial), os.path.basename(partial)
+ if os.path.isdir(dirname):
+ d = dirname
+ partial = basename
+ else:
+ d = '.'
+ else:
+ return
+
+ # fetch all dirs and files matching wildcard
+ files = []
+ for x in os.listdir(d):
+ if os.path.isdir(os.path.join(d, x)):
+ files.append(x + '/')
+ elif x.endswith('.py') or x.endswith('yaml') or x.endswith('pcap') or x.endswith('cap'):
+ files.append(x)
+
+ # dir might not have the files
+ if not files:
+ self.last_status = format_text('no loadble files under path', 'bold')
+ return
+
+
+ # find all the matching files
+ matching_files = [x for x in files if x.startswith(partial)] if partial else files
+
+ # do we have a longer common than partial ?
+ common = os.path.commonprefix([x for x in files if x.startswith(partial)])
+ if not common:
+ common = partial
+
+ tokens.append(os.path.join(d, common) if d is not '.' else common)
+
+ # reforge the line
+ newline = ' '.join(tokens)
+
+ if len(matching_files) == 1:
+ if os.path.isfile(tokens[-1]):
+ newline += ' '
+
+ self.lines[self.line_index].set(newline)
+ self.last_status = ''
+ else:
+ self.lines[self.line_index].set(newline)
+ self.last_status = ' '.join([format_text(f, 'bold') for f in matching_files[:5]])
+ if len(matching_files) > 5:
+ self.last_status += ' ... [{0} more matches]'.format(len(matching_files) - 5)
+
+
+
+ def split_cmd (self, cmd):
+ s = cmd.split(' ', 1)
+ op = s[0]
+ param = s[1] if len(s) == 2 else ''
+ return op, param
+
+
+ def handle_cmd (self):
+ cmd = self.lines[self.line_index].get().strip()
+ if not cmd:
+ return
+
+ op, param = self.split_cmd(cmd)
+
+ func = self.ac.get(op)
+ if func:
+ func_rc = func(param)
+
+ # take out the empty line
+ empty_line = self.lines.popleft()
+ assert(empty_line.ro_line == '')
+
+ if not self.lines or self.lines[0].ro_line != cmd:
+ self.lines.appendleft(CmdLine(cmd))
+
+ # back in
+ self.lines.appendleft(empty_line)
+ self.line_index = 0
+ readline.add_history(cmd)
+ self.save_console_history()
+
+ # back to readonly
+ for line in self.lines:
+ line.invalidate()
+
+ assert(self.lines[0].modified == False)
+ color = None
+ if not func:
+ self.last_status = "unknown command: '{0}'".format(format_text(cmd.split()[0], 'bold'))
+ else:
+ # internal commands
+ if isinstance(func_rc, str):
+ self.last_status = func_rc
+
+ # RC response
+ else:
+ # success
+ if func_rc:
+ self.last_status = format_text("[OK]", 'green')
+
+ # errors
+ else:
+ err_msgs = ascii_split(str(func_rc))
+ self.last_status = format_text(err_msgs[0], 'red')
+ if len(err_msgs) > 1:
+ self.last_status += " [{0} more errors messages]".format(len(err_msgs) - 1)
+ color = 'red'
+
+ # trim too long lines
+ if ansi_len(self.last_status) > TrexTUI.MIN_COLS:
+ self.last_status = format_text(self.last_status[:TrexTUI.MIN_COLS] + "...", color, 'bold')
+
+
+ def draw (self):
+ sys.stdout.write("\nPress 'ESC' for navigation panel...\n")
+ sys.stdout.write("status: \x1b[0K{0}\n".format(self.last_status))
+ sys.stdout.write("\n{0}\x1b[0K".format(self.generate_prompt(prefix = 'tui')))
+ self.lines[self.line_index].draw()
+
+
+# a readline alike command line - can be modified during edit
+class CmdLine(object):
+ def __init__ (self, line):
+ self.ro_line = line
+ self.w_line = None
+ self.modified = False
+ self.cursor_index = len(line)
+
+ def get (self):
+ if self.modified:
+ return self.w_line
+ else:
+ return self.ro_line
+
+ def set (self, line, cursor_pos = None):
+ self.w_line = line
+ self.modified = True
+
+ if cursor_pos is None:
+ self.cursor_index = len(self.w_line)
+ else:
+ self.cursor_index = cursor_pos
+
+
+ def __add__ (self, other):
+ assert(0)
+
+
+ def __str__ (self):
+ return self.get()
+
+
+ def __iadd__ (self, other):
+
+ self.set(self.get()[:self.cursor_index] + other + self.get()[self.cursor_index:],
+ cursor_pos = self.cursor_index + len(other))
+
+ return self
+
+
+ def backspace (self):
+ if self.cursor_index == 0:
+ return
+
+ self.set(self.get()[:self.cursor_index - 1] + self.get()[self.cursor_index:],
+ self.cursor_index - 1)
+
+
+ def del_key (self):
+ if self.cursor_index == len(self.get()):
+ return
+
+ self.set(self.get()[:self.cursor_index] + self.get()[self.cursor_index + 1:],
+ self.cursor_index)
+
+ def home_key (self):
+ self.cursor_index = 0
+
+ def end_key (self):
+ self.cursor_index = len(self.get())
+
+ def invalidate (self):
+ self.modified = False
+ self.w_line = None
+ self.cursor_index = len(self.ro_line)
+
+ def go_left (self):
+ self.cursor_index = max(0, self.cursor_index - 1)
+
+ def go_right (self):
+ self.cursor_index = min(len(self.get()), self.cursor_index + 1)
+
+ def draw (self):
+ sys.stdout.write(self.get())
+ sys.stdout.write('\b' * (len(self.get()) - self.cursor_index))
+