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.
- __init__.py +1 -0
- controller/controller.py +255 -0
- model/infrastructure.py +95 -0
- model/job.py +75 -0
- nodeocc-1.0.1.dist-info/METADATA +33 -0
- nodeocc-1.0.1.dist-info/RECORD +17 -0
- nodeocc-1.0.1.dist-info/WHEEL +5 -0
- nodeocc-1.0.1.dist-info/entry_points.txt +2 -0
- nodeocc-1.0.1.dist-info/top_level.txt +5 -0
- readers/compute_wait_time.py +28 -0
- readers/slurmreader.py +308 -0
- view/__init__.py +12 -0
- view/curses_multiwindow.py +580 -0
- view/slurm_list.py +148 -0
- view/slurm_viz.py +494 -0
- view/styles.py +76 -0
- view/utils.py +60 -0
|
@@ -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())
|