1#!/usr/local/bin/python3.8
2# vim:ft=python
3#
4# Copyright (C) 2011 John Feuerstein <john@feurix.com>
5#
6#   Project URL: https://github.com/jhunt/hatop
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU Library General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21#
22'''\
23HATop is an interactive ncurses client for HAProxy
24==================================================
25
26Display mode reference:
27
28ID  Mode    Description
29
301   STATUS  The default mode with health, session and queue statistics
312   TRAFFIC Display connection and request rates as well as traffic stats
323   HTTP    Display various statistical information related to HTTP
334   ERRORS  Display health info, various error counters and downtimes
345   CLI     Display embedded command line client for the unix socket
35
36Keybind reference:
37
38Key             Action
39
40Hh?             Display this help screen
41CTRL-C / Qq     Quit
42
43TAB             Cycle mode forwards
44SHIFT-TAB       Cycle mode backwards
45ALT-n / ESC-n   Switch to mode n, where n is the numeric mode id
46ESC-ESC         Jump to previous mode
47
48ENTER           Display hotkey menu for selected service
49SPACE           Copy and paste selected service identifier to the CLI
50
51You can scroll the stat views using UP / DOWN / PGUP / PGDOWN / HOME / END.
52
53The reverse colored cursor line is used to select a given service instance.
54
55Some common administrative actions have hotkeys:
56
57Hotkey      Action
58
59F1          Enable server on all backends (return from maintenance mode)
60F2          Disable server on all backends (put into maintenance mode)
61
62F4          Restore initial server weight
63
64F5          Decrease server weight:     - 10
65F6          Decrease server weight:     -  1
66F7          Increase server weight:     +  1
67F8          Increase server weight:     + 10
68
69F9          Enable server on one backend (return from maintenance mode)
70F10         Disable server on one backend (put into maintenance mode)
71
72Hotkey actions and server responses are logged on the CLI viewport.
73You can scroll the output on the CLI view using PGUP / PGDOWN.
74
75Header reference:
76
77Node        configured name of the haproxy node
78Uptime      runtime since haproxy was initially started
79Pipes       pipes are currently used for kernel-based tcp slicing
80Procs       number of haproxy processes
81Tasks       number of actice process tasks
82Queue       number of queued process tasks (run queue)
83Proxies     number of configured proxies
84Services    number of configured services
85
86In multiple modes:
87
88NAME        name of the proxy and his services
89W           configured weight of the service
90STATUS      service status (UP/DOWN/NOLB/MAINT/MAINT(via)...)
91CHECK       status of last health check (see status reference below)
92
93In STATUS mode:
94
95ACT         server is active (server), number of active servers (backend)
96BCK         server is backup (server), number of backup servers (backend)
97QCUR        current queued requests
98QMAX        max queued requests
99SCUR        current sessions
100SMAX        max sessions
101SLIM        sessions limit
102STOT        total sessions
103
104In TRAFFIC mode:
105
106LBTOT       total number of times a server was selected
107RATE        number of sessions per second over last elapsed second
108RLIM        limit on new sessions per second
109RMAX        max number of new sessions per second
110BIN         bytes in (IEEE 1541-2002)
111BOUT        bytes out (IEEE 1541-2002)
112
113In HTTP mode:
114
115RATE        HTTP requests per second over last elapsed second
116RMAX        max number of HTTP requests per second observed
117RTOT        total number of HTTP requests received
1181xx         number of HTTP responses with 1xx code
1192xx         number of HTTP responses with 2xx code
1203xx         number of HTTP responses with 3xx code
1214xx         number of HTTP responses with 4xx code
1225xx         number of HTTP responses with 5xx code
123?xx         number of HTTP responses with other codes (protocol error)
124
125In ERRORS mode:
126
127CF          number of failed checks
128CD          number of UP->DOWN transitions
129CL          last status change
130ECONN       connection errors
131EREQ        request errors
132ERSP        response errors
133DREQ        denied requests
134DRSP        denied responses
135DOWN        total downtime
136
137Health check status reference:
138
139UNK         unknown
140INI         initializing
141SOCKERR     socket error
142L4OK        check passed on layer 4, no upper layers testing enabled
143L4TMOUT     layer 1-4 timeout
144L4CON       layer 1-4 connection problem, for example
145            "Connection refused" (tcp rst) or "No route to host" (icmp)
146L6OK        check passed on layer 6
147L6TOUT      layer 6 (SSL) timeout
148L6RSP       layer 6 invalid response - protocol error
149L7OK        check passed on layer 7
150L7OKC       check conditionally passed on layer 7, for example 404 with
151            disable-on-404
152L7TOUT      layer 7 (HTTP/SMTP) timeout
153L7RSP       layer 7 invalid response - protocol error
154L7STS       layer 7 response error, for example HTTP 5xx
155'''
156__author__    = 'John Feuerstein <john@feurix.com>'
157__copyright__ = 'Copyright (C) 2011 %s' % __author__
158__license__   = 'GNU GPLv3'
159__version__   = '0.8.0'
160
161import fcntl
162import os
163import re
164import signal
165import socket
166import struct
167import sys
168import time
169import tty
170
171import curses
172import curses.ascii
173
174from socket import error as SocketError
175from _curses import error as CursesError
176
177from collections import deque
178from textwrap import TextWrapper
179
180# ------------------------------------------------------------------------- #
181#                               GLOBALS                                     #
182# ------------------------------------------------------------------------- #
183
184# Settings of interactive command session over the unix-socket
185HAPROXY_CLI_BUFSIZE = 4096
186HAPROXY_CLI_TIMEOUT = 60
187HAPROXY_CLI_PROMPT = '> '
188HAPROXY_CLI_CMD_SEP = ';'
189HAPROXY_CLI_CMD_TIMEOUT = 1
190HAPROXY_CLI_MAXLINES = 1000
191
192# Settings of the embedded CLI
193CLI_MAXLINES = 1000
194CLI_MAXHIST = 100
195CLI_INPUT_LIMIT = 200
196CLI_INPUT_RE = re.compile('[a-zA-Z0-9_:\.\-\+; /#%]')
197CLI_INPUT_DENY_CMD = ['prompt', 'set timeout cli', 'quit']
198
199# Note: Only the last 3 lines are visible instantly on 80x25
200CLI_HELP_TEXT = '''\
201             Welcome on the embedded interactive HAProxy shell!
202
203                  Type `help' to get a command reference
204'''
205
206# Screen setup
207SCREEN_XMIN = 78
208SCREEN_YMIN = 20
209SCREEN_XMAX = 200
210SCREEN_YMAX = 100
211SCREEN_HPOS = 11
212
213HAPROXY_INFO_RE = {
214'software_name':    re.compile('^Name:\s*(?P<value>\S+)'),
215'software_version': re.compile('^Version:\s*(?P<value>\S+)'),
216'software_release': re.compile('^Release_date:\s*(?P<value>\S+)'),
217'nproc':            re.compile('^Nbproc:\s*(?P<value>\d+)'),
218'procn':            re.compile('^Process_num:\s*(?P<value>\d+)'),
219'pid':              re.compile('^Pid:\s*(?P<value>\d+)'),
220'uptime':           re.compile('^Uptime:\s*(?P<value>[\S ]+)$'),
221'maxconn':          re.compile('^Maxconn:\s*(?P<value>\d+)'),
222'curconn':          re.compile('^CurrConns:\s*(?P<value>\d+)'),
223'maxpipes':         re.compile('^Maxpipes:\s*(?P<value>\d+)'),
224'curpipes':         re.compile('^PipesUsed:\s*(?P<value>\d+)'),
225'tasks':            re.compile('^Tasks:\s*(?P<value>\d+)'),
226'runqueue':         re.compile('^Run_queue:\s*(?P<value>\d+)'),
227'node':             re.compile('^node:\s*(?P<value>\S+)'),
228}
229
230HAPROXY_STAT_MAX_SERVICES = 1000
231HAPROXY_STAT_LIMIT_WARNING = '''\
232Warning: You have reached the stat parser limit! (%d)
233Use --filter to parse specific service stats only.
234''' % HAPROXY_STAT_MAX_SERVICES
235HAPROXY_STAT_FILTER_RE = re.compile(
236        '^(?P<iid>-?\d+)\s+(?P<type>-?\d+)\s+(?P<sid>-?\d+)$')
237HAPROXY_STAT_PROXY_FILTER_RE = re.compile(
238        '^(?P<pxname>[a-zA-Z0-9_:\.\-]+)$')
239HAPROXY_STAT_COMMENT = '#'
240HAPROXY_STAT_SEP = ','
241HAPROXY_STAT_CSV = [
242# Note: Fields must be listed in correct order, as described in:
243# http://haproxy.1wt.eu/download/1.4/doc/configuration.txt [9.1]
244
245# TYPE  FIELD
246
247(str,   'pxname'),          # proxy name
248(str,   'svname'),          # service name (FRONTEND / BACKEND / name)
249(int,   'qcur'),            # current queued requests
250(int,   'qmax'),            # max queued requests
251(int,   'scur'),            # current sessions
252(int,   'smax'),            # max sessions
253(int,   'slim'),            # sessions limit
254(int,   'stot'),            # total sessions
255(int,   'bin'),             # bytes in
256(int,   'bout'),            # bytes out
257(int,   'dreq'),            # denied requests
258(int,   'dresp'),           # denied responses
259(int,   'ereq'),            # request errors
260(int,   'econ'),            # connection errors
261(int,   'eresp'),           # response errors (among which srv_abrt)
262(int,   'wretr'),           # retries (warning)
263(int,   'wredis'),          # redispatches (warning)
264(str,   'status'),          # status (UP/DOWN/NOLB/MAINT/MAINT(via)...)
265(int,   'weight'),          # server weight (server), total weight (backend)
266(int,   'act'),             # server is active (server),
267                            # number of active servers (backend)
268(int,   'bck'),             # server is backup (server),
269                            # number of backup servers (backend)
270(int,   'chkfail'),         # number of failed checks
271(int,   'chkdown'),         # number of UP->DOWN transitions
272(int,   'lastchg'),         # last status change (in seconds)
273(int,   'downtime'),        # total downtime (in seconds)
274(int,   'qlimit'),          # queue limit
275(int,   'pid'),             # process id
276(int,   'iid'),             # unique proxy id
277(int,   'sid'),             # service id (unique inside a proxy)
278(int,   'throttle'),        # warm up status
279(int,   'lbtot'),           # total number of times a server was selected
280(str,   'tracked'),         # id of proxy/server if tracking is enabled
281(int,   'type'),            # (0=frontend, 1=backend, 2=server, 3=socket)
282(int,   'rate'),            # number of sessions per second
283                            # over the last elapsed second
284(int,   'rate_lim'),        # limit on new sessions per second
285(int,   'rate_max'),        # max number of new sessions per second
286(str,   'check_status'),    # status of last health check
287(int,   'check_code'),      # layer5-7 code, if available
288(int,   'check_duration'),  # time in ms took to finish last health check
289(int,   'hrsp_1xx'),        # http responses with 1xx code
290(int,   'hrsp_2xx'),        # http responses with 2xx code
291(int,   'hrsp_3xx'),        # http responses with 3xx code
292(int,   'hrsp_4xx'),        # http responses with 4xx code
293(int,   'hrsp_5xx'),        # http responses with 5xx code
294(int,   'hrsp_other'),      # http responses with other codes (protocol error)
295(str,   'hanafail'),        # failed health checks details
296(int,   'req_rate'),        # HTTP requests per second
297(int,   'req_rate_max'),    # max number of HTTP requests per second
298(int,   'req_tot'),         # total number of HTTP requests received
299(int,   'cli_abrt'),        # number of data transfers aborted by client
300(int,   'srv_abrt'),        # number of data transfers aborted by server
301]
302HAPROXY_STAT_NUMFIELDS = len(HAPROXY_STAT_CSV)
303HAPROXY_STAT_CSV = [(k, v) for k, v in enumerate(HAPROXY_STAT_CSV)]
304
305# All big numeric values on the screen are prefixed using the metric prefix
306# set, while everything byte related is prefixed using binary prefixes.
307# Note: If a non-byte numeric value fits into the field, we skip prefixing.
308PREFIX_BINARY = {
309        1024:    'K',
310        1024**2: 'M',
311}
312PREFIX_METRIC = {
313        1000:    'k',
314        1000**2: 'M',
315        1000**3: 'G',
316}
317PREFIX_TIME = {
318        60:      'm',
319        60*60:   'h',
320        60*60*24:'d',
321}
322
323# ------------------------------------------------------------------------- #
324#                           CLASS DEFINITIONS                               #
325# ------------------------------------------------------------------------- #
326
327# Use bounded length deque if available (Python 2.6+)
328try:
329    deque(maxlen=0)
330
331    class RingBuffer(deque):
332
333        def __init__(self, maxlen):
334            assert maxlen > 0
335            deque.__init__(self, maxlen=maxlen)
336
337except TypeError:
338
339    class RingBuffer(deque):
340
341        def __init__(self, maxlen):
342            assert maxlen > 0
343            deque.__init__(self)
344            self.maxlen = maxlen
345
346        def append(self, item):
347            if len(self) == self.maxlen:
348                self.popleft()
349            deque.append(self, item)
350
351        def appendleft(self, item):
352            if len(self) == self.maxlen:
353                self.popright()
354            deque.appendleft(self, item)
355
356        def extend(self, iterable):
357            for item in iterable:
358                self.append(item)
359
360        def extendleft(self, iterable):
361            for item in iterable:
362                self.appendleft(item)
363
364
365class Socket:
366
367    def __init__(self, path, readonly=False, tcp=False):
368        self.path = path
369        self.ro = readonly
370        self.tcp = tcp
371        if not self.tcp:
372            self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
373        else:
374            self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
375
376    def _recv(self):
377        # socket.recv() wrapper raising SocketError if we receive
378        # EOF before seeing the interactive socket prompt.
379        data = self._socket.recv(HAPROXY_CLI_BUFSIZE)
380        if not data:
381            raise SocketError('error while waiting for prompt')
382        return data.decode()
383
384    def connect(self):
385        # Initialize socket connection
386        self._socket.settimeout(HAPROXY_CLI_CMD_TIMEOUT)
387        if self.tcp:
388            host, port = self.path.split(":")
389            self._socket.connect((host, int(port)))
390        else:
391            self._socket.connect(self.path)
392
393        # Enter the interactive socket mode. This requires HAProxy 1.4+ and
394        # allows us to error out early if connected to an older version.
395        try:
396            self.send(b'prompt')
397            self.wait()
398            self.send(b'set timeout cli %d' % HAPROXY_CLI_TIMEOUT)
399            self.wait()
400        except SocketError:
401            raise SocketError('error while initializing interactive mode')
402
403    def close(self):
404        try:
405            self.send(b'quit')
406        except:
407            pass
408        try:
409            self._socket.close()
410        except:
411            pass
412
413    def send(self, cmdline):
414        self._socket.sendall(b'%s\n' % cmdline)
415
416    def wait(self):
417        # Wait for the prompt and discard data.
418        rbuf = ''
419        while not rbuf.endswith(HAPROXY_CLI_PROMPT):
420            data = self._recv()
421            rbuf = rbuf[-(len(HAPROXY_CLI_PROMPT)-1):] + data
422
423    def recv(self):
424        # Receive lines until HAPROXY_CLI_MAXLINES or the prompt is reached.
425        # If the prompt was still not found, discard data and wait for it.
426        linecount = 0
427        rbuf = ''
428        while not rbuf.endswith(HAPROXY_CLI_PROMPT):
429
430            if linecount == HAPROXY_CLI_MAXLINES:
431                data = self._recv()
432                rbuf = rbuf[-(len(HAPROXY_CLI_PROMPT)-1):] + data
433                continue
434
435            data = self._recv()
436            rbuf += data
437
438            while linecount < HAPROXY_CLI_MAXLINES and '\n' in rbuf:
439                line, rbuf = rbuf.split('\n', 1)
440                linecount += 1
441                yield line
442
443
444class SocketData:
445
446    def __init__(self, socket):
447        self.socket = socket
448        self.pxcount = 0
449        self.svcount = 0
450        self.info = {}
451        self.stat = {}
452        self._filters = set()
453
454    def register_stat_filter(self, stat_filter):
455
456        # Validate and register filters
457        stat_filter_set = set(stat_filter)
458        for filter in stat_filter_set:
459            match = HAPROXY_STAT_FILTER_RE.match(filter)
460            if not match:
461                raise ValueError('invalid stat filter: %s' % filter)
462            self._filters.add((
463                    int(match.group('iid'), 10),
464                    int(match.group('type'), 10),
465                    int(match.group('sid'), 10),
466            ))
467
468    def register_proxy_filter(self, proxy_filter):
469
470        # Validate filters
471        proxy_filter_set = set(proxy_filter)
472        for filter in proxy_filter_set:
473            if not HAPROXY_STAT_PROXY_FILTER_RE.match(filter):
474                raise ValueError('invalid proxy filter: %s' % filter)
475
476        # Convert proxy filters into more efficient stat filters
477        self.socket.send(b'show stat')
478        pxstat, pxcount, svcount = parse_stat(self.socket.recv())
479
480        proxy_iid_map = {} # {pxname: iid, ...}
481
482        for pxname in proxy_filter_set:
483            for iid in pxstat:
484                for sid in pxstat[iid]:
485                    if pxstat[iid][sid]['pxname'] == pxname:
486                        proxy_iid_map[pxname] = iid
487                    break
488                if pxname in proxy_iid_map:
489                    break
490
491        for pxname in proxy_filter_set:
492            if not pxname in proxy_iid_map:
493                raise RuntimeError('proxy not found: %s' % pxname)
494
495        # Register filters
496        for iid in proxy_iid_map.values():
497            self._filters.add((iid, -1, -1))
498
499    def update_info(self):
500        self.socket.send(b'show info')
501        iterable = self.socket.recv()
502        self.info = parse_info(iterable)
503
504    def update_stat(self):
505        # Store current data
506        pxcount_old = self.pxcount
507        svcount_old = self.svcount
508        stat_old = self.stat
509
510        # Reset current data
511        self.pxcount = 0
512        self.svcount = 0
513        self.stat = {}
514
515        if self._filters:
516            for filter in self._filters:
517                self.socket.send(b'show stat %d %d %d' % filter)
518                filter_stat, filter_pxcount, filter_svcount = \
519                        parse_stat(self.socket.recv())
520
521                if filter_pxcount == 0:
522                    raise RuntimeError('stale stat filter: %d %d %d' % filter)
523
524                self.pxcount += filter_pxcount
525                self.svcount += filter_svcount
526                self.stat.update(filter_stat)
527        else:
528            self.socket.send(b'show stat')
529            self.stat, self.pxcount, self.svcount = \
530                    parse_stat(self.socket.recv())
531
532        if self.pxcount == 0:
533            raise RuntimeWarning('no stat data available')
534
535        # Warn if the HAProxy configuration has changed on-the-fly
536        pxdiff = 0
537        svdiff = 0
538
539        if self.pxcount < pxcount_old:
540            pxdiff -= pxcount_old - self.pxcount
541        if pxcount_old > 0 and self.pxcount > pxcount_old:
542            pxdiff += self.pxcount - pxcount_old
543        if self.svcount < svcount_old:
544            svdiff -= svcount_old - self.svcount
545        if svcount_old > 0 and self.svcount > svcount_old:
546            svdiff += self.svcount - svcount_old
547
548        if pxdiff != 0 or svdiff != 0:
549            raise RuntimeWarning(
550                    'config changed: proxy %+d, service %+d '
551                    '(reloading...)' % (pxdiff, svdiff))
552
553
554class ScreenCLI:
555
556    def __init__(self, screen):
557        self.screen = screen
558
559        # Output
560        self.obuf = RingBuffer(CLI_MAXLINES)
561        self.ypos = 0
562        self.wrapper = TextWrapper()
563        self.screenlines = []
564
565        # Input
566        self.ihist = RingBuffer(CLI_MAXHIST)
567        self.ibuf = []
568        self.ibpos = 0
569        self.ibmin = 0
570
571
572    # INPUT
573    @property
574    def imin(self):
575        return self.screen.xmin + 2
576
577    @property
578    def imax(self):
579        return self.screen.xmax - 4
580
581    @property
582    def ispan(self):
583        return self.imax - self.imin
584
585    @property
586    def ipos(self):
587        return self.ibpos - self.ibmin
588
589    @property
590    def ibmax(self):
591        return self.ibmin + self.ispan
592
593    @property
594    def iblen(self):
595        return len(self.ibuf)
596
597    @property
598    def cmdline(self):
599        return ''.join(self.ibuf)
600
601    # OUTPUT
602    @property
603    def ospan(self):
604        return self.screen.span - 1
605
606
607    def setup(self):
608        self.ipad = curses.newpad(1, SCREEN_XMAX)               # input
609        self.opad = curses.newpad(SCREEN_YMAX, SCREEN_XMAX)     # output
610
611        # Display initial help text...
612        self.obuf.extend(CLI_HELP_TEXT.split('\n'))
613        self.update_screenlines()
614
615    def start(self):
616        try:
617            curses.curs_set(1)
618        except CursesError:
619            pass
620        self.draw_output()
621        self.draw_input()
622
623    def stop(self):
624        try:
625            curses.curs_set(0)
626        except CursesError:
627            pass
628
629    def update_screenlines(self):
630        self.wrapper.width = self.screen.xmax
631        self.screenlines = []
632        for line in self.obuf:
633            if len(line) > self.wrapper.width:
634                self.screenlines.extend(self.wrapper.wrap(line))
635            else:
636                self.screenlines.append(line)
637        self.ypos = len(self.screenlines)
638
639    def reset_input(self):
640        self.ibuf = []
641        self.ibpos = 0
642        self.ibmin = 0
643
644    def refresh_input(self, sync=False):
645        if sync:
646            refresh = self.ipad.refresh
647        else:
648            refresh = self.ipad.noutrefresh
649
650        refresh(0, 0,
651                self.screen.smax, self.screen.xmin,
652                self.screen.smax, self.screen.xmax - 1)
653
654    def refresh_output(self, sync=False):
655        if sync:
656            refresh = self.opad.refresh
657        else:
658            refresh = self.opad.noutrefresh
659
660        refresh(0, 0,
661                self.screen.smin, self.screen.xmin,
662                self.screen.smax - 2, self.screen.xmax - 1)
663
664    def draw_input(self):
665        self.ipad.clear()
666        self.ipad.addstr(0, 0, '> ', curses.A_BOLD)
667        self.ipad.addstr(0, 2, ''.join(self.ibuf[self.ibmin:self.ibmax]))
668
669        # Mark input lines longer than the visible input span
670        if self.ibmin > 0:
671            self.ipad.addstr(0, self.imin - 1, '<')
672        if self.iblen > self.ibmax:
673            self.ipad.addstr(0, self.imax, '>')
674
675        self.ipad.move(0, self.imin + self.ipos)
676
677    def draw_output(self):
678        self.opad.clear()
679        vmin = max(0, self.ypos - self.ospan)
680        vmax = vmin + self.ospan
681        lines = self.screenlines[vmin:vmax]
682        self.opad.addstr(0, 0, '\n'.join(lines))
683
684    # INPUT
685    def prev(self):
686        if len(self.ihist) == 0:
687            return
688        if len(self.ibuf) == 0:
689            self.ibuf = list(self.ihist[-1])
690            self.mvend()
691            return
692        if self.ibuf != self.ihist[-1]:
693            self.ihist.append(self.ibuf)
694        self.ihist.rotate(1)
695        self.ibuf = list(self.ihist[-1])
696        self.mvend()
697
698    def __next__(self):
699        if len(self.ihist) == 0:
700            return
701        self.ihist.rotate(-1)
702        self.ibuf = list(self.ihist[-1])
703        self.mvend()
704
705    def puts(self, s):
706        s = list(s)
707        if len(self.ibuf) + len(s) >= CLI_INPUT_LIMIT:
708            return
709        for c in s:
710            if not CLI_INPUT_RE.match(c):
711                return
712
713        if self.ibpos < self.iblen:
714            self.ibuf = self.ibuf[:self.ibpos] + s + self.ibuf[self.ibpos:]
715        else:
716            self.ibuf.extend(s)
717
718        self.mvc(len(s))
719        return True
720
721    def putc(self, c):
722        if len(self.ibuf) == CLI_INPUT_LIMIT:
723            return
724        if not CLI_INPUT_RE.match(c):
725            return
726
727        if self.ibpos < self.iblen:
728            self.ibuf.insert(self.ibpos, c)
729        else:
730            self.ibuf.append(c)
731
732        self.mvc(1)
733
734    def delc(self, n):
735        if n == 0 or self.iblen == 0:
736            return
737
738        # Delete LEFT
739        elif n < 0 and self.ibpos >= 1:
740            self.ibuf.pop(self.ibpos - 1)
741            self.mvc(-1)
742
743        # Delete RIGHT
744        elif n > 0 and self.ibpos < self.iblen:
745            self.ibuf.pop(self.ibpos)
746            self.draw_input()
747            self.refresh_input(sync=True)
748
749    def mvhome(self):
750        self.ibmin = 0
751        self.ibpos = 0
752        self.draw_input()
753        self.refresh_input(sync=True)
754
755    def mvend(self):
756        self.ibmin = max(0, self.iblen - self.ispan)
757        self.ibpos = self.iblen
758        self.draw_input()
759        self.refresh_input(sync=True)
760
761    def mvc(self, n):
762        if n == 0:
763            return
764
765        # Move LEFT
766        if n < 0:
767            self.ibpos = max(0, self.ibpos + n)
768            if self.ibpos < self.ibmin:
769                self.ibmin = self.ibpos
770
771        # Move RIGHT
772        elif n > 0:
773            self.ibpos = min(self.iblen, self.ibpos + n)
774            if self.ibpos > self.ibmax:
775                self.ibmin += n
776
777        self.draw_input()
778        self.refresh_input(sync=True)
779
780    # OUTPUT
781    def mvo(self, n):
782        if n == 0:
783            return
784
785        # Move UP
786        if n < 0 and self.ypos > self.ospan:
787            self.ypos = max(self.ospan, self.ypos + n)
788
789        # Move DOWN
790        elif n > 0 and self.ypos < len(self.screenlines):
791            self.ypos = min(len(self.screenlines), self.ypos + n)
792
793        self.draw_output()
794        self.refresh_output(sync=True)
795
796    def execute(self):
797
798        # Nothing to do... print marker line instead.
799        if self.iblen == 0:
800            self.obuf.append('- %s %s' % (time.ctime(), '-' * 50))
801            self.obuf.append('')
802            self.update_screenlines()
803            self.draw_output()
804            self.refresh_output(sync=True)
805            self.refresh_input(sync=True)
806            return
807
808        # Validate each command on the command line
809        cmds = [cmd.strip() for cmd in
810                self.cmdline.split(HAPROXY_CLI_CMD_SEP)]
811
812        for pattern in CLI_INPUT_DENY_CMD:
813            for cmd in cmds:
814                if re.match(r'^\s*%s(?:\s|$)' % pattern, cmd):
815                    self.obuf.append('* command not allowed: %s' % cmd)
816                    self.obuf.append('')
817                    self.update_screenlines()
818                    self.draw_output()
819                    self.refresh_output(sync=True)
820                    self.refresh_input(sync=True)
821                    return
822
823        self.execute_cmdline(self.cmdline)
824
825        self.draw_output()
826        self.refresh_output(sync=True)
827
828        self.ihist.append(self.ibuf)
829        self.reset_input()
830        self.draw_input()
831        self.refresh_input(sync=True)
832
833    def execute_cmdline(self, cmdline):
834        self.obuf.append('* %s' % time.ctime())
835        self.obuf.append('> %s' % cmdline)
836        self.screen.data.socket.send(cmdline.encode())
837        self.obuf.extend(self.screen.data.socket.recv())
838        self.update_screenlines()
839
840
841class Screen:
842
843    def __init__(self, data, mid=1):
844        self.data = data
845        self.modes = SCREEN_MODES
846        self.sb_conn = StatusBar()
847        self.sb_pipe = StatusBar()
848        self.lines = []
849        self.screen = None
850        self.xmin = 0
851        self.xmax = SCREEN_XMIN
852        self.ymin = 0
853        self.ymax = SCREEN_YMIN
854        self.vmin = 0
855        self.cmin = 0
856        self.cpos = 0
857        self.hpos = SCREEN_HPOS
858        self.help = ScreenHelp(self)
859        self.cli = ScreenCLI(self)
860
861        self._pmid = mid # previous mode id
862        self._cmid = mid # current mode id
863        self._mode = self.modes[mid]
864
865        # Display state
866        self.active = False
867
868        # Assume a dumb TTY, setup() will detect smart features.
869        self.dumbtty = True
870
871        # Toggled by the SIGWINCH handler...
872        # Note: Defaults to true to force the initial size sync.
873        self._resized = True
874
875        # Display given exceptions on screen
876        self.exceptions = []
877
878        # Show cursor line?
879        self.cursor = True
880
881        # Show hotkeys?
882        self.hotkeys = False
883
884    def _sigwinchhandler(self, signum, frame):
885        self._resized = True
886
887    @property
888    def resized(self):
889        if self.dumbtty:
890            ymax, xmax = self.getmaxyx()
891            return ymax != self.ymax or xmax != self.xmax
892        else:
893            return self._resized
894
895    @property
896    def mid(self):
897        return self._cmid
898
899    @property
900    def mode(self):
901        return self._mode
902
903    @property
904    def ncols(self):
905        return self.xmax - self.xmin
906
907    @property
908    def smin(self):
909        return self.hpos + 2
910
911    @property
912    def smax(self):
913        return self.ymax - 3
914
915    @property
916    def span(self):
917        return self.smax - self.smin
918
919    @property
920    def cmax(self):
921        return min(self.span, len(self.lines) - 1)
922
923    @property
924    def cstat(self):
925        return self.lines[self.vpos].stat
926
927    @property
928    def vpos(self):
929        return self.vmin + self.cpos
930
931    @property
932    def vmax(self):
933        return min(self.vmin + self.span, len(self.lines) - 1)
934
935    @property
936    def screenlines(self):
937        return enumerate(self.lines[self.vmin:self.vmax + 1])
938
939    # Proxies
940    def getch(self, *args, **kwargs):
941        return self.screen.getch(*args, **kwargs)
942    def hline(self, *args, **kwargs):
943        return self.screen.hline(*args, **kwargs)
944    def addstr(self, *args, **kwargs):
945        return self.screen.addstr(*args, **kwargs)
946
947    def setup(self):
948        self.screen = curses_init()
949        self.screen.keypad(1)
950        self.screen.nodelay(1)
951        self.screen.idlok(1)
952        self.screen.move(0, 0)
953        curses.def_prog_mode()
954        self.help.setup()
955        self.cli.setup()
956
957        # Register some terminal resizing magic if supported...
958        if hasattr(curses, 'resize_term') and hasattr(signal, 'SIGWINCH'):
959            self.dumbtty = False
960            signal.signal(signal.SIGWINCH, self._sigwinchhandler)
961
962        # If we came this far the display is active
963        self.active = True
964
965    def getmaxyx(self):
966        ymax, xmax = self.screen.getmaxyx()
967        xmax = min(xmax, SCREEN_XMAX)
968        ymax = min(ymax, SCREEN_YMAX)
969        return ymax, xmax
970
971    def resize(self):
972        if not self.dumbtty:
973            self.clear()
974            size = fcntl.ioctl(0, tty.TIOCGWINSZ, '12345678')
975            size = struct.unpack('4H', size)
976            curses.resize_term(size[0], size[1])
977
978        ymax, xmax = self.getmaxyx()
979
980        if xmax < SCREEN_XMIN or ymax < SCREEN_YMIN:
981            raise RuntimeError(
982                    'screen too small, need at least %dx%d'
983                    % (SCREEN_XMIN, SCREEN_YMIN))
984        if ymax == self.ymax and xmax == self.xmax:
985            self._resized = False
986            return
987        if xmax != self.xmax:
988            self.xmax = xmax
989        if ymax != self.ymax:
990            self.ymax = ymax
991
992        self.mode.sync(self)
993
994        # Force re-wrapping of the screenlines in CLI mode
995        if self.mid == 5:
996            self.cli.update_screenlines()
997            self.cli.draw_output()
998
999        self._resized = False
1000
1001    def reset(self):
1002        if not self.active:
1003            return
1004        curses_reset(self.screen)
1005
1006    def recover(self):
1007        curses.reset_prog_mode()
1008
1009    def refresh(self):
1010        self.screen.noutrefresh()
1011
1012        if self.mid == 0:
1013            self.help.refresh()
1014        elif self.mid == 5:
1015            self.cli.refresh_output()
1016            self.cli.refresh_input()
1017
1018        curses.doupdate()
1019
1020    def clear(self):
1021        # Note: Forces the whole screen to be repainted upon refresh()
1022        self.screen.clear()
1023
1024    def erase(self):
1025        self.screen.erase()
1026
1027    def switch_mode(self, mid):
1028        if mid == 5 and self.data.socket.ro:
1029            return # noop
1030
1031        mode = self.modes[mid]
1032        mode.sync(self)
1033
1034        if self.mid != 5 and mid == 5:
1035            self.cli.start()
1036        elif self.mid == 5 and mid != 5:
1037            self.cli.stop()
1038
1039        self._pmid = self._cmid
1040        self._cmid, self._mode = mid, mode
1041
1042    def toggle_mode(self):
1043        if self._pmid == self._cmid:
1044            return
1045        self.switch_mode(self._pmid)
1046
1047    def cycle_mode(self, n):
1048        if n == 0:
1049            return
1050
1051        if self.data.socket.ro:
1052            border = 4
1053        else:
1054            border = 5
1055
1056        if self._cmid == 0:
1057            self.switch_mode(1)
1058        elif n < 0 and self._cmid == 1:
1059            self.switch_mode(border)
1060        elif n > 0 and self._cmid == border:
1061            self.switch_mode(1)
1062        else:
1063            self.switch_mode(self._cmid + n)
1064
1065    def update_data(self):
1066        self.data.update_info()
1067        try:
1068            self.data.update_stat()
1069        except RuntimeWarning as x:
1070            self.exceptions.append(x)
1071
1072    def update_bars(self):
1073        self.sb_conn.update_max(int(self.data.info['maxconn'], 10))
1074        self.sb_conn.update_cur(int(self.data.info['curconn'], 10))
1075        self.sb_pipe.update_max(int(self.data.info['maxpipes'], 10))
1076        self.sb_pipe.update_cur(int(self.data.info['curpipes'], 10))
1077
1078    def update_lines(self):
1079        # Display non-fatal exceptions on screen
1080        if self.exceptions:
1081            self.mvhome()
1082            self.lines = []
1083            self.cursor = False
1084            for x in self.exceptions:
1085                for line in str(x).splitlines():
1086                    line = line.center(SCREEN_XMIN)
1087                    self.lines.append(ScreenLine(text=line))
1088            self.exceptions = []
1089            return
1090
1091        # Reset cursor visibility
1092        if not self.cursor:
1093            self.cursor = True
1094
1095        # Update screen lines
1096        self.lines = get_screenlines(self.data.stat)
1097        if self.data.svcount >= HAPROXY_STAT_MAX_SERVICES:
1098            self.lines.append(ScreenLine())
1099            for line in HAPROXY_STAT_LIMIT_WARNING.splitlines():
1100                self.lines.append(ScreenLine(text=line))
1101
1102    def draw_line(self, ypos, xpos=0, text=None,
1103            attr=curses.A_REVERSE):
1104        self.hline(ypos, self.xmin, ' ', self.xmax, attr)
1105        if text:
1106            self.addstr(ypos, self.xmin + xpos, text, attr)
1107
1108    def draw_head(self):
1109        self.draw_line(self.ymin)
1110        attr = curses.A_REVERSE | curses.A_BOLD
1111        self.addstr(self.ymin, self.xmin,
1112                time.ctime().rjust(self.xmax - 1), attr)
1113        self.addstr(self.ymin, self.xmin + 1,
1114                'HATop version ' + __version__, attr)
1115
1116    def draw_info(self):
1117        self.addstr(self.ymin + 2, self.xmin + 2,
1118                '%s Version: %s  (released: %s)' % (
1119                    self.data.info['software_name'],
1120                    self.data.info['software_version'],
1121                    self.data.info['software_release'],
1122                ), curses.A_BOLD)
1123        self.addstr(self.ymin + 2, self.xmin + 56,
1124                'PID: %d (proc %d)' % (
1125                    int(self.data.info['pid'], 10),
1126                    int(self.data.info['procn'], 10),
1127                ), curses.A_BOLD)
1128        self.addstr(self.ymin + 4, self.xmin + 2,
1129                '       Node: %s (uptime %s)' % (
1130                    self.data.info['node'] or 'unknown',
1131                    self.data.info['uptime'],
1132                ))
1133        self.addstr(self.ymin + 6, self.xmin + 2,
1134                '      Pipes: %s'  % self.sb_pipe)
1135        self.addstr(self.ymin + 7, self.xmin + 2,
1136                'Connections: %s'  % self.sb_conn)
1137        self.addstr(self.ymin + 9, self.xmin + 2,
1138                'Procs: %3d   Tasks: %5d    Queue: %5d    '
1139                'Proxies: %3d   Services: %4d' % (
1140                    int(self.data.info['nproc'], 10),
1141                    int(self.data.info['tasks'], 10),
1142                    int(self.data.info['runqueue'], 10),
1143                    self.data.pxcount,
1144                    self.data.svcount,
1145                ))
1146
1147    def draw_cols(self):
1148        self.draw_line(self.hpos, text=self.mode.head,
1149                attr=curses.A_REVERSE | curses.A_BOLD)
1150
1151    def draw_foot(self):
1152        xpos = self.xmin
1153        ypos = self.ymax - 1
1154        self.draw_line(ypos)
1155        attr_active = curses.A_BOLD
1156        attr_inactive = curses.A_BOLD | curses.A_REVERSE
1157
1158        # HOTKEYS
1159        if (self.hotkeys and
1160                0 < self.mid < 5 and
1161                self.cstat and
1162                self.cstat['iid'] > 0 and
1163                self.cstat['sid'] > 0):
1164                self.draw_line(ypos)
1165                self.addstr(ypos, 1, 'HOTKEYS:',
1166                        curses.A_BOLD | curses.A_REVERSE)
1167                self.addstr(ypos, 11,
1168                        'F1=ENABLE  F2=DISABLE  '
1169                        'F4=W-RESET  '
1170                        'F5=W-10  F6=W-1  F7=W+1  F8=W+10  '
1171                        'F9=ENABLE (HERE)  F10=DISABLE (HERE)',
1172                        curses.A_NORMAL | curses.A_REVERSE)
1173                return
1174
1175        # VIEWPORTS
1176        for mid, mode in enumerate(self.modes):
1177            if mid == 0:
1178                continue
1179            if mid == 5 and self.data.socket.ro:
1180                continue
1181            if mid == self.mid:
1182                attr = attr_active
1183            else:
1184                attr = attr_inactive
1185
1186            s = ' %d-%s ' % (mid, mode.name)
1187            self.addstr(ypos, xpos, s, attr)
1188            xpos += len(s)
1189
1190        if 0 < self.mid < 5 and self.cstat:
1191            if self.cstat['iid'] > 0 and self.cstat['sid'] > 0:
1192                if self.data.socket.ro:
1193                    s = 'READ-ONLY [#%d/#%d]' % (
1194                            self.cstat['iid'], self.cstat['sid'])
1195                else:
1196                    s = 'ENTER=MENU SPACE=SEL [#%d/#%d]' % (
1197                            self.cstat['iid'], self.cstat['sid'])
1198            else:
1199                if self.data.socket.ro:
1200                    s = 'READ-ONLY [#%d/#%d]' % (
1201                            self.cstat['iid'], self.cstat['sid'])
1202                else:
1203                    s = '[#%d/#%d]' % (
1204                            self.cstat['iid'], self.cstat['sid'])
1205        elif self.mid == 5:
1206            s = 'PGUP/PGDOWN=SCROLL'
1207        else:
1208            s = 'UP/DOWN=SCROLL H=HELP Q=QUIT'
1209        self.addstr(ypos, self.xmax - len(s) - 1, s, attr_inactive)
1210
1211    def draw_stat(self):
1212        for idx, line in self.screenlines:
1213            if self.cursor and idx == self.cpos:
1214                attr = line.attr | curses.A_REVERSE
1215            else:
1216                attr = line.attr
1217            if not line.stat:
1218                screenline = get_cell(self.xmax, 'L', line.text)
1219            elif 'message' in line.stat:
1220                screenline = get_cell(self.xmax, 'L', line.stat['message'])
1221            else:
1222                screenline = get_screenline(self.mode, line.stat)
1223            self.addstr(self.smin + idx, self.xmin, screenline, attr)
1224
1225    def draw_mode(self):
1226        if self.mid == 0:
1227            self.help.draw()
1228        elif self.mid == 5 and self._pmid == self._cmid:
1229            self.cli.start() # initial mid was 5
1230        elif 0 < self.mid < 5:
1231            self.draw_stat()
1232
1233    def mvc(self, n):
1234        if n == 0:
1235            return
1236
1237        # Move DOWN
1238        if n > 0:
1239            # move cursor
1240            if self.cpos < self.cmax:
1241                self.cpos = min(self.cmax, self.cpos + n)
1242                return
1243            # move screenlines
1244            maxvmin = max(0, len(self.lines) - self.span - 1)
1245            if self.cpos == self.cmax and self.vmin < maxvmin:
1246                self.vmin = min(maxvmin, self.vmin + n)
1247
1248        # Move UP
1249        elif n < 0: # UP
1250            # move cursor
1251            if self.cpos > self.cmin:
1252                self.cpos = max(self.cmin, self.cpos + n)
1253                return
1254            # move screenlines
1255            if self.cpos == self.cmin and self.vmin > 0:
1256                self.vmin = max(0, self.vmin + n)
1257
1258    def mvhome(self):
1259        # move cursor
1260        if self.cpos != self.cmin:
1261            self.cpos = self.cmin
1262        # move screenlines
1263        if self.vmin != 0:
1264            self.vmin = 0
1265
1266    def mvend(self):
1267        # move cursor
1268        if self.cpos != self.cmax:
1269            self.cpos = self.cmax
1270        # move screenlines
1271        maxvmin = max(0, len(self.lines) - self.span - 1)
1272        if self.vmin != maxvmin:
1273            self.vmin = maxvmin
1274
1275
1276class ScreenHelp:
1277
1278    def __init__(self, screen):
1279        self.screen = screen
1280        self.xmin = screen.xmin + 1
1281        self.xmax = screen.xmax
1282        self.ymin = 0
1283        self.ymax = __doc__.count('\n')
1284        self.xpos = 0
1285        self.ypos = 0
1286
1287    def setup(self):
1288        self.pad = curses.newpad(self.ymax + 1, self.xmax + 1)
1289
1290    def addstr(self, *args, **kwargs):
1291        return self.pad.addstr(*args, **kwargs)
1292
1293    def refresh(self):
1294        self.pad.noutrefresh(
1295                self.ypos, self.xpos,
1296                self.screen.smin, self.xmin,
1297                self.screen.smax, self.xmax - 2)
1298
1299    def draw(self):
1300        self.addstr(0, 0, __doc__)
1301
1302    def mvc(self, n):
1303        if n == 0:
1304            return
1305
1306        # Move DOWN
1307        if n > 0:
1308            self.ypos = min(self.ymax - self.screen.span, self.ypos + n)
1309
1310        # Move UP
1311        elif n < 0:
1312            self.ypos = max(self.ymin, self.ypos + n)
1313
1314    def mvhome(self):
1315        self.ypos = self.ymin
1316
1317    def mvend(self):
1318        self.ypos = self.ymax - self.screen.span
1319
1320
1321class ScreenMode:
1322
1323    def __init__(self, name):
1324        self.name = name
1325        self.columns = []
1326
1327    @property
1328    def head(self):
1329        return get_head(self)
1330
1331    def sync(self, screen):
1332        for idx, column in enumerate(self.columns):
1333            column.width = get_width(column.minwidth, screen.xmax,
1334                    len(self.columns), idx)
1335
1336
1337class ScreenColumn:
1338
1339    def __init__(self, name, header, minwidth, maxwidth, align, filters={}):
1340        self.name = name
1341        self.header = header
1342        self.align = align
1343        self.minwidth = minwidth
1344        self.maxwidth = maxwidth
1345        self.width = minwidth
1346        self.filters = {'always': [], 'ondemand': []}
1347        self.filters.update(filters)
1348
1349    def get_width(self):
1350        return self._width
1351
1352    def set_width(self, n):
1353        if self.maxwidth:
1354            self._width = min(self.maxwidth, n)
1355        self._width = max(self.minwidth, n)
1356
1357    width = property(get_width, set_width)
1358
1359
1360class ScreenLine:
1361
1362    def __init__(self, stat=None, text='', attr=0):
1363        self.stat = stat
1364        self.text = text
1365        self.attr = attr
1366
1367
1368class StatusBar:
1369
1370    def __init__(self, width=60, min=0, max=100, status=True):
1371        self.width = width
1372        self.curval = min
1373        self.minval = min
1374        self.maxval = max
1375        self.status = status
1376        self.prepend = '['
1377        self.append = ']'
1378        self.usedchar = '|'
1379        self.freechar = ' '
1380
1381    def __str__(self):
1382        if self.status:
1383            status = '%d/%d' % (self.curval, self.maxval)
1384
1385        space = self.width - len(self.prepend) - len(self.append)
1386        span = self.maxval - self.minval
1387
1388        if span:
1389            used = min(float(self.curval) / float(span), 1.0)
1390        else:
1391            used = 0.0
1392        free = 1.0 - used
1393
1394        # 100% equals full bar width, ignoring status text within the bar
1395        bar  = self.prepend
1396        bar += self.usedchar * int(space * used)
1397        bar += self.freechar * int(space * free)
1398        if self.status:
1399            bar  = bar[:(self.width - len(status) - len(self.append))]
1400            bar += status
1401        bar += self.append
1402
1403        return bar
1404
1405    def update_cur(self, value):
1406        value = min(self.maxval, value)
1407        value = max(self.minval, value)
1408        self.curval = value
1409
1410    def update_max(self, value):
1411        if value >= self.minval:
1412            self.maxval = value
1413        else:
1414            self.maxval = self.minval
1415
1416# ------------------------------------------------------------------------- #
1417#                             DISPLAY FILTERS                               #
1418# ------------------------------------------------------------------------- #
1419
1420def human_seconds(numeric):
1421    for minval, prefix in sorted(list(PREFIX_TIME.items()), reverse=True):
1422        if (numeric/minval):
1423            return '%d%s' % (numeric/minval, prefix)
1424    return '%ds' % numeric
1425
1426def human_metric(numeric):
1427    for minval, prefix in sorted(list(PREFIX_METRIC.items()), reverse=True):
1428        if (numeric/minval):
1429            return '%d%s' % (numeric/minval, prefix)
1430    return str(numeric)
1431
1432def human_binary(numeric):
1433    for minval, prefix in sorted(list(PREFIX_BINARY.items()), reverse=True):
1434        if (numeric/minval):
1435            return '%.2f%s' % (float(numeric)/float(minval), prefix)
1436    return '%dB' % numeric
1437
1438def trim(string, length):
1439    if len(string) <= length:
1440        return string
1441    if length == 1:
1442        return string[0]
1443    if length > 5:
1444        return '..%s' % string[-(length-2):]
1445    return '...'
1446
1447# ------------------------------------------------------------------------- #
1448#                             SCREEN LAYOUT                                 #
1449# ------------------------------------------------------------------------- #
1450
1451SCREEN_MODES = [
1452        ScreenMode('HELP'),
1453        ScreenMode('STATUS'),
1454        ScreenMode('TRAFFIC'),
1455        ScreenMode('HTTP'),
1456        ScreenMode('ERRORS'),
1457        ScreenMode('CLI'),
1458]
1459
1460# Mode: HELP         name            header     xmin    xmax    align
1461SCREEN_MODES[0].columns = [
1462        ScreenColumn('help', ' HATop Online Help ',
1463                                         SCREEN_XMIN,      0,    'L'),
1464]
1465
1466# Mode: STATUS       name            header     xmin    xmax    align
1467SCREEN_MODES[1].columns = [
1468        ScreenColumn('svname',       'NAME',      10,     50,    'L'),
1469        ScreenColumn('weight',       'W',          4,      6,    'R'),
1470        ScreenColumn('status',       'STATUS',     6,     10,    'L'),
1471        ScreenColumn('check_status', 'CHECK',      7,     20,    'L'),
1472        ScreenColumn('act',          'ACT',        3,      0,    'R',
1473            filters={'ondemand': [human_metric]}),
1474        ScreenColumn('bck',          'BCK',        3,      0,    'R',
1475            filters={'ondemand': [human_metric]}),
1476        ScreenColumn('qcur',         'QCUR',       5,      0,    'R',
1477            filters={'ondemand': [human_metric]}),
1478        ScreenColumn('qmax',         'QMAX',       5,      0,    'R',
1479            filters={'ondemand': [human_metric]}),
1480        ScreenColumn('scur',         'SCUR',       6,      0,    'R',
1481            filters={'ondemand': [human_metric]}),
1482        ScreenColumn('smax',         'SMAX',       6,      0,    'R',
1483            filters={'ondemand': [human_metric]}),
1484        ScreenColumn('slim',         'SLIM',       6,      0,    'R',
1485            filters={'ondemand': [human_metric]}),
1486        ScreenColumn('stot',         'STOT',       6,      0,    'R',
1487            filters={'ondemand': [human_metric]}),
1488]
1489
1490# Mode: TRAFFIC      name            header     xmin    xmax    align
1491SCREEN_MODES[2].columns = [
1492        ScreenColumn('svname',       'NAME',      10,     50,    'L'),
1493        ScreenColumn('weight',       'W',          4,      6,    'R'),
1494        ScreenColumn('status',       'STATUS',     6,     10,    'L'),
1495        ScreenColumn('lbtot',        'LBTOT',      8,      0,    'R',
1496            filters={'ondemand': [human_metric]}),
1497        ScreenColumn('rate',         'RATE',       6,      0,    'R',
1498            filters={'ondemand': [human_metric]}),
1499        ScreenColumn('rate_lim',     'RLIM',       6,      0,    'R',
1500            filters={'ondemand': [human_metric]}),
1501        ScreenColumn('rate_max',     'RMAX',       6,      0,    'R',
1502            filters={'ondemand': [human_metric]}),
1503        ScreenColumn('bin',          'BIN',       12,      0,    'R',
1504            filters={'always':   [human_binary]}),
1505        ScreenColumn('bout',         'BOUT',      12,      0,    'R',
1506            filters={'always':   [human_binary]}),
1507]
1508
1509# Mode: HTTP         name            header     xmin    xmax    align
1510SCREEN_MODES[3].columns = [
1511        ScreenColumn('svname',       'NAME',      10,     50,    'L'),
1512        ScreenColumn('weight',       'W',          4,      6,    'R'),
1513        ScreenColumn('status',       'STATUS',     6,     10,    'L'),
1514        ScreenColumn('req_rate',     'RATE',       5,      0,    'R',
1515            filters={'ondemand': [human_metric]}),
1516        ScreenColumn('req_rate_max', 'RMAX',       5,      0,    'R',
1517            filters={'ondemand': [human_metric]}),
1518        ScreenColumn('req_tot',      'RTOT',       7,      0,    'R',
1519            filters={'ondemand': [human_metric]}),
1520        ScreenColumn('hrsp_1xx',     '1xx',        5,      0,    'R',
1521            filters={'ondemand': [human_metric]}),
1522        ScreenColumn('hrsp_2xx',     '2xx',        5,      0,    'R',
1523            filters={'ondemand': [human_metric]}),
1524        ScreenColumn('hrsp_3xx',     '3xx',        5,      0,    'R',
1525            filters={'ondemand': [human_metric]}),
1526        ScreenColumn('hrsp_4xx',     '4xx',        5,      0,    'R',
1527            filters={'ondemand': [human_metric]}),
1528        ScreenColumn('hrsp_5xx',     '5xx',        5,      0,    'R',
1529            filters={'ondemand': [human_metric]}),
1530        ScreenColumn('hrsp_other',   '?xx',        5,      0,    'R',
1531            filters={'ondemand': [human_metric]}),
1532]
1533
1534# Mode: ERRORS       name            header     xmin    xmax    align
1535SCREEN_MODES[4].columns = [
1536        ScreenColumn('svname',       'NAME',      10,     50,    'L'),
1537        ScreenColumn('weight',       'W',          4,      6,    'R'),
1538        ScreenColumn('status',       'STATUS',     6,     10,    'L'),
1539        ScreenColumn('check_status', 'CHECK',      7,     20,    'L'),
1540        ScreenColumn('chkfail',      'CF',         3,      0,    'R',
1541            filters={'ondemand': [human_metric]}),
1542        ScreenColumn('chkdown',      'CD',         3,      0,    'R',
1543            filters={'ondemand': [human_metric]}),
1544        ScreenColumn('lastchg',      'CL',         3,      0,    'R',
1545            filters={'always':   [human_seconds]}),
1546        ScreenColumn('econ',         'ECONN',      5,      0,    'R',
1547            filters={'ondemand': [human_metric]}),
1548        ScreenColumn('ereq',         'EREQ',       5,      0,    'R',
1549            filters={'ondemand': [human_metric]}),
1550        ScreenColumn('eresp',        'ERSP',       5,      0,    'R',
1551            filters={'ondemand': [human_metric]}),
1552        ScreenColumn('dreq',         'DREQ',       5,      0,    'R',
1553            filters={'ondemand': [human_metric]}),
1554        ScreenColumn('dresp',        'DRSP',       5,      0,    'R',
1555            filters={'ondemand': [human_metric]}),
1556        ScreenColumn('downtime',     'DOWN',       5,      0,    'R',
1557            filters={'always':   [human_seconds]}),
1558]
1559
1560# Mode: CLI          name            header     xmin    xmax    align
1561SCREEN_MODES[5].columns = [
1562        ScreenColumn('cli',
1563            ' haproxy command line                              '
1564            ' use ALT-n / ESC-n to escape',
1565                                         SCREEN_XMIN,      0,    'L'),
1566]
1567
1568# ------------------------------------------------------------------------- #
1569#                                HELPERS                                    #
1570# ------------------------------------------------------------------------- #
1571
1572def log(msg):
1573    sys.stderr.write('%s\n' % msg)
1574
1575def parse_stat(iterable):
1576    pxcount = svcount = 0
1577    pxstat = {} # {iid: {sid: svstat, ...}, ...}
1578
1579    idx_iid = get_idx('iid')
1580    idx_sid = get_idx('sid')
1581
1582    for line in iterable:
1583        if not line:
1584            continue
1585        if line.startswith(HAPROXY_STAT_COMMENT):
1586            continue # comment
1587        if line.count(HAPROXY_STAT_SEP) < HAPROXY_STAT_NUMFIELDS:
1588            continue # unknown format
1589
1590        csv = line.split(HAPROXY_STAT_SEP, HAPROXY_STAT_NUMFIELDS)
1591
1592        # Skip further parsing?
1593        if svcount > HAPROXY_STAT_MAX_SERVICES:
1594            try:
1595                iid = csv[idx_iid]
1596                iid = int(iid, 10)
1597            except ValueError:
1598                raise RuntimeError(
1599                        'garbage proxy identifier: iid="%s" (need %s)' %
1600                        (iid, int))
1601            try:
1602                sid = csv[idx_sid]
1603                sid = int(sid, 10)
1604            except ValueError:
1605                raise RuntimeError(
1606                        'garbage service identifier: sid="%s" (need %s)' %
1607                        (sid, int))
1608            if iid not in pxstat:
1609                pxcount += 1
1610                svcount += 1
1611            elif sid not in pxstat[iid]:
1612                svcount += 1
1613            continue
1614
1615        # Parse stat...
1616        svstat = {} # {field: value, ...}
1617
1618        for idx, field in HAPROXY_STAT_CSV:
1619            field_type, field_name = field
1620            value = csv[idx]
1621
1622            try:
1623                if field_type is int:
1624                    if len(value):
1625                        value = int(value, 10)
1626                    else:
1627                        value = 0
1628                elif field_type is not type(value):
1629                        value = field_type(value)
1630            except ValueError:
1631                raise RuntimeError('garbage field: %s="%s" (need %s)' % (
1632                        field_name, value, field_type))
1633
1634            # Special case
1635            if field_name == 'status' and value == 'no check':
1636                value = '-'
1637            elif field_name == 'check_status' and svstat['status'] == '-':
1638                value = 'none'
1639
1640            svstat[field_name] = value
1641
1642        # Record result...
1643        iid = svstat['iid']
1644        stype = svstat['type']
1645
1646        if stype == 0 or stype == 1:  # FRONTEND / BACKEND
1647            id = svstat['svname']
1648        else:
1649            id = svstat['sid']
1650
1651        try:
1652            pxstat[iid][id] = svstat
1653        except KeyError:
1654            pxstat[iid] = { id: svstat }
1655            pxcount += 1
1656        svcount += 1
1657
1658    return pxstat, pxcount, svcount
1659
1660def parse_info(iterable):
1661    info = {}
1662    for line in iterable:
1663        line = line.strip()
1664        if not line:
1665            continue
1666        for key, regexp in HAPROXY_INFO_RE.items():
1667            match = regexp.match(line)
1668            if match:
1669                info[key] = match.group('value')
1670                break
1671
1672    for key in HAPROXY_INFO_RE.keys():
1673        if not key in info:
1674            raise RuntimeError('missing "%s" in info data' % key)
1675
1676    return info
1677
1678def get_idx(field):
1679    return [x for x in HAPROXY_STAT_CSV if x[1][1] == field][0][0]
1680
1681def get_width(width, xmax, ncols, idx):
1682    # distribute excess space evenly from left to right
1683    if xmax > SCREEN_XMIN:
1684        xdiff = xmax - SCREEN_XMIN
1685        if xdiff <= ncols:
1686            if idx < xdiff:
1687                width += 1
1688        else:
1689            if idx < (xdiff - (xdiff / ncols) * ncols):
1690                width += 1 # compensate rounding
1691            width = width + xdiff // ncols
1692    return width
1693
1694def get_cell(width, align, value):
1695    s = str(value)
1696    if align == 'L':
1697        s = s.ljust(width)
1698    elif align == 'C':
1699        s = s.center(width)
1700    elif align == 'R':
1701        s = s.rjust(width)
1702    return s
1703
1704def get_head(mode):
1705    columns = []
1706    for column in mode.columns:
1707        s = column.header
1708        s = get_cell(column.width, column.align, s)
1709        columns.append(s)
1710    return ' '.join(columns)
1711
1712def get_screenlines(stat):
1713    screenlines = []
1714
1715    for iid, svstats in stat.items():
1716        lines = []
1717
1718        try:
1719            frontend = svstats.pop('FRONTEND')
1720        except KeyError:
1721            frontend = None
1722        try:
1723            backend = svstats.pop('BACKEND')
1724        except KeyError:
1725            backend = None
1726
1727        if frontend:
1728            lines.append(ScreenLine(stat=frontend))
1729
1730        for sid, svstat in sorted(svstats.items()):
1731            lines.append(ScreenLine(stat=svstat))
1732
1733        if backend:
1734            lines.append(ScreenLine(stat=backend))
1735
1736        if not len(lines):
1737            continue
1738
1739        pxname = lines[0].stat['pxname']
1740        screenlines.append(ScreenLine(attr=curses.A_BOLD,
1741            text='>>> %s' % pxname))
1742        screenlines += lines
1743        screenlines.append(ScreenLine())
1744
1745    # remove trailing empty line
1746    if len(screenlines) > 1:
1747        screenlines.pop()
1748
1749    return screenlines
1750
1751def get_screenline(mode, stat):
1752    cells = []
1753    for column in mode.columns:
1754        value = stat[column.name]
1755
1756        for filter in column.filters['always']:
1757            value = list(filter(value))
1758
1759        if len(str(value)) > column.width:
1760            for filter in column.filters['ondemand']:
1761                value = list(filter(value))
1762
1763        value = str(value)
1764        value = trim(value, column.width)
1765        cells.append(get_cell(column.width, column.align, value))
1766
1767    return ' '.join(cells)
1768
1769# ------------------------------------------------------------------------- #
1770#                            CURSES HELPERS                                 #
1771# ------------------------------------------------------------------------- #
1772
1773def curses_init():
1774    screen = curses.initscr()
1775    curses.noecho()
1776    curses.nonl()
1777    curses.raw()
1778
1779    # Some terminals don't support different cursor visibilities
1780    try:
1781        curses.curs_set(0)
1782    except CursesError:
1783        pass
1784
1785    # Some terminals don't support the default color palette
1786    try:
1787        curses.start_color()
1788        curses.use_default_colors()
1789    except CursesError:
1790        pass
1791
1792    return screen
1793
1794def curses_reset(screen):
1795    if not screen:
1796        return
1797    screen.keypad(0)
1798    curses.noraw()
1799    curses.echo()
1800    curses.endwin()
1801
1802# ------------------------------------------------------------------------- #
1803#                               MAIN LOOP                                   #
1804# ------------------------------------------------------------------------- #
1805
1806def mainloop(screen, interval):
1807    # Sleep time of each iteration in seconds
1808    scan = 1.0 / 100.0
1809    # Query socket and redraw the screen in the given interval
1810    iterations = interval / scan
1811
1812    i = 0
1813    update_stat = True      # Toggle stat update (query socket, parse csv)
1814    update_lines = True     # Toggle screen line update (sync with stat data)
1815    update_display = True   # Toggle screen update (resize, repaint, refresh)
1816    switch_mode = False     # Toggle mode / viewport switch
1817
1818    while True:
1819
1820        # Resize toggled by SIGWINCH?
1821        if screen.resized:
1822            screen.resize()
1823            update_display = True
1824
1825        # Update interval reached...
1826        if i == iterations:
1827            update_stat = True
1828            if 0 < screen.mid < 5:
1829                update_lines = True
1830            update_display = True
1831            i = 0
1832
1833        # Refresh screen?
1834        if update_display:
1835
1836            if update_stat:
1837                screen.update_data()
1838                screen.update_bars()
1839                update_stat = False
1840
1841            if update_lines:
1842                screen.update_lines()
1843                update_lines = False
1844
1845            screen.erase()
1846            screen.draw_head()
1847            screen.draw_info()
1848            screen.draw_cols()
1849            screen.draw_mode()
1850            screen.draw_foot()
1851            screen.refresh()
1852
1853            update_display = False
1854
1855        c = screen.getch()
1856
1857        if c < 0:
1858            time.sleep(scan)
1859            i += 1
1860            continue
1861
1862        # Toggle hotkey footer
1863        if screen.hotkeys:
1864            if c not in (
1865                    curses.KEY_F1,          # enable server on all backends
1866                    curses.KEY_F2,          # disable server on all backends
1867                    curses.KEY_F4,          # reset weight
1868                    curses.KEY_F5,          # weight -10
1869                    curses.KEY_F6,          # weight -1
1870                    curses.KEY_F7,          # weight +1
1871                    curses.KEY_F8,          # weight +10
1872                    curses.KEY_F9,          # enable server on one backend
1873                    curses.KEY_F10,         # disable server on one backend
1874            ):
1875                screen.hotkeys = False
1876                update_display = True
1877            if c in (
1878                    curses.KEY_ENTER,
1879                    curses.ascii.CR
1880            ):
1881                continue
1882
1883        if c == curses.ascii.ETX:
1884            raise KeyboardInterrupt()
1885
1886        # Mode switch (ALT-n / ESC-n) or toggle (ESC / ESC-ESC)
1887        if c == curses.ascii.ESC:
1888            c = screen.getch()
1889            if c < 0 or c == curses.ascii.ESC:
1890                screen.toggle_mode()
1891                update_display = True
1892                continue
1893            if 0 < c < 256:
1894                c = chr(c)
1895                if c in 'qQHh?12345':
1896                    switch_mode = True
1897
1898        # Mode cycle (TAB / BTAB)
1899        elif c == ord('\t'):
1900            screen.cycle_mode(1)
1901            update_display = True
1902            continue
1903        elif c == curses.KEY_BTAB:
1904            screen.cycle_mode(-1)
1905            update_display = True
1906            continue
1907
1908        # Mode switch in non-CLI modes using the number only
1909        elif 0 <= screen.mid < 5 and 0 < c < 256:
1910            c = chr(c)
1911            if c in 'qQHh?12345':
1912                switch_mode = True
1913
1914        if switch_mode:
1915            switch_mode = False
1916            if c in 'qQ':
1917                    raise StopIteration()
1918            if c != str(screen.mid) or (c in 'Hh?' and screen.mid != 0):
1919                if c in 'Hh?':
1920                    screen.switch_mode(0)
1921                elif c in '12345':
1922                    screen.switch_mode(int(c))
1923
1924                # Force screen update with existing data
1925                update_display = True
1926                continue
1927
1928        # -> HELP
1929        if screen.mid == 0:
1930            if c == curses.KEY_UP and screen.help.ypos > 0:
1931                screen.help.mvc(-1)
1932            elif c == curses.KEY_DOWN and \
1933                    screen.help.ypos < screen.help.ymax - screen.span:
1934                screen.help.mvc(1)
1935            elif c == curses.KEY_PPAGE and screen.help.ypos > 0:
1936                screen.help.mvc(-10)
1937            elif c == curses.KEY_NPAGE and \
1938                    screen.help.ypos < screen.help.ymax - screen.span:
1939                screen.help.mvc(10)
1940            elif c == curses.ascii.SOH or c == curses.KEY_HOME:
1941                screen.help.mvhome()
1942            elif c == curses.ascii.ENQ or c == curses.KEY_END:
1943                screen.help.mvend()
1944
1945        # -> STATUS / TRAFFIC / HTTP / ERRORS
1946        elif 1 <= screen.mid <= 4:
1947
1948            # movements
1949            if c == curses.KEY_UP:
1950                screen.mvc(-1)
1951            elif c == curses.KEY_DOWN:
1952                screen.mvc(1)
1953            elif c == curses.KEY_PPAGE:
1954                screen.mvc(-10)
1955            elif c == curses.KEY_NPAGE:
1956                screen.mvc(10)
1957            elif c == curses.ascii.SOH or c == curses.KEY_HOME:
1958                screen.mvhome()
1959            elif c == curses.ascii.ENQ or c == curses.KEY_END:
1960                screen.mvend()
1961
1962            # actions
1963            elif c in (
1964                    curses.KEY_ENTER,       # show hotkeys
1965                    chr(curses.ascii.CR),   # show hotkeys
1966                    chr(curses.ascii.SP),   # copy & paste identifier
1967                    curses.KEY_F1,          # enable server on all backends
1968                    curses.KEY_F2,          # disable server on all backends
1969                    curses.KEY_F4,          # reset weight
1970                    curses.KEY_F5,          # weight -10
1971                    curses.KEY_F6,          # weight -1
1972                    curses.KEY_F7,          # weight +1
1973                    curses.KEY_F8,          # weight +10
1974                    curses.KEY_F9,          # enable server on one backend
1975                    curses.KEY_F10,         # disable server on one backend
1976            ):
1977                if screen.data.socket.ro:
1978                    continue
1979                if not screen.cstat:
1980                    continue
1981
1982                if c == curses.KEY_ENTER or c == chr(curses.ascii.CR):
1983                    screen.hotkeys = True
1984                    update_display = True
1985                    continue
1986
1987                iid = screen.cstat['iid']
1988                sid = screen.cstat['sid']
1989
1990                if iid <= 0 or sid <= 0:
1991                    continue
1992
1993                pxname = screen.cstat['pxname']
1994                svname = screen.cstat['svname']
1995
1996                if not pxname or not svname:
1997                    continue
1998
1999                notify = False
2000
2001                if c == ' ':
2002                    if screen.cli.puts('%s/%s' % (pxname, svname)):
2003                        screen.switch_mode(5)
2004                        update_display = True
2005                        continue
2006                elif c == curses.KEY_F1 or c == curses.KEY_F2:
2007                    cliname = 'enable server'
2008                    if c == curses.KEY_F2:
2009                        cliname = 'disable server'
2010
2011                    # loop on all proxies (backends)
2012                    for px in screen.data.stat.values():
2013                        # in a given proxy, loop on all services id (unique inside a proxy)
2014                        # a service can be a frontend, backend, server or socket object
2015                        for sv in px.values():
2016                            if sv['type'] == 2: # only the type "server" interests us
2017                                # selects only the service name (hostname)
2018                                # which matches that of the current line
2019                                if sv['svname'] == svname:
2020                                    screen.cli.execute_cmdline(
2021                                            '%s %s/%s' % (cliname, sv['pxname'], svname))
2022                                    notify = True
2023                elif c == curses.KEY_F4:
2024                    screen.cli.execute_cmdline(
2025                            'set weight %s/%s 100%%' % (pxname, svname))
2026                    notify = True
2027                elif c == curses.KEY_F5:
2028                    curweight = screen.cstat['weight']
2029                    if curweight <= 0:
2030                        continue
2031                    weight = max(0, curweight - 10)             # - 10
2032                    screen.cli.execute_cmdline(
2033                            'set weight %s/%s %d' % (pxname, svname, weight))
2034                    notify = True
2035                elif c == curses.KEY_F6:
2036                    curweight = screen.cstat['weight']
2037                    if curweight <= 0:
2038                        continue
2039                    weight = max(0, curweight - 1)              # - 1
2040                    screen.cli.execute_cmdline(
2041                            'set weight %s/%s %d' % (pxname, svname, weight))
2042                    notify = True
2043                elif c == curses.KEY_F7:
2044                    curweight = screen.cstat['weight']
2045                    if curweight >= 256:
2046                        continue
2047                    weight = min(256, curweight + 1)            # + 1
2048                    screen.cli.execute_cmdline(
2049                            'set weight %s/%s %d' % (pxname, svname, weight))
2050                    notify = True
2051                elif c == curses.KEY_F8:
2052                    curweight = screen.cstat['weight']
2053                    if curweight >= 256:
2054                        continue
2055                    weight = min(256, curweight + 10)           # + 10
2056                    screen.cli.execute_cmdline(
2057                            'set weight %s/%s %d' % (pxname, svname, weight))
2058                    notify = True
2059                elif c == curses.KEY_F9:
2060                    screen.cli.execute_cmdline(
2061                            'enable server %s/%s' % (pxname, svname))
2062                    notify = True
2063                elif c == curses.KEY_F10:
2064                    screen.cli.execute_cmdline(
2065                            'disable server %s/%s' % (pxname, svname))
2066                    notify = True
2067
2068                # Refresh the screen indicating pending changes...
2069                if notify:
2070                    screen.cstat['message'] = 'updating...'
2071                    update_display = True
2072                    continue
2073
2074        # -> CLI
2075        elif screen.mid == 5:
2076
2077            # enter
2078            if c == curses.KEY_ENTER or c == curses.ascii.CR:
2079                screen.cli.execute()
2080
2081            # input movements
2082            elif c == curses.KEY_LEFT:
2083                screen.cli.mvc(-1)
2084            elif c == curses.KEY_RIGHT:
2085                screen.cli.mvc(1)
2086            elif c == curses.ascii.SOH or c == curses.KEY_HOME:
2087                screen.cli.mvhome()
2088            elif c == curses.ascii.ENQ or c == curses.KEY_END:
2089                screen.cli.mvend()
2090
2091            # input editing
2092            elif c == curses.ascii.ETB:
2093                pass # TODO (CTRL-W)
2094            elif c == curses.KEY_DC:
2095                screen.cli.delc(1)
2096            elif c == curses.KEY_BACKSPACE or c == curses.ascii.DEL:
2097                screen.cli.delc(-1)
2098
2099            # input history
2100            elif c == curses.KEY_UP:
2101                screen.cli.prev()
2102            elif c == curses.KEY_DOWN:
2103                next(screen.cli)
2104
2105            # output history
2106            elif c == curses.KEY_PPAGE:
2107                screen.cli.mvo(-1)
2108            elif c == curses.KEY_NPAGE:
2109                screen.cli.mvo(1)
2110
2111            elif 0 < c < 256:
2112                screen.cli.putc(chr(c))
2113
2114        # Force screen update with existing data if key was a movement key
2115        if c in (
2116            curses.KEY_UP,
2117            curses.KEY_DOWN,
2118            curses.KEY_PPAGE,
2119            curses.KEY_NPAGE,
2120        ) or screen.mid != 5 and c in (
2121            curses.ascii.SOH, curses.KEY_HOME,
2122            curses.ascii.ENQ, curses.KEY_END,
2123        ):
2124            update_display = True
2125
2126        time.sleep(scan)
2127        i += 1
2128
2129
2130if __name__ == '__main__':
2131
2132    from optparse import OptionParser, OptionGroup
2133
2134    version  = 'hatop version %s' % __version__
2135    usage    = 'Usage: hatop (-s SOCKET| -t HOST:PORT) [OPTIONS]...'
2136
2137    parser = OptionParser(usage=usage, version=version)
2138
2139    opts = OptionGroup(parser, 'Mandatory')
2140    opts.add_option('-s', '--unix-socket', dest='socket',
2141            help='path to the haproxy unix stats socket')
2142    opts.add_option('-t', '--tcp-socket', dest='tcp_socket',
2143            help='address of the haproxy tcp stats socket')
2144    parser.add_option_group(opts)
2145
2146    opts = OptionGroup(parser, 'Optional')
2147    opts.add_option('-i', '--update-interval', type='int', dest='interval',
2148            help='update interval in seconds (1-30, default: 3)', default=3)
2149    opts.add_option('-m', '--mode', type='int', dest='mode',
2150            help='start in specific mode (1-5, default: 1)', default=1)
2151    opts.add_option('-n', '--read-only', action='store_true', dest='ro',
2152            help='disable the cli and query for stats only')
2153    parser.add_option_group(opts)
2154
2155    opts = OptionGroup(parser, 'Filters',
2156            'Note: All filter options may be given multiple times.')
2157    opts.add_option('-f', '--filter', action='append', dest='stat_filter',
2158            default=[], metavar='FILTER',
2159            help='stat filter in format "<iid> <type> <sid>"')
2160    opts.add_option('-p', '--proxy', action='append', dest='proxy_filter',
2161            default=[], metavar='PROXY',
2162            help='proxy filter in format "<pxname>"')
2163    parser.add_option_group(opts)
2164
2165    opts, args = parser.parse_args()
2166
2167    if not 1 <= opts.interval <= 30:
2168        log('invalid update interval: %d' % opts.interval)
2169        sys.exit(1)
2170    if not 1 <= opts.mode <= 5:
2171        log('invalid mode: %d' % opts.mode)
2172        sys.exit(1)
2173    if len(opts.stat_filter) + len(opts.proxy_filter) > 50:
2174        log('filter limit exceeded (50)')
2175        sys.exit(1)
2176    if opts.ro and opts.mode == 5:
2177        log('cli not available in read-only mode')
2178        sys.exit(1)
2179    if opts.socket and opts.tcp_socket:
2180        log('Specify either --unix-socket OR --tcp-socket')
2181        parser.print_help()
2182        sys.exit(1)
2183    if not opts.socket and not opts.tcp_socket:
2184        parser.print_help()
2185        sys.exit(0)
2186    if opts.socket and not os.access(opts.socket, os.R_OK | os.W_OK):
2187        log('insufficient permissions for socket path %s' % opts.socket)
2188        sys.exit(2)
2189
2190    if opts.socket:
2191        socket = Socket(opts.socket, opts.ro)
2192    else:
2193        socket = Socket(opts.tcp_socket, opts.ro, True)
2194
2195    data = SocketData(socket)
2196    screen = Screen(data, opts.mode)
2197
2198    signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit(0))
2199
2200    try:
2201        try:
2202            socket.connect()
2203            screen.setup()
2204
2205            # Register filters
2206            data.register_stat_filter(opts.stat_filter)
2207            data.register_proxy_filter(opts.proxy_filter)
2208
2209            while True:
2210                try:
2211                    mainloop(screen, opts.interval)
2212                except StopIteration:
2213                    break
2214                except KeyboardInterrupt:
2215                    break
2216                except CursesError as e:
2217                    screen.reset()
2218                    log('curses error: %s, restarting...' % e)
2219                    time.sleep(1)
2220                    screen.recover()
2221
2222        except ValueError as e:
2223            screen.reset()
2224            log('value error: %s' % e)
2225            sys.exit(1)
2226        except RuntimeError as e:
2227            screen.reset()
2228            log('runtime error: %s' % e)
2229            sys.exit(1)
2230        except SocketError as e:
2231            screen.reset()
2232            log('socket error: %s' % e)
2233            sys.exit(2)
2234
2235    finally:
2236        screen.reset()
2237        socket.close()
2238
2239    sys.exit(0)
2240
2241# vim: et sw=4 sts=4 ts=4 tw=78 fdn=1 fdm=indent
2242