trex_tui.py revision 552626cf
1from __future__ import print_function
2
3import termios
4import sys
5import os
6import time
7import threading
8
9from collections import OrderedDict, deque
10from texttable import ansi_len
11
12
13import datetime
14import readline
15
16
17if sys.version_info > (3,0):
18    from io import StringIO
19else:
20    from cStringIO import StringIO
21
22from trex_stl_lib.utils.text_opts import *
23from trex_stl_lib.utils import text_tables
24from trex_stl_lib import trex_stl_stats
25from trex_stl_lib.utils.filters import ToggleFilter
26
27class TUIQuit(Exception):
28    pass
29
30
31# for STL exceptions
32from trex_stl_lib.api import *
33
34def ascii_split (s):
35    output = []
36
37    lines = s.split('\n')
38    for elem in lines:
39        if ansi_len(elem) > 0:
40            output.append(elem)
41
42    return output
43
44class SimpleBar(object):
45    def __init__ (self, desc, pattern):
46        self.desc = desc
47        self.pattern = pattern
48        self.pattern_len = len(pattern)
49        self.index = 0
50
51    def show (self, buffer):
52        if self.desc:
53            print(format_text("{0} {1}".format(self.desc, self.pattern[self.index]), 'bold'), file = buffer)
54        else:
55            print(format_text("{0}".format(self.pattern[self.index]), 'bold'), file = buffer)
56
57        self.index = (self.index + 1) % self.pattern_len
58
59
60# base type of a panel
61class TrexTUIPanel(object):
62    def __init__ (self, mng, name):
63
64        self.mng = mng
65        self.name = name
66        self.stateless_client = mng.stateless_client
67        self.is_graph = False
68
69    def show (self, buffer):
70        raise NotImplementedError("must implement this")
71
72    def get_key_actions (self):
73        raise NotImplementedError("must implement this")
74
75
76    def get_name (self):
77        return self.name
78
79
80# dashboard panel
81class TrexTUIDashBoard(TrexTUIPanel):
82
83    FILTER_ACQUIRED = 1
84    FILTER_ALL      = 2
85
86    def __init__ (self, mng):
87        super(TrexTUIDashBoard, self).__init__(mng, "dashboard")
88
89        self.ports = self.stateless_client.get_all_ports()
90
91        self.key_actions = OrderedDict()
92
93        self.key_actions['c'] = {'action': self.action_clear,  'legend': 'clear', 'show': True}
94        self.key_actions['p'] = {'action': self.action_pause,  'legend': 'pause', 'show': True, 'color': 'red'}
95        self.key_actions['r'] = {'action': self.action_resume, 'legend': 'resume', 'show': True, 'color': 'blue'}
96
97        self.key_actions['o'] = {'action': self.action_show_owned,  'legend': 'owned ports', 'show': True}
98        self.key_actions['n'] = {'action': self.action_reset_view,  'legend': 'reset view', 'show': True}
99        self.key_actions['a'] = {'action': self.action_show_all,  'legend': 'all ports', 'show': True}
100
101        # register all the ports to the toggle action
102        for port_id in self.ports:
103            self.key_actions[str(port_id)] = {'action': self.action_toggle_port(port_id), 'legend': 'port {0}'.format(port_id), 'show': False}
104
105
106        self.toggle_filter = ToggleFilter(self.ports)
107
108        if self.stateless_client.get_acquired_ports():
109            self.action_show_owned()
110        else:
111            self.action_show_all()
112
113
114    def get_showed_ports (self):
115        return self.toggle_filter.filter_items()
116
117
118    def show (self, buffer):
119        stats = self.stateless_client._get_formatted_stats(self.get_showed_ports())
120        # print stats to screen
121        for stat_type, stat_data in stats.items():
122            text_tables.print_table_with_header(stat_data.text_table, stat_type, buffer = buffer)
123
124
125    def get_key_actions (self):
126        allowed = OrderedDict()
127
128
129        allowed['n'] = self.key_actions['n']
130        allowed['o'] = self.key_actions['o']
131        allowed['a'] = self.key_actions['a']
132        for i in self.ports:
133            allowed[str(i)] = self.key_actions[str(i)]
134
135
136        if self.get_showed_ports():
137            allowed['c'] = self.key_actions['c']
138
139        # if not all ports are acquired - no operations
140        if not (set(self.get_showed_ports()) <= set(self.stateless_client.get_acquired_ports())):
141            return allowed
142
143        # if any/some ports can be resumed
144        if set(self.get_showed_ports()) & set(self.stateless_client.get_paused_ports()):
145            allowed['r'] = self.key_actions['r']
146
147        # if any/some ports are transmitting - support those actions
148        if set(self.get_showed_ports()) & set(self.stateless_client.get_transmitting_ports()):
149            allowed['p'] = self.key_actions['p']
150
151
152        return allowed
153
154
155    ######### actions
156    def action_pause (self):
157        try:
158            rc = self.stateless_client.pause(ports = self.get_showed_ports())
159        except STLError:
160            pass
161
162        return ""
163
164
165
166    def action_resume (self):
167        try:
168            self.stateless_client.resume(ports = self.get_showed_ports())
169        except STLError:
170            pass
171
172        return ""
173
174
175    def action_reset_view (self):
176        self.toggle_filter.reset()
177        return ""
178
179    def action_show_owned (self):
180        self.toggle_filter.reset()
181        self.toggle_filter.toggle_items(*self.stateless_client.get_acquired_ports())
182        return ""
183
184    def action_show_all (self):
185        self.toggle_filter.reset()
186        self.toggle_filter.toggle_items(*self.stateless_client.get_all_ports())
187        return ""
188
189    def action_clear (self):
190        self.stateless_client.clear_stats(self.toggle_filter.filter_items())
191        return "cleared all stats"
192
193
194    def action_toggle_port(self, port_id):
195        def action_toggle_port_x():
196            self.toggle_filter.toggle_item(port_id)
197            return ""
198
199        return action_toggle_port_x
200
201
202
203# streams stats
204class TrexTUIStreamsStats(TrexTUIPanel):
205    def __init__ (self, mng):
206        super(TrexTUIStreamsStats, self).__init__(mng, "sstats")
207
208        self.key_actions = OrderedDict()
209
210        self.key_actions['c'] = {'action': self.action_clear,  'legend': 'clear', 'show': True}
211
212
213    def show (self, buffer):
214        stats = self.stateless_client._get_formatted_stats(port_id_list = None, stats_mask = trex_stl_stats.SS_COMPAT)
215        # print stats to screen
216        for stat_type, stat_data in stats.items():
217            text_tables.print_table_with_header(stat_data.text_table, stat_type, buffer = buffer)
218        pass
219
220
221    def get_key_actions (self):
222        return self.key_actions
223
224    def action_clear (self):
225         self.stateless_client.flow_stats.clear_stats()
226
227         return ""
228
229
230# latency stats
231class TrexTUILatencyStats(TrexTUIPanel):
232    def __init__ (self, mng):
233        super(TrexTUILatencyStats, self).__init__(mng, "lstats")
234        self.key_actions = OrderedDict()
235        self.key_actions['c'] = {'action': self.action_clear,  'legend': 'clear', 'show': True}
236        self.key_actions['h'] = {'action': self.action_toggle_histogram,  'legend': 'histogram toggle', 'show': True}
237        self.is_histogram = False
238
239
240    def show (self, buffer):
241        if self.is_histogram:
242            stats = self.stateless_client._get_formatted_stats(port_id_list = None, stats_mask = trex_stl_stats.LH_COMPAT)
243        else:
244            stats = self.stateless_client._get_formatted_stats(port_id_list = None, stats_mask = trex_stl_stats.LS_COMPAT)
245        # print stats to screen
246        for stat_type, stat_data in stats.items():
247            if stat_type == 'latency_statistics':
248                untouched_header = ' (usec)'
249            else:
250                untouched_header = ''
251            text_tables.print_table_with_header(stat_data.text_table, stat_type, untouched_header = untouched_header, buffer = buffer)
252
253    def get_key_actions (self):
254        return self.key_actions
255
256    def action_toggle_histogram (self):
257        self.is_histogram = not self.is_histogram
258        return ""
259
260    def action_clear (self):
261         self.stateless_client.latency_stats.clear_stats()
262         return ""
263
264
265# utilization stats
266class TrexTUIUtilizationStats(TrexTUIPanel):
267    def __init__ (self, mng):
268        super(TrexTUIUtilizationStats, self).__init__(mng, "ustats")
269        self.key_actions = {}
270
271    def show (self, buffer):
272        stats = self.stateless_client._get_formatted_stats(port_id_list = None, stats_mask = trex_stl_stats.UT_COMPAT)
273        # print stats to screen
274        for stat_type, stat_data in stats.items():
275            text_tables.print_table_with_header(stat_data.text_table, stat_type, buffer = buffer)
276
277    def get_key_actions (self):
278        return self.key_actions
279
280
281# log
282class TrexTUILog():
283    def __init__ (self):
284        self.log = []
285
286    def add_event (self, msg):
287        self.log.append("[{0}] {1}".format(str(datetime.datetime.now().time()), msg))
288
289    def show (self, buffer, max_lines = 4):
290
291        cut = len(self.log) - max_lines
292        if cut < 0:
293            cut = 0
294
295        print(format_text("\nLog:", 'bold', 'underline'), file = buffer)
296
297        for msg in self.log[cut:]:
298            print(msg, file = buffer)
299
300
301# a predicate to wrap function as a bool
302class Predicate(object):
303    def __init__ (self, func):
304        self.func = func
305
306    def __nonzero__ (self):
307        return True if self.func() else False
308    def __bool__ (self):
309        return True if self.func() else False
310
311
312# Panels manager (contains server panels)
313class TrexTUIPanelManager():
314    def __init__ (self, tui):
315        self.tui = tui
316        self.stateless_client = tui.stateless_client
317        self.ports = self.stateless_client.get_all_ports()
318        self.locked = False
319
320        self.panels = {}
321        self.panels['dashboard'] = TrexTUIDashBoard(self)
322        self.panels['sstats']    = TrexTUIStreamsStats(self)
323        self.panels['lstats']    = TrexTUILatencyStats(self)
324        self.panels['ustats']    = TrexTUIUtilizationStats(self)
325
326        self.key_actions = OrderedDict()
327
328        # we allow console only when ports are acquired
329        self.key_actions['ESC'] = {'action': self.action_none, 'legend': 'console', 'show': Predicate(lambda : not self.locked)}
330
331        self.key_actions['q'] = {'action': self.action_none, 'legend': 'quit', 'show': True}
332        self.key_actions['d'] = {'action': self.action_show_dash, 'legend': 'dashboard', 'show': True}
333        self.key_actions['s'] = {'action': self.action_show_sstats, 'legend': 'streams', 'show': True}
334        self.key_actions['l'] = {'action': self.action_show_lstats, 'legend': 'latency', 'show': True}
335        self.key_actions['u'] = {'action': self.action_show_ustats, 'legend': 'util', 'show': True}
336
337
338        # start with dashboard
339        self.main_panel = self.panels['dashboard']
340
341        # log object
342        self.log = TrexTUILog()
343
344        self.generate_legend()
345
346        self.conn_bar = SimpleBar('status: ', ['|','/','-','\\'])
347        self.dis_bar =  SimpleBar('status: ', ['X', ' '])
348        self.show_log = False
349
350
351    def generate_legend (self):
352
353        self.legend = "\n{:<12}".format("browse:")
354
355        for k, v in self.key_actions.items():
356            if v['show']:
357                x = "'{0}' - {1}, ".format(k, v['legend'])
358                if v.get('color'):
359                    self.legend += "{:}".format(format_text(x, v.get('color')))
360                else:
361                    self.legend += "{:}".format(x)
362
363
364        self.legend += "\n{:<12}".format(self.main_panel.get_name() + ":")
365
366        for k, v in self.main_panel.get_key_actions().items():
367            if v['show']:
368                x = "'{0}' - {1}, ".format(k, v['legend'])
369
370                if v.get('color'):
371                    self.legend += "{:}".format(format_text(x, v.get('color')))
372                else:
373                    self.legend += "{:}".format(x)
374
375
376    def print_connection_status (self, buffer):
377        if self.tui.get_state() == self.tui.STATE_ACTIVE:
378            self.conn_bar.show(buffer = buffer)
379        else:
380            self.dis_bar.show(buffer = buffer)
381
382    def print_legend (self, buffer):
383        print(format_text(self.legend, 'bold'), file = buffer)
384
385
386    # on window switch or turn on / off of the TUI we call this
387    def init (self, show_log = False, locked = False):
388        self.show_log = show_log
389        self.locked = locked
390        self.generate_legend()
391
392    def show (self, show_legend, buffer):
393        self.main_panel.show(buffer)
394        self.print_connection_status(buffer)
395
396        if show_legend:
397            self.generate_legend()
398            self.print_legend(buffer)
399
400        if self.show_log:
401            self.log.show(buffer)
402
403
404    def handle_key (self, ch):
405        # check for the manager registered actions
406        if ch in self.key_actions:
407            msg = self.key_actions[ch]['action']()
408
409        # check for main panel actions
410        elif ch in self.main_panel.get_key_actions():
411            msg = self.main_panel.get_key_actions()[ch]['action']()
412
413        else:
414            return False
415
416        self.generate_legend()
417        return True
418
419        #if msg == None:
420        #    return False
421        #else:
422        #    if msg:
423        #        self.log.add_event(msg)
424        #    return True
425
426
427    # actions
428
429    def action_none (self):
430        return None
431
432    def action_show_dash (self):
433        self.main_panel = self.panels['dashboard']
434        self.init(self.show_log)
435        return ""
436
437    def action_show_port (self, port_id):
438        def action_show_port_x ():
439            self.main_panel = self.panels['port {0}'.format(port_id)]
440            self.init()
441            return ""
442
443        return action_show_port_x
444
445
446    def action_show_sstats (self):
447        self.main_panel = self.panels['sstats']
448        self.init(self.show_log)
449        return ""
450
451
452    def action_show_lstats (self):
453        self.main_panel = self.panels['lstats']
454        self.init(self.show_log)
455        return ""
456
457    def action_show_ustats(self):
458        self.main_panel = self.panels['ustats']
459        self.init(self.show_log)
460        return ""
461
462
463
464# ScreenBuffer is a class designed to
465# avoid inline delays when reprinting the screen
466class ScreenBuffer():
467    def __init__ (self, redraw_cb):
468        self.snapshot = ''
469        self.lock = threading.Lock()
470
471        self.redraw_cb = redraw_cb
472        self.update_flag = False
473
474
475    def start (self):
476        self.active = True
477        self.t = threading.Thread(target = self.__handler)
478        self.t.setDaemon(True)
479        self.t.start()
480
481    def stop (self):
482        self.active = False
483        self.t.join()
484
485
486    # request an update
487    def update (self):
488        self.update_flag = True
489
490    # fetch the screen, return None if no new screen exists yet
491    def get (self):
492
493        if not self.snapshot:
494            return None
495
496        # we have a snapshot - fetch it
497        with self.lock:
498            x = self.snapshot
499            self.snapshot = None
500            return x
501
502
503    def __handler (self):
504
505        while self.active:
506            if self.update_flag:
507                self.__redraw()
508
509            time.sleep(0.01)
510
511    # redraw the next screen
512    def __redraw (self):
513        buffer = StringIO()
514
515        self.redraw_cb(buffer)
516
517        with self.lock:
518            self.snapshot = buffer
519            self.update_flag = False
520
521# a policer class to make sure no too-fast redraws
522# occurs - it filters fast bursts of redraws
523class RedrawPolicer():
524    def __init__ (self, rate):
525        self.ts = 0
526        self.marked = False
527        self.rate = rate
528        self.force = False
529
530    def mark_for_redraw (self, force = False):
531        self.marked = True
532        if force:
533            self.force = True
534
535    def should_redraw (self):
536        dt = time.time() - self.ts
537        return self.force or (self.marked and (dt > self.rate))
538
539    def reset (self, restart = False):
540        self.ts = time.time()
541        self.marked = restart
542        self.force = False
543
544
545# shows a textual top style window
546class TrexTUI():
547
548    STATE_ACTIVE     = 0
549    STATE_LOST_CONT  = 1
550    STATE_RECONNECT  = 2
551    is_graph = False
552
553    MIN_ROWS = 50
554    MIN_COLS = 111
555
556
557    class ScreenSizeException(Exception):
558        def __init__ (self, cols, rows):
559            msg = "TUI requires console screen size of at least {0}x{1}, current is {2}x{3}".format(TrexTUI.MIN_COLS,
560                                                                                                    TrexTUI.MIN_ROWS,
561                                                                                                    cols,
562                                                                                                    rows)
563            super(TrexTUI.ScreenSizeException, self).__init__(msg)
564
565
566    def __init__ (self, stateless_client):
567        self.stateless_client = stateless_client
568
569        self.tui_global_lock = threading.Lock()
570        self.pm = TrexTUIPanelManager(self)
571        self.sb = ScreenBuffer(self.redraw_handler)
572
573    def redraw_handler (self, buffer):
574        # this is executed by the screen buffer - should be protected against TUI commands
575        with self.tui_global_lock:
576            self.pm.show(show_legend = self.async_keys.is_legend_mode(), buffer = buffer)
577
578    def clear_screen (self, lines = 50):
579        # reposition the cursor
580        sys.stdout.write("\x1b[0;0H")
581
582        # clear all lines
583        for i in range(lines):
584            sys.stdout.write("\x1b[0K")
585            if i < (lines - 1):
586                sys.stdout.write("\n")
587
588        # reposition the cursor
589        sys.stdout.write("\x1b[0;0H")
590
591
592
593    def show (self, client, save_console_history, show_log = False, locked = False):
594
595        rows, cols = os.popen('stty size', 'r').read().split()
596        if (int(rows) < TrexTUI.MIN_ROWS) or (int(cols) < TrexTUI.MIN_COLS):
597            raise self.ScreenSizeException(rows = rows, cols = cols)
598
599        with AsyncKeys(client, save_console_history, self.tui_global_lock, locked) as async_keys:
600            sys.stdout.write("\x1bc")
601            self.async_keys = async_keys
602            self.show_internal(show_log, locked)
603
604
605
606    def show_internal (self, show_log, locked):
607
608        self.pm.init(show_log, locked)
609
610        self.state = self.STATE_ACTIVE
611
612        # create print policers
613        self.full_redraw = RedrawPolicer(0.5)
614        self.keys_redraw = RedrawPolicer(0.05)
615        self.full_redraw.mark_for_redraw()
616
617
618        try:
619            self.sb.start()
620
621            while True:
622                # draw and handle user input
623                status = self.async_keys.tick(self.pm)
624
625                # prepare the next frame
626                self.prepare(status)
627                time.sleep(0.01)
628                self.draw_screen()
629
630                with self.tui_global_lock:
631                    self.handle_state_machine()
632
633        except TUIQuit:
634            print("\nExiting TUI...")
635
636        finally:
637            self.sb.stop()
638
639        print("")
640
641
642
643    # handle state machine
644    def handle_state_machine (self):
645       # regular state
646        if self.state == self.STATE_ACTIVE:
647            # if no connectivity - move to lost connecitivty
648            if not self.stateless_client.async_client.is_active():
649                self.stateless_client._invalidate_stats(self.pm.ports)
650                self.state = self.STATE_LOST_CONT
651
652
653        # lost connectivity
654        elif self.state == self.STATE_LOST_CONT:
655            # if the async is alive (might be zomibe, but alive) try to reconnect
656            if self.stateless_client.async_client.is_alive():
657                # move to state reconnect
658                self.state = self.STATE_RECONNECT
659
660
661        # restored connectivity - try to reconnect
662        elif self.state == self.STATE_RECONNECT:
663
664            try:
665                self.stateless_client.connect()
666                self.stateless_client.acquire()
667                self.state = self.STATE_ACTIVE
668            except STLError:
669                self.state = self.STATE_LOST_CONT
670
671
672    # logic before printing
673    def prepare (self, status):
674        if status == AsyncKeys.STATUS_REDRAW_ALL:
675            self.full_redraw.mark_for_redraw(force = True)
676
677        elif status == AsyncKeys.STATUS_REDRAW_KEYS:
678            self.keys_redraw.mark_for_redraw()
679
680        if self.full_redraw.should_redraw():
681            self.sb.update()
682            self.full_redraw.reset(restart = True)
683
684        return
685
686
687    # draw once
688    def draw_screen (self):
689
690        # check for screen buffer's new screen
691        x = self.sb.get()
692
693        # we have a new screen to draw
694        if x:
695            self.clear_screen()
696
697            self.async_keys.draw(x)
698            sys.stdout.write(x.getvalue())
699            sys.stdout.flush()
700
701        # maybe we need to redraw the keys
702        elif self.keys_redraw.should_redraw():
703            sys.stdout.write("\x1b[4A")
704            self.async_keys.draw(sys.stdout)
705            sys.stdout.flush()
706
707            # reset the policer for next time
708            self.keys_redraw.reset()
709
710
711
712
713    def get_state (self):
714        return self.state
715
716
717class TokenParser(object):
718    def __init__ (self, seq):
719        self.buffer = list(seq)
720
721    def pop (self):
722        return self.buffer.pop(0)
723
724
725    def peek (self):
726        if not self.buffer:
727            return None
728        return self.buffer[0]
729
730    def next_token (self):
731        if not self.peek():
732            return None
733
734        token = self.pop()
735
736        # special chars
737        if token == '\x1b' and self.peek() == '[':
738            token += self.pop()
739            if self.peek():
740                token += self.pop()
741
742        return token
743
744    def parse (self):
745        tokens = []
746
747        while True:
748            token = self.next_token()
749            if token == None:
750                break
751            tokens.append(token)
752
753        return tokens
754
755
756# handles async IO
757class AsyncKeys:
758
759    MODE_LEGEND  = 1
760    MODE_CONSOLE = 2
761
762    STATUS_NONE        = 0
763    STATUS_REDRAW_KEYS = 1
764    STATUS_REDRAW_ALL  = 2
765
766    def __init__ (self, client, save_console_history, tui_global_lock, locked = False):
767        self.tui_global_lock = tui_global_lock
768
769        self.engine_console = AsyncKeysEngineConsole(self, client, save_console_history)
770        self.engine_legend  = AsyncKeysEngineLegend(self)
771        self.locked = locked
772
773        if locked:
774            self.engine = self.engine_legend
775            self.locked = True
776        else:
777            self.engine = self.engine_console
778            self.locked = False
779
780    def __enter__ (self):
781        # init termios
782        self.old_settings = termios.tcgetattr(sys.stdin)
783        new_settings = termios.tcgetattr(sys.stdin)
784        new_settings[3] = new_settings[3] & ~(termios.ECHO | termios.ICANON) # lflags
785        new_settings[6][termios.VMIN] = 0  # cc
786        new_settings[6][termios.VTIME] = 0 # cc
787        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
788
789        # huge buffer - no print without flush
790        sys.stdout = open('/dev/stdout', 'w', TrexTUI.MIN_COLS * TrexTUI.MIN_COLS * 2)
791        return self
792
793    def __exit__ (self, type, value, traceback):
794        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings)
795
796        # restore sys.stdout
797        sys.stdout.close()
798        sys.stdout = sys.__stdout__
799
800
801    def is_legend_mode (self):
802        return self.engine.get_type() == AsyncKeys.MODE_LEGEND
803
804    def is_console_mode (self):
805        return self.engine.get_type == AsyncKeys.MODE_CONSOLE
806
807    def switch (self):
808        if self.is_legend_mode():
809            self.engine = self.engine_console
810        else:
811            self.engine = self.engine_legend
812
813
814    # parse the buffer to manageble tokens
815    def parse_tokens (self, seq):
816
817        tokens = []
818        chars = list(seq)
819
820        while chars:
821            token = chars.pop(0)
822
823            # special chars
824            if token == '\x1b' and chars[0] == '[':
825                token += chars.pop(0)
826                token += chars.pop(0)
827
828            tokens.append(token)
829
830        return tokens
831
832    def handle_token (self, token, pm):
833        # ESC for switch
834        if token == '\x1b':
835            if not self.locked:
836                self.switch()
837            return self.STATUS_REDRAW_ALL
838
839        # EOF (ctrl + D)
840        if token == '\x04':
841            raise TUIQuit()
842
843        # pass tick to engine
844        return self.engine.tick(token, pm)
845
846
847    def tick (self, pm):
848        rc = self.STATUS_NONE
849
850        # fetch the stdin buffer
851        seq = os.read(sys.stdin.fileno(), 1024).decode()
852        if not seq:
853            return self.STATUS_NONE
854
855        # parse all the tokens from the buffer
856        tokens = TokenParser(seq).parse()
857
858        # process them
859        for token in tokens:
860            token_rc = self.handle_token(token, pm)
861            rc = max(rc, token_rc)
862
863
864        return rc
865
866
867    def draw (self, buffer):
868        self.engine.draw(buffer)
869
870
871
872# Legend engine
873class AsyncKeysEngineLegend:
874    def __init__ (self, async):
875        self.async = async
876
877    def get_type (self):
878        return self.async.MODE_LEGEND
879
880    def tick (self, seq, pm):
881
882        if seq == 'q':
883            raise TUIQuit()
884
885        # ignore escapes
886        if len(seq) > 1:
887            return AsyncKeys.STATUS_NONE
888
889        rc = pm.handle_key(seq)
890        return AsyncKeys.STATUS_REDRAW_ALL if rc else AsyncKeys.STATUS_NONE
891
892    def draw (self, buffer):
893        pass
894
895
896# console engine
897class AsyncKeysEngineConsole:
898    def __init__ (self, async, client, save_console_history):
899        self.async = async
900        self.lines = deque(maxlen = 100)
901
902        self.generate_prompt       = client.generate_prompt
903        self.save_console_history  = save_console_history
904
905        self.ac = {'start'        : client.start_line,
906                   'stop'         : client.stop_line,
907                   'pause'        : client.pause_line,
908                   'clear'        : client.clear_stats_line,
909                   'push'         : client.push_line,
910                   'resume'       : client.resume_line,
911                   'reset'        : client.reset_line,
912                   'update'       : client.update_line,
913                   'connect'      : client.connect_line,
914                   'disconnect'   : client.disconnect_line,
915                   'acquire'      : client.acquire_line,
916                   'release'      : client.release_line,
917                   'quit'         : self.action_quit,
918                   'q'            : self.action_quit,
919                   'exit'         : self.action_quit,
920                   'help'         : self.action_help,
921                   '?'            : self.action_help}
922
923        # fetch readline history and add relevants
924        for i in range(0, readline.get_current_history_length()):
925            cmd = readline.get_history_item(i)
926            if cmd and cmd.split()[0] in self.ac:
927                self.lines.appendleft(CmdLine(cmd))
928
929        # new line
930        self.lines.appendleft(CmdLine(''))
931        self.line_index = 0
932        self.last_status = ''
933
934    def action_quit (self, _):
935        raise TUIQuit()
936
937    def action_help (self, _):
938        return ' '.join([format_text(cmd, 'bold') for cmd in self.ac.keys()])
939
940    def get_type (self):
941        return self.async.MODE_CONSOLE
942
943
944    def handle_escape_char (self, seq):
945        # up
946        if seq == '\x1b[A':
947            self.line_index = min(self.line_index + 1, len(self.lines) - 1)
948
949        # down
950        elif seq == '\x1b[B':
951            self.line_index = max(self.line_index - 1, 0)
952
953        # left
954        elif seq == '\x1b[D':
955            self.lines[self.line_index].go_left()
956
957        # right
958        elif seq == '\x1b[C':
959            self.lines[self.line_index].go_right()
960
961        # del
962        elif seq == '\x1b[3~':
963            self.lines[self.line_index].del_key()
964
965        # home
966        elif seq == '\x1b[H':
967            self.lines[self.line_index].home_key()
968
969        # end
970        elif seq == '\x1b[F':
971            self.lines[self.line_index].end_key()
972            return True
973
974        # unknown key
975        else:
976            return AsyncKeys.STATUS_NONE
977
978        return AsyncKeys.STATUS_REDRAW_KEYS
979
980
981    def tick (self, seq, _):
982
983        # handle escape chars
984        if len(seq) > 1:
985            return self.handle_escape_char(seq)
986
987        # handle each char
988        for ch in seq:
989            return self.handle_single_key(ch)
990
991
992
993    def handle_single_key (self, ch):
994        # newline
995        if ch == '\n':
996            self.handle_cmd()
997
998        # backspace
999        elif ch == '\x7f':
1000            self.lines[self.line_index].backspace()
1001
1002        # TAB
1003        elif ch == '\t':
1004            tokens = self.lines[self.line_index].get().split()
1005            if not tokens:
1006                return
1007
1008            if len(tokens) == 1:
1009                self.handle_tab_names(tokens[0])
1010            else:
1011                self.handle_tab_files(tokens)
1012
1013
1014        # simple char
1015        else:
1016            self.lines[self.line_index] += ch
1017
1018        return AsyncKeys.STATUS_REDRAW_KEYS
1019
1020
1021    # handle TAB key for completing function names
1022    def handle_tab_names (self, cur):
1023        matching_cmds = [x for x in self.ac if x.startswith(cur)]
1024
1025        common = os.path.commonprefix([x for x in self.ac if x.startswith(cur)])
1026        if common:
1027            if len(matching_cmds) == 1:
1028                self.lines[self.line_index].set(common + ' ')
1029                self.last_status = ''
1030            else:
1031                self.lines[self.line_index].set(common)
1032                self.last_status = 'ambigious: '+ ' '.join([format_text(cmd, 'bold') for cmd in matching_cmds])
1033
1034
1035    # handle TAB for completing filenames
1036    def handle_tab_files (self, tokens):
1037
1038        # only commands with files
1039        if tokens[0] not in {'start', 'push'}:
1040            return
1041
1042        # '-f' with no paramters - no partial and use current dir
1043        if tokens[-1] == '-f':
1044            partial = ''
1045            d = '.'
1046
1047        # got a partial path
1048        elif tokens[-2] == '-f':
1049            partial = tokens.pop()
1050
1051            # check for dirs
1052            dirname, basename = os.path.dirname(partial), os.path.basename(partial)
1053            if os.path.isdir(dirname):
1054                d = dirname
1055                partial = basename
1056            else:
1057                d = '.'
1058        else:
1059            return
1060
1061        # fetch all dirs and files matching wildcard
1062        files = []
1063        for x in os.listdir(d):
1064            if os.path.isdir(os.path.join(d, x)):
1065                files.append(x + '/')
1066            elif x.endswith( ('.py', 'yaml', 'pcap', 'cap', 'erf') ):
1067                files.append(x)
1068
1069        # dir might not have the files
1070        if not files:
1071            self.last_status = format_text('no loadble files under path', 'bold')
1072            return
1073
1074
1075        # find all the matching files
1076        matching_files = [x for x in files if x.startswith(partial)] if partial else files
1077
1078        # do we have a longer common than partial ?
1079        common = os.path.commonprefix([x for x in files if x.startswith(partial)])
1080        if not common:
1081            common = partial
1082
1083        tokens.append(os.path.join(d, common) if d is not '.' else common)
1084
1085        # reforge the line
1086        newline = ' '.join(tokens)
1087
1088        if len(matching_files) == 1:
1089            if os.path.isfile(tokens[-1]):
1090                newline += ' '
1091
1092            self.lines[self.line_index].set(newline)
1093            self.last_status = ''
1094        else:
1095            self.lines[self.line_index].set(newline)
1096            self.last_status = '    '.join([format_text(f, 'bold') for f in matching_files[:5]])
1097            if len(matching_files) > 5:
1098                self.last_status += ' ... [{0} more matches]'.format(len(matching_files) - 5)
1099
1100
1101
1102    def split_cmd (self, cmd):
1103        s = cmd.split(' ', 1)
1104        op = s[0]
1105        param = s[1] if len(s) == 2 else ''
1106        return op, param
1107
1108
1109    def handle_cmd (self):
1110
1111        cmd = self.lines[self.line_index].get().strip()
1112        if not cmd:
1113            return
1114
1115        op, param = self.split_cmd(cmd)
1116
1117        func = self.ac.get(op)
1118        if func:
1119            with self.async.tui_global_lock:
1120                func_rc = func(param)
1121
1122        # take out the empty line
1123        empty_line = self.lines.popleft()
1124        assert(empty_line.ro_line == '')
1125
1126        if not self.lines or self.lines[0].ro_line != cmd:
1127            self.lines.appendleft(CmdLine(cmd))
1128
1129        # back in
1130        self.lines.appendleft(empty_line)
1131        self.line_index = 0
1132        readline.add_history(cmd)
1133        self.save_console_history()
1134
1135        # back to readonly
1136        for line in self.lines:
1137            line.invalidate()
1138
1139        assert(self.lines[0].modified == False)
1140        color = None
1141        if not func:
1142            self.last_status = "unknown command: '{0}'".format(format_text(cmd.split()[0], 'bold'))
1143        else:
1144            # internal commands
1145            if isinstance(func_rc, str):
1146                self.last_status = func_rc
1147
1148            # RC response
1149            else:
1150                # success
1151                if func_rc:
1152                    self.last_status = format_text("[OK]", 'green')
1153                # errors
1154                else:
1155                    err_msgs = ascii_split(str(func_rc))
1156                    self.last_status = format_text(err_msgs[0], 'red')
1157                    if len(err_msgs) > 1:
1158                        self.last_status += " [{0} more errors messages]".format(len(err_msgs) - 1)
1159                    color = 'red'
1160
1161
1162
1163        # trim too long lines
1164        if ansi_len(self.last_status) > TrexTUI.MIN_COLS:
1165            self.last_status = format_text(self.last_status[:TrexTUI.MIN_COLS] + "...", color, 'bold')
1166
1167
1168    def draw (self, buffer):
1169        buffer.write("\nPress 'ESC' for navigation panel...\n")
1170        buffer.write("status: \x1b[0K{0}\n".format(self.last_status))
1171        buffer.write("\n{0}\x1b[0K".format(self.generate_prompt(prefix = 'tui')))
1172        self.lines[self.line_index].draw(buffer)
1173
1174
1175# a readline alike command line - can be modified during edit
1176class CmdLine(object):
1177    def __init__ (self, line):
1178        self.ro_line      = line
1179        self.w_line       = None
1180        self.modified     = False
1181        self.cursor_index = len(line)
1182
1183    def get (self):
1184        if self.modified:
1185            return self.w_line
1186        else:
1187            return self.ro_line
1188
1189    def set (self, line, cursor_pos = None):
1190        self.w_line = line
1191        self.modified = True
1192
1193        if cursor_pos is None:
1194            self.cursor_index = len(self.w_line)
1195        else:
1196            self.cursor_index = cursor_pos
1197
1198
1199    def __add__ (self, other):
1200        assert(0)
1201
1202
1203    def __str__ (self):
1204        return self.get()
1205
1206
1207    def __iadd__ (self, other):
1208
1209        self.set(self.get()[:self.cursor_index] + other + self.get()[self.cursor_index:],
1210                 cursor_pos = self.cursor_index + len(other))
1211
1212        return self
1213
1214
1215    def backspace (self):
1216        if self.cursor_index == 0:
1217            return
1218
1219        self.set(self.get()[:self.cursor_index - 1] + self.get()[self.cursor_index:],
1220                 self.cursor_index - 1)
1221
1222
1223    def del_key (self):
1224        if self.cursor_index == len(self.get()):
1225            return
1226
1227        self.set(self.get()[:self.cursor_index] + self.get()[self.cursor_index + 1:],
1228                 self.cursor_index)
1229
1230    def home_key (self):
1231        self.cursor_index = 0
1232
1233    def end_key (self):
1234        self.cursor_index = len(self.get())
1235
1236    def invalidate (self):
1237        self.modified = False
1238        self.w_line = None
1239        self.cursor_index = len(self.ro_line)
1240
1241    def go_left (self):
1242        self.cursor_index = max(0, self.cursor_index - 1)
1243
1244    def go_right (self):
1245        self.cursor_index = min(len(self.get()), self.cursor_index + 1)
1246
1247    def draw (self, buffer):
1248        buffer.write(self.get())
1249        buffer.write('\b' * (len(self.get()) - self.cursor_index))
1250
1251