nodeocc 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,580 @@
1
+ import asyncio
2
+ import curses
3
+ import grp
4
+ import logging
5
+ import os
6
+ import signal
7
+ import socket
8
+ import threading
9
+ import time
10
+ import traceback
11
+ from logging.handlers import RotatingFileHandler
12
+ from pathlib import Path
13
+
14
+ from view import update_views
15
+
16
+ a_filter_values = [None, 'me', 'prod', 'stud', 'cvcs']
17
+
18
+
19
+ def try_open_socket_as_slave(instance):
20
+ if not instance.port_file_exists():
21
+ raise Exception("No master running")
22
+
23
+ cur_port = int(instance.get_port_file_name()[0].split('.')[0])
24
+ if hasattr(instance, 'port'):
25
+ # check if port file has same name
26
+ if instance.port != cur_port:
27
+ instance.log(f"- MASTER HAS CHANGED PORT: {instance.port} vs {cur_port}")
28
+
29
+ instance.port = cur_port
30
+ instance._open_socket_as_slave(instance.port)
31
+
32
+ if instance.try_open_counter > 5:
33
+ instance.err(f"Could not open socket as slave on port {instance.port}")
34
+ raise Exception(f"Could not open socket on port {instance.port}")
35
+
36
+
37
+ class Singleton:
38
+ __instance = None
39
+
40
+ @staticmethod
41
+ def getInstance(args=None):
42
+ """ Static access method. """
43
+ if Singleton.__instance is None:
44
+ Singleton(args)
45
+ if args is not None:
46
+ Singleton.__instance.args = args
47
+ return Singleton.__instance
48
+
49
+ # destructor
50
+ def __del__(self):
51
+ if self.args.master:
52
+ if self.port_file_exists():
53
+ Path(self.get_port_file_name()[1]).unlink()
54
+
55
+ def __init__(self, args=None):
56
+ """ Virtually private constructor. """
57
+ if Singleton.__instance is not None:
58
+ raise Exception("This class is a singleton!")
59
+ else:
60
+ Singleton.__instance = self
61
+ self.try_open_counter = 0
62
+ self.voff = 0
63
+ self.mouse_state = {}
64
+ self.max_columns = 0
65
+
66
+ self.nocc = ""
67
+ self.rens = ""
68
+
69
+ self.fetch_fn = None
70
+ self.view_mode = 'gpu'
71
+ self.job_id_type = 'agg'
72
+ self.args = args
73
+
74
+ self.show_account = False
75
+ self.show_prio = False
76
+ self.sort_by_prio = False
77
+ self.show_starttime = False
78
+
79
+ self.inf = None
80
+ self.jobs = []
81
+ self.avg_wait_time = 'err'
82
+ self.a_filter = 0
83
+ self.k = -1
84
+
85
+ # check if user group is 'student' or 'tesisti'
86
+ user_group = grp.getgrgid(os.getgid()).gr_name
87
+ self.cur_partition = 'stud' if user_group in ['studenti', 'tesisti'] else 'prod'
88
+
89
+ self.basepath = self.args.basepath
90
+
91
+ if self.args.daemon_only:
92
+ handler = RotatingFileHandler(os.path.join(self.basepath, '.master_log_n.txt'), maxBytes=5 * 1024 * 1024, backupCount=2, mode='w')
93
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S',
94
+ handlers=[handler])
95
+ # set logging file
96
+ elif self.args.debug:
97
+ # create rotating file handler
98
+ handler = RotatingFileHandler(os.path.expanduser('~') + '/.nodeocc_log.txt', maxBytes=5 * 1024 * 1024, backupCount=2, mode='w')
99
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S',
100
+ handlers=[handler])
101
+
102
+ if self.args.master:
103
+ # check if any .port file exists
104
+ if self.port_file_exists():
105
+ if self.args.force_override:
106
+ _, port_filepath = self.get_port_file_name()
107
+
108
+ # read pid from file
109
+ pid = Path(port_filepath).read_text()
110
+ # kill process
111
+ try:
112
+ os.kill(int(pid), signal.SIGTERM)
113
+ except Exception as e:
114
+ self.log("Could not kill process, deleting port file")
115
+ self.log(e)
116
+
117
+ # delete port file
118
+ Path(port_filepath).unlink()
119
+ else:
120
+ raise Exception("Master already running")
121
+ self.port = self.create_socket_as_master()
122
+ else:
123
+ try_open_socket_as_slave(self)
124
+
125
+ def get_port_file_name(self):
126
+ if self.port_file_exists():
127
+ fname = [f for f in os.listdir(self.basepath) if f.endswith('.port')][0]
128
+ return fname, os.path.join(self.basepath, fname)
129
+ return None
130
+
131
+ def check_port_file_master(self):
132
+ if self.port_file_exists():
133
+ prev_port = int(self.get_port_file_name()[0].split('.')[0])
134
+ if hasattr(self, 'port'):
135
+ return self.port == prev_port
136
+ return False
137
+
138
+ def port_file_exists(self):
139
+ return len([f for f in os.listdir(self.basepath) if f.endswith('.port')]) > 0
140
+
141
+ def create_socket_as_master(self):
142
+ # create udp socket for broadcasting
143
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
144
+
145
+ self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
146
+
147
+ # Enable broadcasting mode
148
+ self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
149
+
150
+ self.sock.bind(('', 0))
151
+
152
+ self.port = self.sock.getsockname()[1]
153
+ self.sock.settimeout(2)
154
+ self.log(f"Socket created as master on port {self.port}")
155
+
156
+ # get pid of current process
157
+ self.pid = os.getpid()
158
+
159
+ # create file to store port
160
+ Path(self.basepath, f'{self.port}.port').write_text(str(self.pid))
161
+
162
+ return self.port
163
+
164
+ def _open_socket_as_slave(self, port):
165
+ try:
166
+ # create udp socket for broadcasting
167
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
168
+
169
+ self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
170
+
171
+ # sock listen on port
172
+ self.sock.bind(('', port))
173
+
174
+ self.sock.setblocking(True)
175
+
176
+ self.sock.settimeout(6.5)
177
+ self.log(f"Socket opened on port {self.port}")
178
+
179
+ self.try_open_counter = 0
180
+ return self.port
181
+ except Exception as e:
182
+ self.err(f"Exception: {e}")
183
+ self.err(traceback.format_exc())
184
+ self.try_open_counter += 1
185
+ return None
186
+
187
+ def timeme(self, msg=None):
188
+ if not hasattr(self, '_ctime'):
189
+ self._ctime = time.time()
190
+ if msg is not None:
191
+ self.log(msg)
192
+ else:
193
+ _ntime = time.time()
194
+ self.log(f"{msg} took {_ntime - self._ctime:.2f} seconds")
195
+ self._ctime = _ntime
196
+
197
+ def err(self, msg):
198
+ if self.args.debug or self.args.daemon_only:
199
+ logging.error(msg)
200
+
201
+ def log(self, msg):
202
+ if self.args.debug or self.args.daemon_only:
203
+ logging.info(msg)
204
+
205
+ async def fetch(self):
206
+ _ctime = time.time()
207
+
208
+ if self.fetch_fn is not None:
209
+ inf, jobs, avg_wait_time = await self.fetch_fn() # a_filter_values[self.a_filter])
210
+ self.inf = inf if inf is not None else self.inf
211
+ self.jobs = jobs if jobs is not None else self.jobs
212
+ self.avg_wait_time = avg_wait_time if avg_wait_time is not None else self.avg_wait_time
213
+
214
+ _delta_t = time.time() - _ctime
215
+ self.log(f"Fetch took {_delta_t:.2f} seconds")
216
+
217
+ def add_button(self, y, x, width, action):
218
+ if y not in self.mouse_state:
219
+ self.mouse_state[y] = {}
220
+ if isinstance(width, str):
221
+ width = len(width)
222
+ for j in range(x, x + width):
223
+ self.mouse_state[y][j] = action
224
+
225
+
226
+ class Buffer(object):
227
+
228
+ def __init__(self, window, lines, screen):
229
+ self.window = window
230
+ self.lines = lines
231
+ self.buffer = [""]
232
+ self.screen = screen
233
+
234
+ def write(self, text):
235
+ lines = text.split("\n")
236
+ self.buffer[-1] += lines[0]
237
+ self.buffer.extend(lines[1:])
238
+ self.refresh()
239
+
240
+ def writeln(self, text):
241
+ self.write(text + "\n")
242
+
243
+ def input(self, text=""):
244
+ return self._input(text, lambda: self.window.getstr().decode('utf-8'))
245
+
246
+ def input_chr(self, text=""):
247
+ return self._input(text, lambda: chr(self.window.getch()))
248
+
249
+ def _input(self, text, get_input):
250
+ self.write(text)
251
+ input = get_input()
252
+ self.writeln(input)
253
+ return input
254
+
255
+ def refresh(self, voff=0, usevoff=False):
256
+ instance = Singleton.getInstance()
257
+ self.window.erase()
258
+ off = max(0, ((self.lines - 2) - len(self.buffer)) // 2)
259
+ if usevoff and voff < 0:
260
+ instance.voff = 0
261
+ elif usevoff and voff > len(self.buffer) - self.lines + 2:
262
+ instance.voff = len(self.buffer) - self.lines + 2
263
+
264
+ voff = max(min(voff, len(self.buffer) - self.lines + 2), 0)
265
+ for nr, line in enumerate(self.buffer[voff:voff + self.lines - 2]): # self.buffer[-self.lines+2:]):
266
+ lastcol = 2
267
+ xacc = 1
268
+
269
+ for chunk in line.split('<*'):
270
+ if ':*>' in chunk:
271
+
272
+ color = int(chunk[0:chunk.index('~')])
273
+ chunk_segments = chunk[chunk.index('~') + 1:].split(':*>')
274
+ try:
275
+ self.window.addstr(nr + off + 1, xacc, chunk_segments[0], curses.color_pair(color) | (curses.A_REVERSE if color > 9 else 0))
276
+ except curses.error:
277
+ pass
278
+ xacc += len(chunk_segments[0])
279
+
280
+ if len(chunk_segments[1]) > 0:
281
+ self.window.addstr(nr + off + 1, xacc, chunk_segments[1])
282
+ xacc += len(chunk_segments[1])
283
+ else:
284
+ try:
285
+ self.window.addstr(nr + off + 1, xacc, chunk)
286
+ except Exception as e:
287
+ instance.err(e)
288
+ pass
289
+ xacc += len(chunk)
290
+ if len(self.buffer) > self.lines - 2:
291
+ self.screen.addstr(self.lines - 2, instance.xoffset + instance.left_width // 2 - 6, ' ▼ SCROLL ▲ ', curses.color_pair(2) | curses.A_REVERSE)
292
+ instance.add_button(self.lines - 2, instance.xoffset + 31, 'D', ord('s'))
293
+ instance.add_button(self.lines - 2, instance.xoffset + 40, 'U', ord('w'))
294
+
295
+ self.window.border()
296
+ self.window.noutrefresh()
297
+
298
+
299
+ def process_mouse():
300
+ try:
301
+ _, x, y, _, bstate = curses.getmouse()
302
+
303
+ if bstate & curses.BUTTON1_RELEASED != 0:
304
+ ms = Singleton.getInstance().mouse_state
305
+ if y in ms and x in ms[y]:
306
+ return True, ms[y][x]
307
+ return False, -1
308
+ except BaseException:
309
+ return False, -1
310
+
311
+
312
+ def handle_keys(stdscr, instance):
313
+ k = stdscr.getch()
314
+ instance.log("GOT CHAR: " + str(k))
315
+ if k == curses.KEY_RESIZE:
316
+ instance.log("RESIZED WINDOW")
317
+
318
+ if k == curses.ERR and k == -1:
319
+ return
320
+
321
+ instance.k = k
322
+
323
+ valid_mouse = False
324
+ if k == curses.KEY_MOUSE:
325
+ valid_mouse, ck = process_mouse()
326
+ if valid_mouse:
327
+ k = ck
328
+
329
+ instance.mouse_state = {}
330
+
331
+ # process input
332
+ # RIGHT
333
+ if k == ord('d') or k == 261:
334
+ instance.a_filter = (instance.a_filter + 1) % len(a_filter_values)
335
+ instance.voff = 0
336
+ # LEFT
337
+ elif k == ord('a') or k == 260:
338
+ instance.a_filter = (instance.a_filter + (len(a_filter_values) - 1)) % len(a_filter_values)
339
+ instance.voff = 0
340
+ elif valid_mouse and isinstance(k, str) and k.startswith('AF_'):
341
+ instance.a_filter = int(k.split('AF_')[1])
342
+ # DOWN
343
+ elif k == ord('s') or k == 258:
344
+ instance.voff += 1
345
+ # UP
346
+ elif k == ord('w') or k == 259:
347
+ instance.voff -= 1
348
+
349
+ if k == ord('S'):
350
+ instance.sort_by_prio = not instance.sort_by_prio
351
+ if k == ord('g'):
352
+ instance.view_mode = {"gpu": "ram", "ram": "cpu", "cpu": "gpu"}[instance.view_mode]
353
+ if k == ord('j'):
354
+ instance.job_id_type = "true" if instance.job_id_type == "agg" else "agg"
355
+ if k == ord('z'):
356
+ # instance.show_starttime = not instance.show_starttime
357
+ pass
358
+ if k == ord('t'):
359
+ instance.show_account = not instance.show_account
360
+ if k == ord('p'):
361
+ instance.show_prio = not instance.show_prio
362
+
363
+
364
+ def update_screen(stdscr, instance):
365
+ update_views(stdscr, instance, a_filter_values[instance.a_filter])
366
+ xoffset = 0
367
+ instance.xoffset = xoffset
368
+
369
+ lines, columns = os.get_terminal_size().lines, os.get_terminal_size().columns
370
+ s_lines, s_columns = stdscr.getmaxyx()
371
+ instance.max_columns = s_columns
372
+ if instance.k == ord('y'):
373
+ stdscr.clear()
374
+
375
+ totsize = 106
376
+ if instance.show_account:
377
+ totsize += 10
378
+ if instance.show_prio:
379
+ totsize += 8
380
+ totsize += 10 # from /etc/update-motd.d/02-wait-times
381
+
382
+ if columns < totsize:
383
+ stdscr.addstr(1, 1, "MINIMUM TERM. WIDTH")
384
+ stdscr.addstr(2, 1, f"REQUIRED: {totsize}")
385
+ stdscr.addstr(3, 1, "CURRENT: " + str(columns))
386
+ stdscr.refresh()
387
+ return
388
+
389
+ # update state (recompute lines for safety)
390
+ if lines != s_lines or columns != s_columns:
391
+ instance.log(f"Resized window: {s_lines}x{s_columns} -> {lines}x{columns}")
392
+ stdscr.clear()
393
+ s_columns = columns
394
+ s_lines = lines
395
+
396
+ stdscr.refresh()
397
+
398
+ left_width = columns - 33 # 72
399
+ instance.left_width = left_width
400
+ left_window = curses.newwin(lines - 1, left_width, 0, xoffset)
401
+ left_buffer = Buffer(left_window, lines, stdscr)
402
+ right_window = curses.newwin(lines - 1, 31, 0, xoffset + left_width + 1)
403
+ right_buffer = Buffer(right_window, lines, stdscr)
404
+
405
+ left_buffer.write(instance.rens)
406
+ right_buffer.write(instance.nocc)
407
+ right_buffer.refresh()
408
+ left_buffer.refresh(instance.voff, True)
409
+
410
+ # render menu
411
+ stdscr.addstr(lines - 1, xoffset + 1 + 0, '◀')
412
+ instance.add_button(lines - 1, xoffset + 1 + 0, '◀', ord('a'))
413
+ stdscr.addstr(lines - 1, xoffset + 1 + 2, 'ALL', curses.color_pair(2) | (curses.A_REVERSE if a_filter_values[instance.a_filter] is None else 0))
414
+
415
+ instance.add_button(lines - 1, xoffset + 1 + 2, 'ALL', 'AF_0')
416
+
417
+ stdscr.addstr(lines - 1, xoffset + 1 + 6, 'ME', curses.color_pair(2) | (curses.A_REVERSE if a_filter_values[instance.a_filter] == 'me' else 0))
418
+ instance.add_button(lines - 1, xoffset + 1 + 6, 'ME', 'AF_1')
419
+ stdscr.addstr(lines - 1, xoffset + 1 + 9, 'PROD', curses.color_pair(2) | (curses.A_REVERSE if a_filter_values[instance.a_filter] == 'prod' else 0))
420
+ instance.add_button(lines - 1, xoffset + 1 + 9, 'PROD', 'AF_2')
421
+ stdscr.addstr(lines - 1, xoffset + 1 + 14, 'STUD', curses.color_pair(2) | (curses.A_REVERSE if a_filter_values[instance.a_filter] == 'stud' else 0))
422
+ instance.add_button(lines - 1, xoffset + 1 + 14, 'STUD', 'AF_3')
423
+ stdscr.addstr(lines - 1, xoffset + 1 + 19, 'CVCS', curses.color_pair(2) | (curses.A_REVERSE if a_filter_values[instance.a_filter] == 'cvcs' else 0))
424
+ instance.add_button(lines - 1, xoffset + 1 + 19, 'CVCS', 'AF_4')
425
+
426
+ stdscr.addstr(lines - 1, xoffset + 1 + 24, '▶')
427
+ instance.add_button(lines - 1, xoffset + 1 + 24, '▶', ord('d'))
428
+ stdscr.addstr(lines - 1, xoffset + 1 + 25, ' ' * (columns - 27 - xoffset))
429
+
430
+ stdscr.addstr(lines - 1, left_width - 8, '[Q:QUIT]', curses.color_pair(2))
431
+ instance.add_button(lines - 1, left_width - 8, '[Q:QUIT]', ord('q')) # 53
432
+
433
+ stdscr.addstr(lines - 1, left_width - 18 + 19, '[Y:REDRAW]', curses.color_pair(2))
434
+ instance.add_button(lines - 1, left_width - 18 + 19, '[Y:REDRAW]', ord('y'))
435
+
436
+ stdscr.addstr(0, columns - 15, '[G:', curses.color_pair(2))
437
+ stdscr.addstr(0, columns - 12, 'GPU', curses.color_pair(2) | (curses.A_REVERSE if instance.view_mode == 'gpu' else 0))
438
+ stdscr.addstr(0, columns - 12 + 3, 'RAM', curses.color_pair(2) | (curses.A_REVERSE if instance.view_mode == 'ram' else 0))
439
+ stdscr.addstr(0, columns - 12 + 6, 'CPU', curses.color_pair(2) | (curses.A_REVERSE if instance.view_mode == 'cpu' else 0))
440
+ stdscr.addstr(0, columns - 12 + 9, ']', curses.color_pair(2))
441
+ instance.add_button(0, columns - 15, '[G:GPURAMCPU]', ord('g'))
442
+
443
+ stdscr.addstr(lines - 1, xoffset + 25 + 2, '[J:', curses.color_pair(2))
444
+ stdscr.addstr(lines - 1, xoffset + 25 + 2 + 3, 'AGG', curses.color_pair(2) | (curses.A_REVERSE if instance.job_id_type == 'agg' else 0))
445
+ stdscr.addstr(lines - 1, xoffset + 25 + 2 + 3 + 3, 'TRUE', curses.color_pair(2) | (curses.A_REVERSE if instance.job_id_type == 'true' else 0))
446
+ stdscr.addstr(lines - 1, xoffset + 25 + 2 + 3 + 3 + 4, ']', curses.color_pair(2))
447
+ instance.add_button(lines - 1, xoffset + 25 + 2, '[J:AGGTRUE]', ord('j'))
448
+
449
+ stdscr.addstr(lines - 1, xoffset + 37 + 2, '[P:', curses.color_pair(2))
450
+ stdscr.addstr(lines - 1, xoffset + 37 + 2 + 3, 'PRIORITY', curses.color_pair(2) | (curses.A_REVERSE if instance.show_prio else 0))
451
+ stdscr.addstr(lines - 1, xoffset + 37 + 2 + 3 + 8, ']', curses.color_pair(2))
452
+ instance.add_button(lines - 1, xoffset + 37 + 2, '[P:PRIORITY]', ord('p'))
453
+
454
+ stdscr.addstr(lines - 1, xoffset + 50 + 2, '[T:', curses.color_pair(2))
455
+ stdscr.addstr(lines - 1, xoffset + 50 + 2 + 3, 'ACCOUNT', curses.color_pair(2) | (curses.A_REVERSE if instance.show_account else 0))
456
+ stdscr.addstr(lines - 1, xoffset + 50 + 2 + 3 + 7, ']', curses.color_pair(2))
457
+ instance.add_button(lines - 1, xoffset + 50 + 2, '[T:ACCOUNT]', ord('t'))
458
+
459
+ # get slurm user partition
460
+ stdscr.addstr(lines - 1, xoffset + 62 + 2, f'(Avg time {instance.avg_wait_time})', curses.color_pair(2))
461
+
462
+ signature = instance.signature
463
+ stdscr.addstr(lines - 1, columns - 2 - len(signature), signature)
464
+
465
+ stdscr.refresh()
466
+ curses.doupdate()
467
+
468
+
469
+ def get_char_async(stdscr, instance):
470
+ while instance.k != ord('q') and instance.k != 'q':
471
+ handle_keys(stdscr, instance)
472
+ update_screen(stdscr, instance)
473
+
474
+ # raise SIGINT to cancel update task
475
+ os.kill(os.getpid(), signal.SIGINT)
476
+
477
+
478
+ async def update_screen_info(stdscr, instance):
479
+ while instance.k != ord('q') and instance.k != 'q':
480
+ await instance.fetch()
481
+ instance.log("GOT DATA FROM MASTER")
482
+ update_screen(stdscr, instance)
483
+
484
+
485
+ async def wait_first(futures, instance):
486
+ ''' Return the result of the first future to finish. Cancel the remaining
487
+ futures. '''
488
+ try:
489
+ done, pending = await asyncio.wait(futures,
490
+ return_when=asyncio.FIRST_COMPLETED)
491
+
492
+ # cancel the other tasks, we have a result. We need to wait for the cancellations
493
+ # to propagate.
494
+ for task in pending:
495
+ task.cancel()
496
+ await asyncio.wait(pending)
497
+ except Exception as e:
498
+ instance.log(e)
499
+ # get trace
500
+ instance.log(traceback.format_exc())
501
+
502
+
503
+ async def main(stdscr):
504
+ # Clear screen
505
+ instance = Singleton.getInstance()
506
+ stdscr.clear()
507
+ curses.noecho()
508
+ curses.curs_set(0)
509
+ # stdscr.timeout(2000)
510
+
511
+ stdscr.nodelay(False)
512
+
513
+ # stdscr.timeout(int(timedelta_refresh*1000))
514
+ _ = curses.mousemask(1)
515
+
516
+ curses.use_default_colors()
517
+ # colors
518
+ curses.init_pair(1, 3, 0)
519
+ # curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
520
+ curses.init_pair(2, -1, -1)
521
+ # curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK)
522
+
523
+ curses.init_pair(3, 1, -1)
524
+ # curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK)
525
+ curses.init_pair(4, 3, -1)
526
+ # curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK)
527
+ curses.init_pair(5, 2, -1)
528
+ # curses.init_pair(5, curses.COLOR_GREEN, curses.COLOR_BLACK)
529
+ curses.init_pair(6, 5, -1)
530
+ # curses.init_pair(6, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
531
+ curses.init_pair(7, 4, -1)
532
+ # curses.init_pair(7, curses.COLOR_BLUE, curses.COLOR_BLACK)
533
+ curses.init_pair(8, 6, -1)
534
+ # curses.init_pair(8, curses.COLOR_CYAN, curses.COLOR_BLACK)
535
+
536
+ curses.init_pair(10, 3, -1)
537
+ # curses.init_pair(10, curses.COLOR_WHITE, curses.COLOR_RED)
538
+ curses.init_pair(11, 3, -1)
539
+ # curses.init_pair(11, curses.COLOR_WHITE, curses.COLOR_GREEN)
540
+ curses.init_pair(12, 3, -1)
541
+ # curses.init_pair(12, curses.COLOR_WHITE, curses.COLOR_YELLOW)
542
+ curses.init_pair(13, 5, -1)
543
+ # curses.init_pair(13, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
544
+
545
+ curses.init_pair(15, -1, 6)
546
+ # curses.init_pair(15, curses.COLOR_WHITE, curses.COLOR_CYAN) (reversed because >9)
547
+ # status
548
+
549
+ instance.a_filter = 0
550
+
551
+ stdscr.clear()
552
+
553
+ def exit_handler(sig, frame):
554
+ instance.log(f"FORCED EXIT...")
555
+ instance.sock.close()
556
+ exit(0)
557
+ signal.signal(signal.SIGINT, exit_handler)
558
+
559
+ # print waiting message un stdscr
560
+ stdscr.addstr(0, 0, "Waiting for data from master...")
561
+ await instance.fetch()
562
+
563
+ update_screen(stdscr, instance)
564
+ update_screen(stdscr, instance) # need 2 for some reason...
565
+
566
+ update_task = None
567
+
568
+ curses_thread = threading.Thread(target=get_char_async, args=(stdscr, instance))
569
+ curses_thread.daemon = True
570
+ curses_thread.start()
571
+
572
+ update_task = asyncio.create_task(update_screen_info(stdscr, instance))
573
+ try:
574
+ await update_task
575
+ except asyncio.exceptions.CancelledError:
576
+ pass
577
+ except Exception as e:
578
+ instance.log(e)
579
+ # get trace
580
+ instance.log(traceback.format_exc())