nodeocc 1.0.9__tar.gz → 1.0.11__tar.gz

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.
Files changed (23) hide show
  1. {nodeocc-1.0.9 → nodeocc-1.0.11}/PKG-INFO +2 -1
  2. {nodeocc-1.0.9 → nodeocc-1.0.11}/pyproject.toml +2 -2
  3. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/controller/controller.py +60 -54
  4. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/nodeocc.egg-info/PKG-INFO +2 -1
  5. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/nodeocc.egg-info/requires.txt +1 -0
  6. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/view/curses_multiwindow.py +77 -43
  7. {nodeocc-1.0.9 → nodeocc-1.0.11}/README.md +0 -0
  8. {nodeocc-1.0.9 → nodeocc-1.0.11}/setup.cfg +0 -0
  9. {nodeocc-1.0.9 → nodeocc-1.0.11}/setup.py +0 -0
  10. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/__init__.py +0 -0
  11. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/model/infrastructure.py +0 -0
  12. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/model/job.py +0 -0
  13. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/nodeocc.egg-info/SOURCES.txt +0 -0
  14. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/nodeocc.egg-info/dependency_links.txt +0 -0
  15. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/nodeocc.egg-info/entry_points.txt +0 -0
  16. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/nodeocc.egg-info/top_level.txt +0 -0
  17. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/readers/compute_wait_time.py +0 -0
  18. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/readers/slurmreader.py +0 -0
  19. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/view/__init__.py +0 -0
  20. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/view/slurm_list.py +0 -0
  21. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/view/slurm_viz.py +0 -0
  22. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/view/styles.py +0 -0
  23. {nodeocc-1.0.9 → nodeocc-1.0.11}/src/view/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nodeocc
3
- Version: 1.0.9
3
+ Version: 1.0.11
4
4
  Summary: TUI application for viewing the status of GPU allocations on a Slurm cluster
5
5
  Author: Matteo Boschini
6
6
  Maintainer: Lorenzo Bonicelli, Aniello Panariello
@@ -14,6 +14,7 @@ Requires-Python: >=3.9
14
14
  Description-Content-Type: text/markdown
15
15
  Requires-Dist: pandas
16
16
  Requires-Dist: numpy
17
+ Requires-Dist: psutil
17
18
 
18
19
  # Slurm NodeOCC
19
20
  ![[screenshot](https://github.com/apanariello4/slurm_nodeocc/blob/master/sshot.jpg?raw=true)](sshot.jpg)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nodeocc"
7
- version = "1.0.9"
7
+ version = "1.0.11"
8
8
  authors = [{ name = "Matteo Boschini" }]
9
9
  maintainers = [{ name = "Lorenzo Bonicelli" }, { name = "Aniello Panariello" }]
10
10
  description = "TUI application for viewing the status of GPU allocations on a Slurm cluster"
@@ -27,7 +27,7 @@ classifiers = [
27
27
  "Programming Language :: Python :: 3",
28
28
  ]
29
29
 
30
- dependencies = ["pandas", "numpy"]
30
+ dependencies = ["pandas", "numpy", "psutil"]
31
31
 
32
32
  [project.urls]
33
33
  Repository = "https://github.com/apanariello4/slurm_nodeocc"
@@ -60,7 +60,6 @@ except Exception as e:
60
60
  newest_version = '?.?.?'
61
61
  time.sleep(5)
62
62
 
63
-
64
63
  BEGIN_DELIM = "!{$"
65
64
  END_DELIM_ENCODED = "!}$".encode('utf-8')
66
65
  EXTRA_MSG_BEGIN_DELIM = "!{#"
@@ -70,8 +69,7 @@ JOB_LIMIT_PER_MSG = 60
70
69
 
71
70
  parser = argparse.ArgumentParser(description='Visualize slurm jobs')
72
71
  parser.add_argument('--debug', action='store_true', help='Enable logging')
73
- parser.add_argument('--master', action='store_true', help='Start master process')
74
- parser.add_argument('--daemon_only', action='store_true', help='Disable all prints - only run in background')
72
+ parser.add_argument('--master_only', action='store_true', help='Disable all prints - only run in background')
75
73
  parser.add_argument('--force_override', action='store_true', help='Force override of port file, kill previous instance')
76
74
  parser.add_argument('--basepath', type=str, default='/nas/softechict-nas-2/mboschini/cool_scripts/new_nodeocc/', help='Base path for nodeocc. Must be readable by all users')
77
75
  args = parser.parse_args()
@@ -80,15 +78,19 @@ args = parser.parse_args()
80
78
  assert os.access(args.basepath, os.R_OK), f"Base path {args.basepath} not readable"
81
79
 
82
80
  # export args
83
- Singleton.getInstance(args)
81
+ orig_instance = Singleton.getInstance(args)
84
82
 
85
- last_update = os.path.getmtime(conf_path + "/view/slurm_viz.py")
86
- if os.path.getmtime(conf_path + "/view/slurm_list.py") > last_update:
87
- last_update = os.path.getmtime(conf_path + "/view/slurm_list.py")
88
- if os.path.getmtime(conf_path + "/view/styles.py") > last_update:
89
- last_update = os.path.getmtime(conf_path + "/view/styles.py")
90
- if os.path.getmtime(conf_path + "/readers/slurmreader.py") > last_update:
91
- last_update = os.path.getmtime(conf_path + "/readers/slurmreader.py")
83
+ try:
84
+ last_update = os.path.getmtime(conf_path + "/view/slurm_viz.py")
85
+ if os.path.getmtime(conf_path + "/view/slurm_list.py") > last_update:
86
+ last_update = os.path.getmtime(conf_path + "/view/slurm_list.py")
87
+ if os.path.getmtime(conf_path + "/view/styles.py") > last_update:
88
+ last_update = os.path.getmtime(conf_path + "/view/styles.py")
89
+ if os.path.getmtime(conf_path + "/readers/slurmreader.py") > last_update:
90
+ last_update = os.path.getmtime(conf_path + "/readers/slurmreader.py")
91
+ except Exception as e:
92
+ orig_instance.err(f"Could not get last update time: {e}")
93
+ last_update = time.time()
92
94
 
93
95
 
94
96
  def get_avg_wait_time(instance: Singleton):
@@ -104,7 +106,8 @@ def get_avg_wait_time(instance: Singleton):
104
106
  instance.err(traceback.format_exc())
105
107
  return 'err', 'err'
106
108
 
107
- def send_msg(instance, msg, n_extra_msgs=0, is_extra=False):
109
+
110
+ def send_msg(instance: Singleton, msg, n_extra_msgs=0, is_extra=False):
108
111
  # msg to bytes
109
112
  msg = msg.encode('utf-8')
110
113
  begin_delim = EXTRA_MSG_BEGIN_DELIM if is_extra else BEGIN_DELIM
@@ -116,11 +119,11 @@ def send_msg(instance, msg, n_extra_msgs=0, is_extra=False):
116
119
 
117
120
  instance.sock.sendto(msg, ('<broadcast>', instance.port))
118
121
 
119
- def update_data_master(instance):
120
- if not instance.check_port_file_master():
121
- instance.log(f"Port file dead, killing current instance")
122
- instance.sock.close()
123
- del instance
122
+
123
+ def update_data_master(instance: Singleton):
124
+ if instance.check_existing_master_running():
125
+ instance.log(f"Master already running, killing current instance")
126
+ instance.cleanup()
124
127
  exit(0)
125
128
 
126
129
  inf = readers.slurmreader.read_infrastructure()
@@ -134,12 +137,11 @@ def update_data_master(instance):
134
137
  queue_jobs = [j.to_nested_dict() for j in jobs if j.state != 'R' and j.state != 'CG']
135
138
  queue_jobs = sorted(queue_jobs, key=lambda x: x['priority'], reverse=True)
136
139
  maxlen = min(JOB_LIMIT_PER_MSG, len(running_jobs) + len(queue_jobs))
137
-
140
+
138
141
  queue_jobs, extra_queue_jobs = queue_jobs[:maxlen - len(running_jobs)], queue_jobs[maxlen - len(running_jobs):]
139
142
  cur_timestamp = time.time()
140
143
  msg = json.dumps({'inf': inf.to_nested_dict(), 'jobs': running_jobs + queue_jobs, 'ts': str(cur_timestamp)})
141
144
 
142
-
143
145
  N_EXTRA_MSGS = (len(extra_queue_jobs) // JOB_LIMIT_PER_MSG) + 1 if len(extra_queue_jobs) > 0 else 0
144
146
  send_msg(instance, msg, n_extra_msgs=N_EXTRA_MSGS)
145
147
  instance.timeme("- main broadcast")
@@ -154,6 +156,7 @@ def update_data_master(instance):
154
156
 
155
157
  return inf, jobs
156
158
 
159
+
157
160
  def decode_msg_slave(data, delim=BEGIN_DELIM):
158
161
  data = data.split(END_DELIM_ENCODED)[0]
159
162
  data = data.decode('utf-8')
@@ -165,8 +168,9 @@ def decode_msg_slave(data, delim=BEGIN_DELIM):
165
168
  avg_wait_time = "N/A" # get_avg_wait_time(instance)
166
169
  return inf, jobs, avg_wait_time, msg
167
170
 
171
+
168
172
  async def get_data_slave(instance):
169
- inf, jobs = None, None
173
+ inf, jobs, avg_wait_time = None, None, None
170
174
  try:
171
175
  # listen for data from master
172
176
  # instance.sock.settimeout(6.5)
@@ -182,24 +186,24 @@ async def get_data_slave(instance):
182
186
  orig_timestamp = float(decoded_msg['ts'])
183
187
 
184
188
  total_msg_n = n_msgs_remaining
185
- msg_store = {i:[] for i in range(total_msg_n)}
186
- while n_msgs_remaining>0:
189
+ msg_store = {i: [] for i in range(total_msg_n)}
190
+ while n_msgs_remaining > 0:
187
191
  data, _ = instance.sock.recvfrom(MAX_BUF)
188
192
  back_msg_idx = int(data.decode('utf-8').split(EXTRA_MSG_BEGIN_DELIM)[0])
189
- msg_idx = total_msg_n - back_msg_idx - 1
193
+ msg_idx = total_msg_n - back_msg_idx - 1
190
194
  _, new_jobs, _, new_ts = decode_msg_slave(data, delim=EXTRA_MSG_BEGIN_DELIM)
191
195
 
192
196
  # check if timestamp is the same
193
197
  if float(new_ts['ts']) != orig_timestamp:
194
198
  instance.timeme(f"Timestamp mismatch, skipping")
195
- return inf, jobs, avg_wait_time # return what we have
196
-
199
+ return inf, jobs, avg_wait_time # return what we have
200
+
197
201
  msg_store[msg_idx] += new_jobs
198
202
 
199
203
  n_msgs_remaining -= 1
200
204
  instance.timeme(f"- extra receive, {n_msgs_remaining} remaining")
201
205
 
202
- # sort and merge
206
+ # sort and merge
203
207
  for msg_idx in range(total_msg_n):
204
208
  if len(msg_store[msg_idx]) == 0:
205
209
  instance.log(f"Empty message {msg_idx}")
@@ -226,8 +230,7 @@ async def get_all():
226
230
  updt = max(updt, os.path.getmtime(conf_path + "/view/styles.py"))
227
231
  updt = max(updt, os.path.getmtime(conf_path + "/readers/slurmreader.py"))
228
232
 
229
- instance = Singleton.getInstance()
230
- instance.timeme(f"Starting update")
233
+ orig_instance.timeme(f"Starting update")
231
234
 
232
235
  if updt > last_update:
233
236
  reload(readers.slurmreader)
@@ -236,21 +239,23 @@ async def get_all():
236
239
  reload(view.styles)
237
240
  last_update = updt
238
241
 
239
- instance.timeme(f"- reload")
242
+ orig_instance.timeme(f"- reload")
240
243
 
241
244
  inf, jobs, avg_wait_time = None, None, None
242
245
  try:
243
- if args.master:
244
- inf, jobs = update_data_master(instance)
246
+ if orig_instance.is_master:
247
+ inf, jobs = update_data_master(orig_instance)
248
+
249
+ time.sleep(5)
245
250
  else:
246
251
  # loop = asyncio.get_event_loop()
247
- instance.timeme(f"- listening for data")
252
+ orig_instance.timeme(f"- listening for data")
248
253
 
249
254
  # wait for data from master but async update the view
250
- inf, jobs, avg_wait_time = await get_data_slave(instance)
255
+ inf, jobs, avg_wait_time = await get_data_slave(orig_instance)
251
256
  except Exception as e:
252
- instance.err(f"Exception: {e}")
253
- instance.err(traceback.format_exc())
257
+ orig_instance.err(f"Exception: {e}")
258
+ orig_instance.err(traceback.format_exc())
254
259
  # instance.rens = 'Something went wrong'
255
260
  # instance.nocc = ':('
256
261
 
@@ -262,43 +267,44 @@ def display_main(stdscr):
262
267
 
263
268
 
264
269
  def _main():
265
- if args.daemon_only:
266
- assert args.master, "Daemon mode only available for master"
267
- instance = Singleton.getInstance()
270
+ if args.master_only:
271
+ assert orig_instance.is_master, "Instance is not `master`, run with `force_override` to override"
268
272
 
269
- instance.log(f"Starting master daemon")
273
+ orig_instance.log(f"Starting master daemon")
270
274
  # register atexit
271
275
  import atexit
272
276
 
273
277
  def exit_handler():
274
- instance.log(f"Exiting...")
275
- instance.sock.close()
278
+ orig_instance.log(f"Exiting...")
279
+ orig_instance.sock.close()
276
280
  # remove .port file
277
- if instance.port_file_exists():
278
- Path(instance.get_port_file_name()[1]).unlink()
281
+ if orig_instance.port_file_exists():
282
+ Path(orig_instance.get_port_file_name()[1]).unlink()
279
283
  atexit.register(exit_handler)
280
284
 
281
285
  while True:
282
- instance.timeme(f"Updating...")
286
+ orig_instance.timeme(f"Updating...")
283
287
  try:
284
- update_data_master(instance)
288
+ update_data_master(orig_instance)
285
289
  except Exception as e:
286
- instance.err(f"Exception: {e}")
287
- instance.err(traceback.format_exc())
290
+ orig_instance.err(f"Exception: {e}")
291
+ orig_instance.err(traceback.format_exc())
288
292
 
289
293
  time.sleep(5)
290
294
 
291
295
  else:
292
296
  # configure singleton
293
297
 
294
- Singleton.getInstance().signature = f"{program_name} v{version_number}"
295
- Singleton.getInstance().version = version_number
296
- Singleton.getInstance().updated = updated
297
- Singleton.getInstance().newest_version = newest_version
298
- Singleton.getInstance().fetch_fn = get_all
298
+ orig_instance.signature = f"{program_name} v{version_number}"
299
+ orig_instance.version = version_number
300
+ orig_instance.updated = updated
301
+ orig_instance.newest_version = newest_version
302
+ orig_instance.fetch_fn = get_all
299
303
 
300
304
  wrapper(display_main)
301
305
 
302
306
 
303
- if __name__ == '__main__':
304
- _main()
307
+ _main()
308
+
309
+ orig_instance.cleanup()
310
+ exit(0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nodeocc
3
- Version: 1.0.9
3
+ Version: 1.0.11
4
4
  Summary: TUI application for viewing the status of GPU allocations on a Slurm cluster
5
5
  Author: Matteo Boschini
6
6
  Maintainer: Lorenzo Bonicelli, Aniello Panariello
@@ -14,6 +14,7 @@ Requires-Python: >=3.9
14
14
  Description-Content-Type: text/markdown
15
15
  Requires-Dist: pandas
16
16
  Requires-Dist: numpy
17
+ Requires-Dist: psutil
17
18
 
18
19
  # Slurm NodeOCC
19
20
  ![[screenshot](https://github.com/apanariello4/slurm_nodeocc/blob/master/sshot.jpg?raw=true)](sshot.jpg)
@@ -1,2 +1,3 @@
1
1
  pandas
2
2
  numpy
3
+ psutil
@@ -1,5 +1,7 @@
1
1
  import asyncio
2
2
  import curses
3
+ import random
4
+ import psutil
3
5
  import grp
4
6
  import logging
5
7
  import os
@@ -10,7 +12,6 @@ import time
10
12
  import traceback
11
13
  from logging.handlers import RotatingFileHandler
12
14
  from pathlib import Path
13
-
14
15
  from view import update_views
15
16
 
16
17
  a_filter_values = [None, 'me', 'prod', 'stud', 'cvcs']
@@ -56,19 +57,40 @@ class Singleton:
56
57
  __instance = None
57
58
 
58
59
  @staticmethod
59
- def getInstance(args=None):
60
+ def getInstance(args=None, create=True):
60
61
  """ Static access method. """
61
62
  if Singleton.__instance is None:
63
+ if not create:
64
+ return None
62
65
  Singleton(args)
63
66
  if args is not None:
64
67
  Singleton.__instance.args = args
65
68
  return Singleton.__instance
66
69
 
67
70
  # destructor
68
- def __del__(self):
69
- if self.args.master:
71
+ def cleanup(self):
72
+ if self.is_master:
70
73
  if self.port_file_exists():
71
- Path(self.get_port_file_name()[1]).unlink()
74
+ os.system(f"rm {self.get_port_file_name()[1]}")
75
+ if hasattr(self, 'sock') and self.sock is not None:
76
+ self.sock.close()
77
+
78
+ def setup_logging(self):
79
+ if self.args.master_only:
80
+ handler = RotatingFileHandler(os.path.join(self.basepath, '.master_log.txt'), maxBytes=5 * 1024 * 1024, backupCount=2, mode='w')
81
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S',
82
+ handlers=[handler])
83
+ # set logging file
84
+ elif self.args.debug:
85
+ # create rotating file handler
86
+ handler = RotatingFileHandler(os.path.expanduser('~') + '/.nodeocc_log.txt', maxBytes=5 * 1024 * 1024, backupCount=2, mode='w')
87
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S',
88
+ handlers=[handler])
89
+
90
+ def clean_port_files(self):
91
+ for f in os.listdir(self.basepath):
92
+ if f.endswith('.port'):
93
+ os.system(f"rm {os.path.join(self.basepath, f)}")
72
94
 
73
95
  def __init__(self, args=None):
74
96
  """ Virtually private constructor. """
@@ -76,6 +98,10 @@ class Singleton:
76
98
  raise Exception("This class is a singleton!")
77
99
  else:
78
100
  Singleton.__instance = self
101
+
102
+ self.args = args
103
+ self.setup_logging()
104
+
79
105
  self.try_open_counter = 0
80
106
  self.voff = 0
81
107
  self.mouse_state = {}
@@ -87,7 +113,6 @@ class Singleton:
87
113
  self.fetch_fn = None
88
114
  self.view_mode = 'gpu'
89
115
  self.job_id_type = 'agg'
90
- self.args = args
91
116
 
92
117
  self.show_account = False
93
118
  self.show_prio = False
@@ -106,36 +131,27 @@ class Singleton:
106
131
 
107
132
  self.basepath = self.args.basepath
108
133
 
109
- if self.args.daemon_only:
110
- handler = RotatingFileHandler(os.path.join(self.basepath, '.master_log.txt'), maxBytes=5 * 1024 * 1024, backupCount=2, mode='w')
111
- logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S',
112
- handlers=[handler])
113
- # set logging file
114
- elif self.args.debug:
115
- # create rotating file handler
116
- handler = RotatingFileHandler(os.path.expanduser('~') + '/.nodeocc_log.txt', maxBytes=5 * 1024 * 1024, backupCount=2, mode='w')
117
- logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S',
118
- handlers=[handler])
134
+ # check if any .port file exists
135
+ if args.force_override and self.port_file_exists():
136
+ _, port_filepath = self.get_port_file_name()
119
137
 
120
- if self.args.master:
121
- # check if any .port file exists
122
- if self.port_file_exists():
123
- if self.args.force_override:
124
- _, port_filepath = self.get_port_file_name()
138
+ # read pid from file
139
+ pid = Path(port_filepath).read_text()
140
+ # kill process
141
+ try:
142
+ os.kill(int(pid), signal.SIGTERM)
143
+ except Exception as e:
144
+ self.log("Could not kill process, deleting port file")
145
+ self.log(e)
125
146
 
126
- # read pid from file
127
- pid = Path(port_filepath).read_text()
128
- # kill process
129
- try:
130
- os.kill(int(pid), signal.SIGTERM)
131
- except Exception as e:
132
- self.log("Could not kill process, deleting port file")
133
- self.log(e)
147
+ # delete port file
148
+ Path(port_filepath).unlink()
134
149
 
135
- # delete port file
136
- Path(port_filepath).unlink()
137
- else:
138
- raise Exception("Master already running")
150
+ self.is_master = not self.check_existing_master_running()
151
+
152
+ if self.is_master:
153
+ # clean up old port files
154
+ self.clean_port_files()
139
155
  self.port = self.create_socket_as_master()
140
156
  else:
141
157
  try_open_socket_as_slave(self)
@@ -146,11 +162,12 @@ class Singleton:
146
162
  return fname, os.path.join(self.basepath, fname)
147
163
  return None
148
164
 
149
- def check_port_file_master(self):
165
+ def check_existing_master_running(self):
150
166
  if self.port_file_exists():
151
- prev_port = int(self.get_port_file_name()[0].split('.')[0])
152
- if hasattr(self, 'port'):
153
- return self.port == prev_port
167
+ # check if process is still running
168
+ pid = Path(self.get_port_file_name()[1]).read_text()
169
+ if pid and int(pid) != os.getpid():
170
+ return psutil.pid_exists(int(pid))
154
171
  return False
155
172
 
156
173
  def port_file_exists(self):
@@ -213,11 +230,11 @@ class Singleton:
213
230
  self._ctime = _ntime
214
231
 
215
232
  def err(self, msg):
216
- if self.args.debug or self.args.daemon_only:
233
+ if self.args.debug or self.args.master_only:
217
234
  logging.error(msg)
218
235
 
219
236
  def log(self, msg):
220
- if self.args.debug or self.args.daemon_only:
237
+ if self.args.debug or self.args.master_only:
221
238
  logging.info(msg)
222
239
 
223
240
  async def fetch(self):
@@ -225,12 +242,14 @@ class Singleton:
225
242
 
226
243
  if self.fetch_fn is not None:
227
244
  inf, jobs, avg_wait_time = await self.fetch_fn() # a_filter_values[self.a_filter])
245
+ master_dead = inf is None
228
246
  self.inf = inf if inf is not None else self.inf
229
247
  self.jobs = jobs if jobs is not None else self.jobs
230
248
  self.avg_wait_time = avg_wait_time if avg_wait_time is not None else self.avg_wait_time
231
249
 
232
250
  _delta_t = time.time() - _ctime
233
251
  self.log(f"Fetch took {_delta_t:.2f} seconds")
252
+ return master_dead
234
253
 
235
254
  def add_button(self, y, x, width, action):
236
255
  if y not in self.mouse_state:
@@ -514,10 +533,25 @@ def get_char_async(stdscr, instance):
514
533
  os.kill(os.getpid(), signal.SIGINT)
515
534
 
516
535
 
517
- async def update_screen_info(stdscr, instance):
536
+ async def update_screen_info(stdscr, instance: Singleton):
518
537
  while instance.k not in BUTTON_ACTIONS['quit']:
519
- await instance.fetch()
520
- instance.log("GOT DATA FROM MASTER")
538
+ master_dead = await instance.fetch()
539
+ if not master_dead:
540
+ instance.log("GOT DATA FROM MASTER")
541
+ else:
542
+ if not instance.check_existing_master_running():
543
+ random_wait_time_ms = random.randint(100, 10000)
544
+ instance.log("MASTER DEAD, WAITING " + str(random_wait_time_ms) + "ms")
545
+
546
+ # WARNING: Possible race-condition here, if master dies and slave tries to become master
547
+ await asyncio.sleep(random_wait_time_ms / 1000)
548
+ if not instance.check_existing_master_running():
549
+ instance.log("MASTER STILL DEAD, TRYING TO BECOME MASTER")
550
+ instance.setup_logging()
551
+ instance.clean_port_files()
552
+ instance.port = instance.create_socket_as_master()
553
+ instance.is_master = True
554
+
521
555
  update_screen(stdscr, instance)
522
556
 
523
557
 
@@ -592,7 +626,7 @@ async def main(stdscr):
592
626
 
593
627
  def exit_handler(sig, frame):
594
628
  instance.log(f"FORCED EXIT...")
595
- instance.sock.close()
629
+ instance.cleanup()
596
630
  exit(0)
597
631
  signal.signal(signal.SIGINT, exit_handler)
598
632
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes