trex_console.py revision 17833369
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4"""
5Dan Klein, Itay Marom
6Cisco Systems, Inc.
7
8Copyright (c) 2015-2015 Cisco Systems, Inc.
9Licensed under the Apache License, Version 2.0 (the "License");
10you may not use this file except in compliance with the License.
11You may obtain a copy of the License at
12    http://www.apache.org/licenses/LICENSE-2.0
13Unless required by applicable law or agreed to in writing, software
14distributed under the License is distributed on an "AS IS" BASIS,
15WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16See the License for the specific language governing permissions and
17limitations under the License.
18"""
19from __future__ import print_function
20
21import subprocess
22import cmd
23import json
24import ast
25import argparse
26import random
27import readline
28import string
29import os
30import sys
31import tty, termios
32
33try:
34    import stl_path
35except:
36    from . import stl_path
37from trex_stl_lib.api import *
38
39from trex_stl_lib.utils.text_opts import *
40from trex_stl_lib.utils.common import user_input, get_current_user
41from trex_stl_lib.utils import parsing_opts
42
43try:
44    import trex_tui
45except:
46    from . import trex_tui
47
48from functools import wraps
49
50__version__ = "2.0"
51
52# console custom logger
53class ConsoleLogger(LoggerApi):
54    def __init__ (self):
55        self.prompt_redraw = None
56
57    def write (self, msg, newline = True):
58        if newline:
59            print(msg)
60        else:
61            print(msg, end=' ')
62
63    def flush (self):
64        sys.stdout.flush()
65
66    # override this for the prompt fix
67    def async_log (self, msg, level = LoggerApi.VERBOSE_REGULAR, newline = True):
68        self.log(msg, level, newline)
69        if ( (self.level >= LoggerApi.VERBOSE_REGULAR) and self.prompt_redraw ):
70            self.prompt_redraw()
71            self.flush()
72
73
74def set_window_always_on_top (title):
75    # we need the GDK module, if not available - ignroe this command
76    try:
77        if sys.version_info < (3,0):
78            from gtk import gdk
79        else:
80            #from gi.repository import Gdk as gdk
81            return
82
83    except ImportError:
84        return
85
86    # search the window and set it as above
87    root = gdk.get_default_root_window()
88
89    for id in root.property_get('_NET_CLIENT_LIST')[2]:
90        w = gdk.window_foreign_new(id)
91        if w:
92            name = w.property_get('WM_NAME')[2]
93            if name == title:
94                w.set_keep_above(True)
95                gdk.window_process_all_updates()
96                break
97
98
99class TRexGeneralCmd(cmd.Cmd):
100    def __init__(self):
101        cmd.Cmd.__init__(self)
102        # configure history behaviour
103        self._history_file_dir = "/tmp/trex/console/"
104        self._history_file = self.get_history_file_full_path()
105        readline.set_history_length(100)
106        # load history, if any
107        self.load_console_history()
108
109
110    def get_console_identifier(self):
111        return self.__class__.__name__
112
113    def get_history_file_full_path(self):
114        return "{dir}{filename}.hist".format(dir=self._history_file_dir,
115                                             filename=self.get_console_identifier())
116
117    def load_console_history(self):
118        if os.path.exists(self._history_file):
119            readline.read_history_file(self._history_file)
120        return
121
122    def save_console_history(self):
123        if not os.path.exists(self._history_file_dir):
124            # make the directory available for every user
125            try:
126                original_umask = os.umask(0)
127                os.makedirs(self._history_file_dir, mode = 0o777)
128            finally:
129                os.umask(original_umask)
130
131
132        # os.mknod(self._history_file)
133        readline.write_history_file(self._history_file)
134        return
135
136    def print_history (self):
137
138        length = readline.get_current_history_length()
139
140        for i in range(1, length + 1):
141            cmd = readline.get_history_item(i)
142            print("{:<5}   {:}".format(i, cmd))
143
144    def get_history_item (self, index):
145        length = readline.get_current_history_length()
146        if index > length:
147            print(format_text("please select an index between {0} and {1}".format(0, length)))
148            return None
149
150        return readline.get_history_item(index)
151
152
153    def emptyline(self):
154        """Called when an empty line is entered in response to the prompt.
155
156        This overriding is such that when empty line is passed, **nothing happens**.
157        """
158        return
159
160    def completenames(self, text, *ignored):
161        """
162        This overriding is such that a space is added to name completion.
163        """
164        dotext = 'do_'+text
165        return [a[3:]+' ' for a in self.get_names() if a.startswith(dotext)]
166
167
168#
169# main console object
170class TRexConsole(TRexGeneralCmd):
171    """Trex Console"""
172
173    def __init__(self, stateless_client, verbose = False):
174
175        self.stateless_client = stateless_client
176
177        TRexGeneralCmd.__init__(self)
178
179        self.tui = trex_tui.TrexTUI(stateless_client)
180        self.terminal = None
181
182        self.verbose = verbose
183
184        self.intro  = "\n-=TRex Console v{ver}=-\n".format(ver=__version__)
185        self.intro += "\nType 'help' or '?' for supported actions\n"
186
187        self.postcmd(False, "")
188
189
190    ################### internal section ########################
191
192    def prompt_redraw (self):
193        self.postcmd(False, "")
194        sys.stdout.write("\n" + self.prompt + readline.get_line_buffer())
195        sys.stdout.flush()
196
197
198    def verify_connected(f):
199        @wraps(f)
200        def wrap(*args):
201            inst = args[0]
202            func_name = f.__name__
203            if func_name.startswith("do_"):
204                func_name = func_name[3:]
205
206            if not inst.stateless_client.is_connected():
207                print(format_text("\n'{0}' cannot be executed on offline mode\n".format(func_name), 'bold'))
208                return
209
210            ret = f(*args)
211            return ret
212
213        return wrap
214
215
216    def get_console_identifier(self):
217        return "{context}_{server}".format(context=get_current_user(),
218                                           server=self.stateless_client.get_connection_info()['server'])
219
220    def register_main_console_methods(self):
221        main_names = set(self.trex_console.get_names()).difference(set(dir(self.__class__)))
222        for name in main_names:
223            for prefix in 'do_', 'help_', 'complete_':
224                if name.startswith(prefix):
225                    self.__dict__[name] = getattr(self.trex_console, name)
226
227    def precmd(self, line):
228        # before doing anything, save history snapshot of the console
229        # this is done before executing the command in case of ungraceful application exit
230        self.save_console_history()
231
232        lines = line.split(';')
233        try:
234            for line in lines:
235                stop = self.onecmd(line)
236                stop = self.postcmd(stop, line)
237                if stop:
238                    return "quit"
239
240            return ""
241        except STLError as e:
242            print(e)
243            return ''
244
245
246    def postcmd(self, stop, line):
247        self.prompt = self.stateless_client.generate_prompt(prefix = 'trex')
248        return stop
249
250
251    def default(self, line):
252        print("'{0}' is an unrecognized command. type 'help' or '?' for a list\n".format(line))
253
254    @staticmethod
255    def tree_autocomplete(text):
256        dir = os.path.dirname(text)
257        if dir:
258            path = dir
259        else:
260            path = "."
261
262
263        start_string = os.path.basename(text)
264
265        targets = []
266
267        for x in os.listdir(path):
268            if x.startswith(start_string):
269                y = os.path.join(path, x)
270                if os.path.isfile(y):
271                    targets.append(x + ' ')
272                elif os.path.isdir(y):
273                    targets.append(x + '/')
274
275        return targets
276
277
278    ####################### shell commands #######################
279    @verify_connected
280    def do_ping (self, line):
281        '''Ping the server\n'''
282        self.stateless_client.ping_line(line)
283
284
285    @verify_connected
286    def do_shutdown (self, line):
287        '''Sends the server a shutdown request\n'''
288        self.stateless_client.shutdown_line(line)
289
290    # set verbose on / off
291    def do_verbose(self, line):
292        '''Shows or set verbose mode\n'''
293        if line == "":
294            print("\nverbose is " + ("on\n" if self.verbose else "off\n"))
295
296        elif line == "on":
297            self.verbose = True
298            self.stateless_client.set_verbose("high")
299            print(format_text("\nverbose set to on\n", 'green', 'bold'))
300
301        elif line == "off":
302            self.verbose = False
303            self.stateless_client.set_verbose("normal")
304            print(format_text("\nverbose set to off\n", 'green', 'bold'))
305
306        else:
307            print(format_text("\nplease specify 'on' or 'off'\n", 'bold'))
308
309    # show history
310    def help_history (self):
311        self.do_history("-h")
312
313    def do_shell (self, line):
314        self.do_history(line)
315
316    def do_push (self, line):
317        '''Push a local PCAP file\n'''
318        self.stateless_client.push_line(line)
319
320    def help_push (self):
321        self.do_push("-h")
322
323    @verify_connected
324    def do_portattr (self, line):
325        '''Change/show port(s) attributes\n'''
326        self.stateless_client.set_port_attr_line(line)
327
328    def help_portattr (self):
329        self.do_portattr("-h")
330
331    def do_set_rx_sniffer (self, line):
332        '''Sets a port sniffer on RX channel as PCAP recorder'''
333        self.stateless_client.set_rx_sniffer_line(line)
334
335    def help_sniffer (self):
336        self.do_set_rx_sniffer("-h")
337
338    def do_resolve (self, line):
339        '''Resolve ARP for ports'''
340        self.stateless_client.resolve_line(line)
341
342    def help_sniffer (self):
343        self.do_resolve("-h")
344
345    @verify_connected
346    def do_map (self, line):
347        '''Maps ports topology\n'''
348        ports = self.stateless_client.get_acquired_ports()
349        if not ports:
350            print("No ports acquired\n")
351            return
352
353        with self.stateless_client.logger.supress():
354            table = stl_map_ports(self.stateless_client, ports = ports)
355
356
357        print(format_text('\nAcquired ports topology:\n', 'bold', 'underline'))
358
359        # bi-dir ports
360        print(format_text('Bi-directional ports:\n','underline'))
361        for port_a, port_b in table['bi']:
362            print("port {0} <--> port {1}".format(port_a, port_b))
363
364        print("")
365
366        # unknown ports
367        print(format_text('Mapping unknown:\n','underline'))
368        for port in table['unknown']:
369            print("port {0}".format(port))
370        print("")
371
372
373
374
375    def do_history (self, line):
376        '''Manage the command history\n'''
377
378        item = parsing_opts.ArgumentPack(['item'],
379                                         {"nargs": '?',
380                                          'metavar': 'item',
381                                          'type': parsing_opts.check_negative,
382                                          'help': "an history item index",
383                                          'default': 0})
384
385        parser = parsing_opts.gen_parser(self.stateless_client,
386                                         "history",
387                                         self.do_history.__doc__,
388                                         item)
389
390        opts = parser.parse_args(line.split())
391        if opts is None:
392            return
393
394        if opts.item == 0:
395            self.print_history()
396        else:
397            cmd = self.get_history_item(opts.item)
398            if cmd == None:
399                return
400
401            print("Executing '{0}'".format(cmd))
402
403            return self.onecmd(cmd)
404
405
406
407    ############### connect
408    def do_connect (self, line):
409        '''Connects to the server and acquire ports\n'''
410
411        self.stateless_client.connect_line(line)
412
413    def help_connect (self):
414        self.do_connect("-h")
415
416    def do_disconnect (self, line):
417        '''Disconnect from the server\n'''
418
419        self.stateless_client.disconnect_line(line)
420
421
422    @verify_connected
423    def do_acquire (self, line):
424        '''Acquire ports\n'''
425
426        self.stateless_client.acquire_line(line)
427
428
429    @verify_connected
430    def do_release (self, line):
431        '''Release ports\n'''
432        self.stateless_client.release_line(line)
433
434    def do_reacquire (self, line):
435        '''reacquire all the ports under your logged user name'''
436        self.stateless_client.reacquire_line(line)
437
438    def help_acquire (self):
439        self.do_acquire("-h")
440
441    def help_release (self):
442        self.do_release("-h")
443
444    def help_reacquire (self):
445        self.do_reacquire("-h")
446
447    ############### start
448
449    def complete_start(self, text, line, begidx, endidx):
450        s = line.split()
451        l = len(s)
452
453        file_flags = parsing_opts.get_flags(parsing_opts.FILE_PATH)
454
455        if (l > 1) and (s[l - 1] in file_flags):
456            return TRexConsole.tree_autocomplete("")
457
458        if (l > 2) and (s[l - 2] in file_flags):
459            return TRexConsole.tree_autocomplete(s[l - 1])
460
461    complete_push = complete_start
462
463    @verify_connected
464    def do_start(self, line):
465        '''Start selected traffic in specified port(s) on TRex\n'''
466
467        self.stateless_client.start_line(line)
468
469
470
471    def help_start(self):
472        self.do_start("-h")
473
474    ############# stop
475    @verify_connected
476    def do_stop(self, line):
477        '''stops port(s) transmitting traffic\n'''
478
479        self.stateless_client.stop_line(line)
480
481    def help_stop(self):
482        self.do_stop("-h")
483
484    ############# update
485    @verify_connected
486    def do_update(self, line):
487        '''update speed of port(s)currently transmitting traffic\n'''
488
489        self.stateless_client.update_line(line)
490
491    def help_update (self):
492        self.do_update("-h")
493
494    ############# pause
495    @verify_connected
496    def do_pause(self, line):
497        '''pause port(s) transmitting traffic\n'''
498
499        self.stateless_client.pause_line(line)
500
501    ############# resume
502    @verify_connected
503    def do_resume(self, line):
504        '''resume port(s) transmitting traffic\n'''
505
506        self.stateless_client.resume_line(line)
507
508
509
510    ########## reset
511    @verify_connected
512    def do_reset (self, line):
513        '''force stop all ports\n'''
514        self.stateless_client.reset_line(line)
515
516
517    ######### validate
518    @verify_connected
519    def do_validate (self, line):
520        '''validates port(s) stream configuration\n'''
521
522        self.stateless_client.validate_line(line)
523
524
525    @verify_connected
526    def do_stats(self, line):
527        '''Fetch statistics from TRex server by port\n'''
528        self.stateless_client.show_stats_line(line)
529
530
531    def help_stats(self):
532        self.do_stats("-h")
533
534    @verify_connected
535    def do_streams(self, line):
536        '''Fetch statistics from TRex server by port\n'''
537        self.stateless_client.show_streams_line(line)
538
539
540    def help_streams(self):
541        self.do_streams("-h")
542
543    @verify_connected
544    def do_clear(self, line):
545        '''Clear cached local statistics\n'''
546        self.stateless_client.clear_stats_line(line)
547
548
549    def help_clear(self):
550        self.do_clear("-h")
551
552
553    def help_events (self):
554        self.do_events("-h")
555
556    def do_events (self, line):
557        '''shows events recieved from server\n'''
558        self.stateless_client.get_events_line(line)
559
560
561    def complete_profile(self, text, line, begidx, endidx):
562        return self.complete_start(text,line, begidx, endidx)
563
564    def do_profile (self, line):
565        '''shows information about a profile'''
566        self.stateless_client.show_profile_line(line)
567
568    # tui
569    @verify_connected
570    def do_tui (self, line):
571        '''Shows a graphical console\n'''
572        parser = parsing_opts.gen_parser(self.stateless_client,
573                                         "tui",
574                                         self.do_tui.__doc__,
575                                         parsing_opts.XTERM,
576                                         parsing_opts.LOCKED)
577
578        opts = parser.parse_args(line.split())
579
580        if not opts:
581            return opts
582        if opts.xterm:
583            if not os.path.exists('/usr/bin/xterm'):
584                print(format_text("XTERM does not exists on this machine", 'bold'))
585                return
586
587            info = self.stateless_client.get_connection_info()
588
589            exe = './trex-console --top -t -q -s {0} -p {1} --async_port {2}'.format(info['server'], info['sync_port'], info['async_port'])
590            cmd = ['/usr/bin/xterm', '-geometry', '{0}x{1}'.format(self.tui.MIN_COLS, self.tui.MIN_ROWS), '-sl', '0', '-title', 'trex_tui', '-e', exe]
591
592            # detach child
593            self.terminal = subprocess.Popen(cmd, preexec_fn = os.setpgrp)
594
595            return
596
597
598        try:
599            with self.stateless_client.logger.supress():
600                self.tui.show(self.stateless_client, self.save_console_history, locked = opts.locked)
601
602        except self.tui.ScreenSizeException as e:
603            print(format_text(str(e) + "\n", 'bold'))
604
605
606    def help_tui (self):
607        do_tui("-h")
608
609
610    # quit function
611    def do_quit(self, line):
612        '''Exit the client\n'''
613        return True
614
615
616    def do_help (self, line):
617         '''Shows This Help Screen\n'''
618         if line:
619             try:
620                 func = getattr(self, 'help_' + line)
621             except AttributeError:
622                 try:
623                     doc = getattr(self, 'do_' + line).__doc__
624                     if doc:
625                         self.stdout.write("%s\n"%str(doc))
626                         return
627                 except AttributeError:
628                     pass
629                 self.stdout.write("%s\n"%str(self.nohelp % (line,)))
630                 return
631             func()
632             return
633
634         print("\nSupported Console Commands:")
635         print("----------------------------\n")
636
637         cmds =  [x[3:] for x in self.get_names() if x.startswith("do_")]
638         hidden = ['EOF', 'q', 'exit', 'h', 'shell']
639         for cmd in cmds:
640             if cmd in hidden:
641                 continue
642
643             try:
644                 doc = getattr(self, 'do_' + cmd).__doc__
645                 if doc:
646                     help = str(doc)
647                 else:
648                     help = "*** Undocumented Function ***\n"
649             except AttributeError:
650                 help = "*** Undocumented Function ***\n"
651
652             l=help.splitlines()
653             print("{:<30} {:<30}".format(cmd + " - ",l[0] ))
654
655    # a custorm cmdloop wrapper
656    def start(self):
657        while True:
658            try:
659                self.cmdloop()
660                break
661            except KeyboardInterrupt as e:
662                if not readline.get_line_buffer():
663                    raise KeyboardInterrupt
664                else:
665                    print("")
666                    self.intro = None
667                    continue
668
669        if self.terminal:
670            self.terminal.kill()
671
672    # aliases
673    do_exit = do_EOF = do_q = do_quit
674    do_h = do_history
675
676
677# run a script of commands
678def run_script_file (self, filename, stateless_client):
679
680    self.logger.log(format_text("\nRunning script file '{0}'...".format(filename), 'bold'))
681
682    with open(filename) as f:
683        script_lines = f.readlines()
684
685    cmd_table = {}
686
687    # register all the commands
688    cmd_table['start'] = stateless_client.start_line
689    cmd_table['stop']  = stateless_client.stop_line
690    cmd_table['reset'] = stateless_client.reset_line
691
692    for index, line in enumerate(script_lines, start = 1):
693        line = line.strip()
694        if line == "":
695            continue
696        if line.startswith("#"):
697            continue
698
699        sp = line.split(' ', 1)
700        cmd = sp[0]
701        if len(sp) == 2:
702            args = sp[1]
703        else:
704            args = ""
705
706        stateless_client.logger.log(format_text("Executing line {0} : '{1}'\n".format(index, line)))
707
708        if not cmd in cmd_table:
709            print("\n*** Error at line {0} : '{1}'\n".format(index, line))
710            stateless_client.logger.log(format_text("unknown command '{0}'\n".format(cmd), 'bold'))
711            return False
712
713        cmd_table[cmd](args)
714
715    stateless_client.logger.log(format_text("\n[Done]", 'bold'))
716
717    return True
718
719
720#
721def is_valid_file(filename):
722    if not os.path.isfile(filename):
723        raise argparse.ArgumentTypeError("The file '%s' does not exist" % filename)
724
725    return filename
726
727
728
729def setParserOptions():
730    parser = argparse.ArgumentParser(prog="trex_console.py")
731
732    parser.add_argument("-s", "--server", help = "TRex Server [default is localhost]",
733                        default = "localhost",
734                        type = str)
735
736    parser.add_argument("-p", "--port", help = "TRex Server Port  [default is 4501]\n",
737                        default = 4501,
738                        type = int)
739
740    parser.add_argument("--async_port", help = "TRex ASync Publisher Port [default is 4500]\n",
741                        default = 4500,
742                        dest='pub',
743                        type = int)
744
745    parser.add_argument("-u", "--user", help = "User Name  [default is currently logged in user]\n",
746                        default = get_current_user(),
747                        type = str)
748
749    parser.add_argument("-v", "--verbose", dest="verbose",
750                        action="store_true", help="Switch ON verbose option. Default is: OFF.",
751                        default = False)
752
753
754    group = parser.add_mutually_exclusive_group()
755
756    group.add_argument("-a", "--acquire", dest="acquire",
757                       nargs = '+',
758                       type = int,
759                       help="Acquire ports on connect. default is all available ports",
760                       default = None)
761
762    group.add_argument("-r", "--readonly", dest="readonly",
763                       action="store_true",
764                       help="Starts console in a read only mode",
765                       default = False)
766
767
768    parser.add_argument("-f", "--force", dest="force",
769                        action="store_true",
770                        help="Force acquire the requested ports",
771                        default = False)
772
773    parser.add_argument("--batch", dest="batch",
774                        nargs = 1,
775                        type = is_valid_file,
776                        help = "Run the console in a batch mode with file",
777                        default = None)
778
779    parser.add_argument("-t", "--tui", dest="tui",
780                        action="store_true", help="Starts with TUI mode",
781                        default = False)
782
783    parser.add_argument("-x", "--xtui", dest="xtui",
784                        action="store_true", help="Starts with XTERM TUI mode",
785                        default = False)
786
787    parser.add_argument("--top", dest="top",
788                        action="store_true", help="Set the window as always on top",
789                        default = False)
790
791    parser.add_argument("-q", "--quiet", dest="quiet",
792                        action="store_true", help="Starts with all outputs suppressed",
793                        default = False)
794
795    return parser
796
797# a simple info printed on log on
798def show_intro (logger, c):
799    x   = c.get_server_system_info()
800    ver = c.get_server_version().get('version', 'N/A')
801
802    # find out which NICs the server has
803    port_types = {}
804    for port in x['ports']:
805        if 'supp_speeds' in port:
806            speed = max(port['supp_speeds']) // 1000
807        else:
808            speed = port['speed']
809        key = (speed, port.get('description', port['driver']))
810        if key not in port_types:
811            port_types[key] = 0
812        port_types[key] += 1
813
814    port_line = ''
815    for k, v in port_types.items():
816        port_line += "{0} x {1}Gbps @ {2}\t".format(v, k[0], k[1])
817
818    logger.log(format_text("\nServer Info:\n", 'underline'))
819    logger.log("Server version:   {:>}".format(format_text(ver, 'bold')))
820    logger.log("Server CPU:       {:>}".format(format_text("{:>} x {:>}".format(x.get('dp_core_count'), x.get('core_type')), 'bold')))
821    logger.log("Ports count:      {:>}".format(format_text(port_line, 'bold')))
822
823
824def main():
825    parser = setParserOptions()
826    options = parser.parse_args()
827
828    if options.xtui:
829        options.tui = True
830
831    # always on top
832    if options.top:
833        set_window_always_on_top('trex_tui')
834
835
836    # Stateless client connection
837    if options.quiet:
838        verbose_level = LoggerApi.VERBOSE_QUIET
839    elif options.verbose:
840        verbose_level = LoggerApi.VERBOSE_HIGH
841    else:
842        verbose_level = LoggerApi.VERBOSE_REGULAR
843
844    # Stateless client connection
845    logger = ConsoleLogger()
846    stateless_client = STLClient(username = options.user,
847                                 server = options.server,
848                                 sync_port = options.port,
849                                 async_port = options.pub,
850                                 verbose_level = verbose_level,
851                                 logger = logger)
852
853    # TUI or no acquire will give us READ ONLY mode
854    try:
855        stateless_client.connect()
856    except STLError as e:
857        logger.log("Log:\n" + format_text(e.brief() + "\n", 'bold'))
858        return
859
860    if not options.tui and not options.readonly:
861        try:
862            # acquire all ports
863            stateless_client.acquire(options.acquire, force = options.force)
864        except STLError as e:
865            logger.log("Log:\n" + format_text(e.brief() + "\n", 'bold'))
866
867            logger.log("\n*** Failed to acquire all required ports ***\n")
868            return
869
870    if options.readonly:
871        logger.log(format_text("\nRead only mode - only few commands will be available", 'bold'))
872
873    show_intro(logger, stateless_client)
874
875
876    # a script mode
877    if options.batch:
878        cont = run_script_file(options.batch[0], stateless_client)
879        if not cont:
880            return
881
882    # console
883    try:
884        console = TRexConsole(stateless_client, options.verbose)
885        logger.prompt_redraw = console.prompt_redraw
886
887        # TUI
888        if options.tui:
889            console.do_tui("-x" if options.xtui else "-l")
890
891        else:
892            console.start()
893
894    except KeyboardInterrupt as e:
895        print("\n\n*** Caught Ctrl + C... Exiting...\n\n")
896
897    finally:
898        with stateless_client.logger.supress():
899            stateless_client.disconnect(stop_traffic = False)
900
901if __name__ == '__main__':
902
903    main()
904
905