]> git.proxmox.com Git - ceph.git/blame - ceph/src/tools/cephfs/top/cephfs-top
import quincy beta 17.1.0
[ceph.git] / ceph / src / tools / cephfs / top / cephfs-top
CommitLineData
f67539c2
TL
1#!/usr/bin/python3
2
3import argparse
4import sys
5import curses
6import errno
7import json
8import signal
a4b75251 9import time
f67539c2
TL
10
11from collections import OrderedDict
12from datetime import datetime
13from enum import Enum, unique
14from threading import Event
15
16import rados
17
18
19class FSTopException(Exception):
20 def __init__(self, msg=''):
21 self.error_msg = msg
22
23 def get_error_msg(self):
24 return self.error_msg
25
26
27@unique
28class MetricType(Enum):
29 METRIC_TYPE_NONE = 0
30 METRIC_TYPE_PERCENTAGE = 1
31 METRIC_TYPE_LATENCY = 2
a4b75251 32 METRIC_TYPE_SIZE = 3
f67539c2
TL
33
34
35FS_TOP_PROG_STR = 'cephfs-top'
36
37# version match b/w fstop and stats emitted by mgr/stats
38FS_TOP_SUPPORTED_VER = 1
39
40ITEMS_PAD_LEN = 1
b3b6e05e 41ITEMS_PAD = " " * ITEMS_PAD_LEN
f67539c2
TL
42DEFAULT_REFRESH_INTERVAL = 1
43# min refresh interval allowed
44MIN_REFRESH_INTERVAL = 0.5
45
46# metadata provided by mgr/stats
b3b6e05e
TL
47FS_TOP_MAIN_WINDOW_COL_CLIENT_ID = "client_id"
48FS_TOP_MAIN_WINDOW_COL_MNT_ROOT = "mount_root"
49FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR = "mount_point@host/addr"
f67539c2
TL
50
51MAIN_WINDOW_TOP_LINE_ITEMS_START = [ITEMS_PAD,
52 FS_TOP_MAIN_WINDOW_COL_CLIENT_ID,
53 FS_TOP_MAIN_WINDOW_COL_MNT_ROOT]
54MAIN_WINDOW_TOP_LINE_ITEMS_END = [FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR]
55
56# adjust this map according to stats version and maintain order
57# as emitted by mgr/stast
58MAIN_WINDOW_TOP_LINE_METRICS = OrderedDict([
59 ("CAP_HIT", MetricType.METRIC_TYPE_PERCENTAGE),
60 ("READ_LATENCY", MetricType.METRIC_TYPE_LATENCY),
61 ("WRITE_LATENCY", MetricType.METRIC_TYPE_LATENCY),
62 ("METADATA_LATENCY", MetricType.METRIC_TYPE_LATENCY),
63 ("DENTRY_LEASE", MetricType.METRIC_TYPE_PERCENTAGE),
64 ("OPENED_FILES", MetricType.METRIC_TYPE_NONE),
65 ("PINNED_ICAPS", MetricType.METRIC_TYPE_NONE),
66 ("OPENED_INODES", MetricType.METRIC_TYPE_NONE),
a4b75251
TL
67 ("READ_IO_SIZES", MetricType.METRIC_TYPE_SIZE),
68 ("WRITE_IO_SIZES", MetricType.METRIC_TYPE_SIZE),
f67539c2
TL
69])
70MGR_STATS_COUNTERS = list(MAIN_WINDOW_TOP_LINE_METRICS.keys())
71
72FS_TOP_VERSION_HEADER_FMT = '{prog_name} - {now}'
73FS_TOP_CLIENT_HEADER_FMT = 'Client(s): {num_clients} - {num_mounts} FUSE, '\
74 '{num_kclients} kclient, {num_libs} libcephfs'
75
76CLIENT_METADATA_KEY = "client_metadata"
77CLIENT_METADATA_MOUNT_POINT_KEY = "mount_point"
78CLIENT_METADATA_MOUNT_ROOT_KEY = "root"
79CLIENT_METADATA_IP_KEY = "IP"
80CLIENT_METADATA_HOSTNAME_KEY = "hostname"
81CLIENT_METADATA_VALID_METRICS_KEY = "valid_metrics"
82
83GLOBAL_METRICS_KEY = "global_metrics"
84GLOBAL_COUNTERS_KEY = "global_counters"
85
a4b75251
TL
86last_time = time.time()
87last_read_size = {}
88last_write_size = {}
89
f67539c2
TL
90
91def calc_perc(c):
92 if c[0] == 0 and c[1] == 0:
93 return 0.0
94 return round((c[0] / (c[0] + c[1])) * 100, 2)
95
96
97def calc_lat(c):
98 return round(c[0] + c[1] / 1000000000, 2)
99
100
a4b75251
TL
101# in MB
102def calc_size(c):
103 return round(c[1] / (1024 * 1024), 2)
104
105
106# in MB
107def calc_avg_size(c):
108 if c[0] == 0:
109 return 0.0
110 return round(c[1] / (c[0] * 1024 * 1024), 2)
111
112
113# in MB/s
114def calc_speed(size, duration):
115 if duration == 0:
116 return 0.0
117 return round(size / (duration * 1024 * 1024), 2)
118
119
f67539c2
TL
120def wrap(s, sl):
121 """return a '+' suffixed wrapped string"""
122 if len(s) < sl:
123 return s
124 return f'{s[0:sl-1]}+'
125
126
127class FSTop(object):
128 def __init__(self, args):
129 self.rados = None
130 self.stdscr = None # curses instance
131 self.client_name = args.id
132 self.cluster_name = args.cluster
133 self.conffile = args.conffile
134 self.refresh_interval_secs = args.delay
135 self.exit_ev = Event()
136
b3b6e05e
TL
137 def refresh_window_size(self):
138 self.height, self.width = self.stdscr.getmaxyx()
139
f67539c2
TL
140 def handle_signal(self, signum, _):
141 self.exit_ev.set()
142
143 def init(self):
144 try:
145 if self.conffile:
146 r_rados = rados.Rados(rados_id=self.client_name, clustername=self.cluster_name,
147 conffile=self.conffile)
148 else:
149 r_rados = rados.Rados(rados_id=self.client_name, clustername=self.cluster_name)
150 r_rados.conf_read_file()
151 r_rados.connect()
152 self.rados = r_rados
153 except rados.Error as e:
154 if e.errno == errno.ENOENT:
155 raise FSTopException(f'cluster {self.cluster_name} does not exist')
156 else:
157 raise FSTopException(f'error connecting to cluster: {e}')
158 self.verify_perf_stats_support()
159 signal.signal(signal.SIGTERM, self.handle_signal)
160 signal.signal(signal.SIGINT, self.handle_signal)
161
162 def fini(self):
163 if self.rados:
164 self.rados.shutdown()
165 self.rados = None
166
167 def selftest(self):
168 stats_json = self.perf_stats_query()
169 if not stats_json['version'] == FS_TOP_SUPPORTED_VER:
170 raise FSTopException('perf stats version mismatch!')
171 missing = [m for m in stats_json["global_counters"] if m.upper() not in MGR_STATS_COUNTERS]
172 if missing:
173 raise FSTopException('Cannot handle unknown metrics from \'ceph fs perf stats\': '
174 f'{missing}')
175
b3b6e05e
TL
176 def setup_curses(self, win):
177 self.stdscr = win
178 curses.use_default_colors()
179 curses.start_color()
180 try:
181 curses.curs_set(0)
182 except curses.error:
183 # If the terminal do not support the visibility
184 # requested it will raise an exception
185 pass
186 self.run_display()
f67539c2
TL
187
188 def verify_perf_stats_support(self):
189 mon_cmd = {'prefix': 'mgr module ls', 'format': 'json'}
190 try:
191 ret, buf, out = self.rados.mon_command(json.dumps(mon_cmd), b'')
192 except Exception as e:
193 raise FSTopException(f'error checking \'stats\' module: {e}')
194 if ret != 0:
195 raise FSTopException(f'error checking \'stats\' module: {out}')
196 if 'stats' not in json.loads(buf.decode('utf-8'))['enabled_modules']:
197 raise FSTopException('\'stats\' module not enabled. Use \'ceph mgr module '
198 'enable stats\' to enable')
199
200 def perf_stats_query(self):
201 mgr_cmd = {'prefix': 'fs perf stats', 'format': 'json'}
202 try:
203 ret, buf, out = self.rados.mgr_command(json.dumps(mgr_cmd), b'')
204 except Exception as e:
205 raise FSTopException(f'error in \'perf stats\' query: {e}')
206 if ret != 0:
207 raise FSTopException(f'error in \'perf stats\' query: {out}')
208 return json.loads(buf.decode('utf-8'))
209
b3b6e05e
TL
210 def items(self, item):
211 if item == "CAP_HIT":
212 return "chit"
213 if item == "READ_LATENCY":
214 return "rlat"
215 if item == "WRITE_LATENCY":
216 return "wlat"
217 if item == "METADATA_LATENCY":
218 return "mlat"
219 if item == "DENTRY_LEASE":
220 return "dlease"
221 if item == "OPENED_FILES":
222 return "ofiles"
223 if item == "PINNED_ICAPS":
224 return "oicaps"
225 if item == "OPENED_INODES":
226 return "oinodes"
a4b75251
TL
227 if item == "READ_IO_SIZES":
228 return "rtio"
229 if item == "WRITE_IO_SIZES":
230 return "wtio"
b3b6e05e
TL
231 else:
232 # return empty string for none type
233 return ''
234
f67539c2
TL
235 def mtype(self, typ):
236 if typ == MetricType.METRIC_TYPE_PERCENTAGE:
237 return "(%)"
238 elif typ == MetricType.METRIC_TYPE_LATENCY:
239 return "(s)"
a4b75251
TL
240 elif typ == MetricType.METRIC_TYPE_SIZE:
241 return "(MB)"
242 else:
243 # return empty string for none type
244 return ''
245
246 def avg_items(self, item):
247 if item == "READ_IO_SIZES":
248 return "raio"
249 if item == "WRITE_IO_SIZES":
250 return "waio"
251 else:
252 # return empty string for none type
253 return ''
254
255 def speed_items(self, item):
256 if item == "READ_IO_SIZES":
257 return "rsp"
258 if item == "WRITE_IO_SIZES":
259 return "wsp"
260 else:
261 # return empty string for none type
262 return ''
263
264 def speed_mtype(self, typ):
265 if typ == MetricType.METRIC_TYPE_SIZE:
266 return "(MB/s)"
f67539c2
TL
267 else:
268 # return empty string for none type
269 return ''
270
271 def refresh_top_line_and_build_coord(self):
b3b6e05e
TL
272 if self.topl is None:
273 return
274
f67539c2
TL
275 xp = 0
276 x_coord_map = {}
277
278 heading = []
279 for item in MAIN_WINDOW_TOP_LINE_ITEMS_START:
280 heading.append(item)
281 nlen = len(item) + len(ITEMS_PAD)
282 x_coord_map[item] = (xp, nlen)
283 xp += nlen
284
285 for item, typ in MAIN_WINDOW_TOP_LINE_METRICS.items():
b3b6e05e 286 it = f'{self.items(item)}{self.mtype(typ)}'
f67539c2
TL
287 heading.append(it)
288 nlen = len(it) + len(ITEMS_PAD)
289 x_coord_map[item] = (xp, nlen)
290 xp += nlen
291
a4b75251
TL
292 if item == "READ_IO_SIZES" or item == "WRITE_IO_SIZES":
293 # average io sizes
294 it = f'{self.avg_items(item)}{self.mtype(typ)}'
295 heading.append(it)
296 nlen = len(it) + len(ITEMS_PAD)
297 if item == "READ_IO_SIZES":
298 x_coord_map["READ_IO_AVG"] = (xp, nlen)
299 if item == "WRITE_IO_SIZES":
300 x_coord_map["WRITE_IO_AVG"] = (xp, nlen)
301 xp += nlen
302
303 # io speeds
304 it = f'{self.speed_items(item)}{self.speed_mtype(typ)}'
305 heading.append(it)
306 nlen = len(it) + len(ITEMS_PAD)
307 if item == "READ_IO_SIZES":
308 x_coord_map["READ_IO_SPEED"] = (xp, nlen)
309 if item == "WRITE_IO_SIZES":
310 x_coord_map["WRITE_IO_SPEED"] = (xp, nlen)
311 xp += nlen
312
f67539c2
TL
313 for item in MAIN_WINDOW_TOP_LINE_ITEMS_END:
314 heading.append(item)
315 nlen = len(item) + len(ITEMS_PAD)
316 x_coord_map[item] = (xp, nlen)
317 xp += nlen
b3b6e05e
TL
318 title = ITEMS_PAD.join(heading)
319 hlen = min(self.width - 2, len(title))
320 self.topl.addnstr(0, 0, title, hlen, curses.A_STANDOUT | curses.A_BOLD)
321 self.topl.refresh()
f67539c2
TL
322 return x_coord_map
323
324 @staticmethod
325 def has_metric(metadata, metrics_key):
326 return metrics_key in metadata
327
328 @staticmethod
329 def has_metrics(metadata, metrics_keys):
330 for key in metrics_keys:
331 if not FSTop.has_metric(metadata, key):
332 return False
333 return True
334
335 def refresh_client(self, client_id, metrics, counters, client_meta, x_coord_map, y_coord):
a4b75251
TL
336 global last_time
337 cur_time = time.time()
338 duration = cur_time - last_time
339 last_time = cur_time
b3b6e05e 340 remaining_hlen = self.width - 1
f67539c2
TL
341 for item in MAIN_WINDOW_TOP_LINE_ITEMS_START:
342 coord = x_coord_map[item]
343 hlen = coord[1] - len(ITEMS_PAD)
b3b6e05e
TL
344 hlen = min(hlen, remaining_hlen)
345 if remaining_hlen < coord[1]:
346 remaining_hlen = 0
347 else:
348 remaining_hlen -= coord[1]
f67539c2 349 if item == FS_TOP_MAIN_WINDOW_COL_CLIENT_ID:
b3b6e05e
TL
350 self.mainw.addnstr(y_coord, coord[0],
351 wrap(client_id.split('.')[1], hlen),
352 hlen)
f67539c2
TL
353 elif item == FS_TOP_MAIN_WINDOW_COL_MNT_ROOT:
354 if FSTop.has_metric(client_meta, CLIENT_METADATA_MOUNT_ROOT_KEY):
b3b6e05e
TL
355 self.mainw.addnstr(y_coord, coord[0],
356 wrap(client_meta[CLIENT_METADATA_MOUNT_ROOT_KEY], hlen),
357 hlen)
f67539c2 358 else:
b3b6e05e
TL
359 self.mainw.addnstr(y_coord, coord[0], "N/A", hlen)
360
361 if remaining_hlen == 0:
362 return
363
f67539c2 364 cidx = 0
a4b75251 365 client_id = x_coord_map[FS_TOP_MAIN_WINDOW_COL_CLIENT_ID]
f67539c2
TL
366 for item in counters:
367 coord = x_coord_map[item]
b3b6e05e
TL
368 hlen = coord[1] - len(ITEMS_PAD)
369 hlen = min(hlen, remaining_hlen)
370 if remaining_hlen < coord[1]:
371 remaining_hlen = 0
372 else:
373 remaining_hlen -= coord[1]
f67539c2 374 m = metrics[cidx]
a4b75251
TL
375 key = MGR_STATS_COUNTERS[cidx]
376 typ = MAIN_WINDOW_TOP_LINE_METRICS[key]
f67539c2
TL
377 if item.lower() in client_meta.get(CLIENT_METADATA_VALID_METRICS_KEY, []):
378 if typ == MetricType.METRIC_TYPE_PERCENTAGE:
b3b6e05e 379 self.mainw.addnstr(y_coord, coord[0], f'{calc_perc(m)}', hlen)
f67539c2 380 elif typ == MetricType.METRIC_TYPE_LATENCY:
b3b6e05e 381 self.mainw.addnstr(y_coord, coord[0], f'{calc_lat(m)}', hlen)
a4b75251
TL
382 elif typ == MetricType.METRIC_TYPE_SIZE:
383 self.mainw.addnstr(y_coord, coord[0], f'{calc_size(m)}', hlen)
384
385 # average io sizes
386 if remaining_hlen == 0:
387 return
388 if key == "READ_IO_SIZES":
389 coord = x_coord_map["READ_IO_AVG"]
390 elif key == "WRITE_IO_SIZES":
391 coord = x_coord_map["WRITE_IO_AVG"]
392 hlen = coord[1] - len(ITEMS_PAD)
393 hlen = min(hlen, remaining_hlen)
394 if remaining_hlen < coord[1]:
395 remaining_hlen = 0
396 else:
397 remaining_hlen -= coord[1]
398 self.mainw.addnstr(y_coord, coord[0], f'{calc_avg_size(m)}', hlen)
399
400 # io speeds
401 if remaining_hlen == 0:
402 return
403 if key == "READ_IO_SIZES":
404 coord = x_coord_map["READ_IO_SPEED"]
405 elif key == "WRITE_IO_SIZES":
406 coord = x_coord_map["WRITE_IO_SPEED"]
407 hlen = coord[1] - len(ITEMS_PAD)
408 hlen = min(hlen, remaining_hlen)
409 if remaining_hlen < coord[1]:
410 remaining_hlen = 0
411 else:
412 remaining_hlen -= coord[1]
413 if key == "READ_IO_SIZES":
414 global last_read_size
415 last_size = last_read_size.get(client_id, 0)
416 size = m[1] - last_size
417 last_read_size[client_id] = m[1]
418 if key == "WRITE_IO_SIZES":
419 global last_write_size
420 last_size = last_write_size.get(client_id, 0)
421 size = m[1] - last_size
422 last_write_size[client_id] = m[1]
423 self.mainw.addnstr(y_coord, coord[0],
424 f'{calc_speed(size, duration)}',
425 hlen)
f67539c2
TL
426 else:
427 # display 0th element from metric tuple
b3b6e05e 428 self.mainw.addnstr(y_coord, coord[0], f'{m[0]}', hlen)
f67539c2 429 else:
b3b6e05e 430 self.mainw.addnstr(y_coord, coord[0], "N/A", hlen)
f67539c2
TL
431 cidx += 1
432
b3b6e05e
TL
433 if remaining_hlen == 0:
434 return
435
436 for item in MAIN_WINDOW_TOP_LINE_ITEMS_END:
437 coord = x_coord_map[item]
438 hlen = coord[1] - len(ITEMS_PAD)
439 # always place the FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR in the
440 # last, it will be a very long string to display
441 if item == FS_TOP_MAIN_WINDOW_COL_MNTPT_HOST_ADDR:
442 if FSTop.has_metrics(client_meta, [CLIENT_METADATA_MOUNT_POINT_KEY,
443 CLIENT_METADATA_HOSTNAME_KEY,
444 CLIENT_METADATA_IP_KEY]):
445 self.mainw.addnstr(y_coord, coord[0],
446 f'{client_meta[CLIENT_METADATA_MOUNT_POINT_KEY]}@'
447 f'{client_meta[CLIENT_METADATA_HOSTNAME_KEY]}/'
448 f'{client_meta[CLIENT_METADATA_IP_KEY]}',
449 remaining_hlen)
450 else:
451 self.mainw.addnstr(y_coord, coord[0], "N/A", remaining_hlen)
452 hlen = min(hlen, remaining_hlen)
453 if remaining_hlen < coord[1]:
454 remaining_hlen = 0
455 else:
456 remaining_hlen -= coord[1]
457 if remaining_hlen == 0:
458 return
459
f67539c2
TL
460 def refresh_clients(self, x_coord_map, stats_json):
461 counters = [m.upper() for m in stats_json[GLOBAL_COUNTERS_KEY]]
462 y_coord = 0
463 for client_id, metrics in stats_json[GLOBAL_METRICS_KEY].items():
464 self.refresh_client(client_id,
465 metrics,
466 counters,
467 stats_json[CLIENT_METADATA_KEY][client_id],
468 x_coord_map,
469 y_coord)
470 y_coord += 1
471
472 def refresh_main_window(self, x_coord_map, stats_json):
b3b6e05e
TL
473 if self.mainw is None:
474 return
f67539c2 475 self.refresh_clients(x_coord_map, stats_json)
b3b6e05e 476 self.mainw.refresh()
f67539c2
TL
477
478 def refresh_header(self, stats_json):
b3b6e05e 479 hlen = self.width - 2
f67539c2 480 if not stats_json['version'] == FS_TOP_SUPPORTED_VER:
b3b6e05e 481 self.header.addnstr(0, 0, 'perf stats version mismatch!', hlen)
f67539c2
TL
482 return False
483 client_metadata = stats_json[CLIENT_METADATA_KEY]
484 num_clients = len(client_metadata)
485 num_mounts = len([client for client, metadata in client_metadata.items() if
486 CLIENT_METADATA_MOUNT_POINT_KEY in metadata
487 and metadata[CLIENT_METADATA_MOUNT_POINT_KEY] != 'N/A'])
488 num_kclients = len([client for client, metadata in client_metadata.items() if
489 "kernel_version" in metadata])
490 num_libs = num_clients - (num_mounts + num_kclients)
491 now = datetime.now().ctime()
b3b6e05e
TL
492 self.header.addnstr(0, 0,
493 FS_TOP_VERSION_HEADER_FMT.format(prog_name=FS_TOP_PROG_STR, now=now),
494 hlen, curses.A_STANDOUT | curses.A_BOLD)
495 self.header.addnstr(1, 0, FS_TOP_CLIENT_HEADER_FMT.format(num_clients=num_clients,
496 num_mounts=num_mounts,
497 num_kclients=num_kclients,
498 num_libs=num_libs), hlen)
499 self.header.refresh()
f67539c2
TL
500 return True
501
b3b6e05e 502 def run_display(self):
f67539c2 503 while not self.exit_ev.is_set():
b3b6e05e
TL
504 # use stdscr.clear() instead of clearing each window
505 # to avoid screen blinking.
506 self.stdscr.clear()
507 self.refresh_window_size()
508 if self.width <= 2 or self.width <= 2:
509 self.exit_ev.wait(timeout=self.refresh_interval_secs)
510 continue
511
512 # coordinate constants for windowing -- (height, width, y, x)
513 # NOTE: requires initscr() call before accessing COLS, LINES.
514 try:
515 HEADER_WINDOW_COORD = (2, self.width - 1, 0, 0)
516 self.header = curses.newwin(*HEADER_WINDOW_COORD)
517 if self.height >= 3:
518 TOPLINE_WINDOW_COORD = (1, self.width - 1, 3, 0)
519 self.topl = curses.newwin(*TOPLINE_WINDOW_COORD)
520 else:
521 self.topl = None
522 if self.height >= 5:
523 MAIN_WINDOW_COORD = (self.height - 4, self.width - 1, 4, 0)
524 self.mainw = curses.newwin(*MAIN_WINDOW_COORD)
525 else:
526 self.mainw = None
527 except curses.error:
528 # this may happen when creating the sub windows the
529 # terminal window size changed, just retry it
530 continue
531
f67539c2 532 stats_json = self.perf_stats_query()
b3b6e05e
TL
533 try:
534 if self.refresh_header(stats_json):
535 x_coord_map = self.refresh_top_line_and_build_coord()
536 self.refresh_main_window(x_coord_map, stats_json)
537 self.exit_ev.wait(timeout=self.refresh_interval_secs)
538 except curses.error:
539 # this may happen when addstr the terminal window
540 # size changed, just retry it
541 pass
f67539c2
TL
542
543
544if __name__ == '__main__':
545 def float_greater_than(x):
546 value = float(x)
547 if value < MIN_REFRESH_INTERVAL:
20effc67
TL
548 raise argparse.ArgumentTypeError(
549 f'Refresh interval should be greater than or equal to {MIN_REFRESH_INTERVAL}')
f67539c2
TL
550 return value
551
552 parser = argparse.ArgumentParser(description='Ceph Filesystem top utility')
553 parser.add_argument('--cluster', nargs='?', const='ceph', default='ceph',
20effc67 554 help='Ceph cluster to connect (default: ceph)')
f67539c2
TL
555 parser.add_argument('--id', nargs='?', const='fstop', default='fstop',
556 help='Ceph user to use to connection (default: fstop)')
557 parser.add_argument('--conffile', nargs='?', default=None,
558 help='Path to cluster configuration file')
559 parser.add_argument('--selftest', dest='selftest', action='store_true',
20effc67 560 help='Run in selftest mode')
f67539c2
TL
561 parser.add_argument('-d', '--delay', nargs='?', default=DEFAULT_REFRESH_INTERVAL,
562 type=float_greater_than, help='Interval to refresh data '
563 f'(default: {DEFAULT_REFRESH_INTERVAL})')
564
565 args = parser.parse_args()
566 err = False
567 ft = FSTop(args)
568 try:
569 ft.init()
570 if args.selftest:
571 ft.selftest()
572 sys.stdout.write("selftest ok\n")
573 else:
b3b6e05e 574 curses.wrapper(ft.setup_curses)
f67539c2
TL
575 except FSTopException as fst:
576 err = True
577 sys.stderr.write(f'{fst.get_error_msg()}\n')
578 except Exception as e:
579 err = True
580 sys.stderr.write(f'exception: {e}\n')
581 finally:
582 ft.fini()
583 sys.exit(0 if not err else -1)