nodeocc 1.0.23__tar.gz → 1.0.25__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.23 → nodeocc-1.0.25}/PKG-INFO +2 -2
  2. {nodeocc-1.0.23 → nodeocc-1.0.25}/pyproject.toml +1 -1
  3. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/controller/controller.py +118 -44
  4. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/PKG-INFO +2 -2
  5. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/readers/slurmreader.py +41 -11
  6. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/__init__.py +2 -0
  7. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/curses_multiwindow.py +98 -43
  8. {nodeocc-1.0.23 → nodeocc-1.0.25}/README.md +0 -0
  9. {nodeocc-1.0.23 → nodeocc-1.0.25}/setup.cfg +0 -0
  10. {nodeocc-1.0.23 → nodeocc-1.0.25}/setup.py +0 -0
  11. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/__init__.py +0 -0
  12. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/model/infrastructure.py +0 -0
  13. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/model/job.py +0 -0
  14. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/SOURCES.txt +0 -0
  15. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/dependency_links.txt +0 -0
  16. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/entry_points.txt +0 -0
  17. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/requires.txt +0 -0
  18. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/top_level.txt +0 -0
  19. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/readers/compute_wait_time.py +0 -0
  20. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/slurm_list.py +0 -0
  21. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/slurm_viz.py +0 -0
  22. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/styles.py +0 -0
  23. {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/utils.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: nodeocc
3
- Version: 1.0.23
3
+ Version: 1.0.25
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nodeocc"
7
- version = "1.0.23"
7
+ version = "1.0.25"
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"
@@ -1,3 +1,4 @@
1
+ import random
1
2
  import sys
2
3
  import os
3
4
  from pathlib import Path
@@ -7,7 +8,7 @@ import numpy as np
7
8
 
8
9
  conf_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9
10
  print(conf_path)
10
- sys.path.append(conf_path)
11
+ sys.path.insert(0, conf_path)
11
12
  from curses import wrapper
12
13
  from view.curses_multiwindow import main, Singleton, try_open_socket_as_slave
13
14
 
@@ -22,6 +23,7 @@ import time
22
23
  import json
23
24
  import argparse
24
25
  import traceback
26
+ import socket
25
27
  from model.infrastructure import Infrastructure
26
28
  from model.job import Job
27
29
  import asyncio
@@ -64,13 +66,16 @@ BEGIN_DELIM = "!{$"
64
66
  END_DELIM_ENCODED = "!}$".encode('utf-8')
65
67
  EXTRA_MSG_BEGIN_DELIM = "!{#"
66
68
  MAX_BUF = 65535
67
- MAX_MSG_LEN = 1024 * 1024 * 1024
68
- JOB_LIMIT_PER_MSG = 60
69
+ JOB_LIMIT_PER_MSG = 40
70
+ TIMEOUT_MAX = 3
71
+ n_timeout = 0
69
72
 
70
73
  parser = argparse.ArgumentParser(description='Visualize slurm jobs')
71
74
  parser.add_argument('--debug', action='store_true', help='Enable logging')
75
+ parser.add_argument('--override', action='store_true', help='Enable logging')
72
76
  parser.add_argument('--master_only', action='store_true', help='Disable all prints - only run in background')
73
77
  parser.add_argument('--basepath', type=str, default='/tmp', help='Base path for nodeocc. Must be readable by all users')
78
+ parser.add_argument('--diag', action='store_true', help='Run one-shot diagnostics and exit')
74
79
  args = parser.parse_args()
75
80
 
76
81
  # check basepath is readable
@@ -168,54 +173,77 @@ def decode_msg_slave(data, delim=BEGIN_DELIM):
168
173
 
169
174
 
170
175
  async def get_data_slave(instance):
176
+ global n_timeout
171
177
  inf, jobs, avg_wait_time = None, None, None
172
178
  try:
173
179
  # listen for data from master
174
180
  # instance.sock.settimeout(6.5)
175
181
  data, _ = instance.sock.recvfrom(MAX_BUF)
176
- decoded_data = data.decode('utf-8')
177
- if BEGIN_DELIM in decoded_data:
178
- n_msgs_remaining = int(decoded_data.split(BEGIN_DELIM)[0])
179
- # msg_len += len(str(msg_len)) message len is deprecated and used for n_msgs_remaining
180
- instance.timeme(f"received first message of {n_msgs_remaining} (total len {len(data)} bytes)")
181
-
182
- inf, jobs, avg_wait_time, decoded_msg = decode_msg_slave(data)
183
- instance.timeme(f"- receive")
184
- orig_timestamp = float(decoded_msg['ts'])
185
-
186
- total_msg_n = n_msgs_remaining
187
- msg_store = {i: [] for i in range(total_msg_n)}
188
- while n_msgs_remaining > 0:
189
- data, _ = instance.sock.recvfrom(MAX_BUF)
190
- back_msg_idx = int(data.decode('utf-8').split(EXTRA_MSG_BEGIN_DELIM)[0])
191
- msg_idx = total_msg_n - back_msg_idx - 1
192
- _, new_jobs, _, new_ts = decode_msg_slave(data, delim=EXTRA_MSG_BEGIN_DELIM)
193
-
194
- # check if timestamp is the same
195
- if float(new_ts['ts']) != orig_timestamp:
196
- instance.timeme(f"Timestamp mismatch, skipping")
197
- return inf, jobs, avg_wait_time # return what we have
198
-
199
- msg_store[msg_idx] += new_jobs
200
-
201
- n_msgs_remaining -= 1
202
- instance.timeme(f"- extra receive, {n_msgs_remaining} remaining")
203
-
204
- # sort and merge
205
- for msg_idx in range(total_msg_n):
206
- if len(msg_store[msg_idx]) == 0:
207
- instance.log(f"Empty message {msg_idx}")
208
- jobs += msg_store[msg_idx]
209
- else:
210
- instance.timeme(f"- no data")
211
- return None, None, None, None
212
-
213
- except BlockingIOError as e:
182
+ decoded_data = data.decode('utf-8', errors='replace')
183
+ if BEGIN_DELIM not in decoded_data:
184
+ if EXTRA_MSG_BEGIN_DELIM in decoded_data:
185
+ instance.log("Received extra packet before main packet, discarding")
186
+ else:
187
+ instance.log("Received packet without known delimiters, discarding")
188
+ return None, None, None
189
+
190
+ n_msgs_remaining = int(decoded_data.split(BEGIN_DELIM)[0])
191
+ # msg_len += len(str(msg_len)) message len is deprecated and used for n_msgs_remaining
192
+ instance.timeme(f"received first message of {n_msgs_remaining} (total len {len(data)} bytes)")
193
+
194
+ inf, jobs, avg_wait_time, decoded_msg = decode_msg_slave(data)
195
+ instance.timeme(f"- receive")
196
+ orig_timestamp = float(decoded_msg['ts'])
197
+
198
+ total_msg_n = n_msgs_remaining
199
+ msg_store = {i: [] for i in range(total_msg_n)}
200
+ while n_msgs_remaining > 0:
201
+ data, _ = instance.sock.recvfrom(MAX_BUF)
202
+ decoded_extra = data.decode('utf-8', errors='replace')
203
+ if EXTRA_MSG_BEGIN_DELIM not in decoded_extra:
204
+ instance.log("Received non-extra packet while waiting for extra packets, skipping")
205
+ continue
206
+
207
+ back_msg_idx = int(decoded_extra.split(EXTRA_MSG_BEGIN_DELIM)[0])
208
+ msg_idx = total_msg_n - back_msg_idx - 1
209
+ if msg_idx < 0 or msg_idx >= total_msg_n:
210
+ instance.log(f"Invalid extra packet index: {msg_idx}, skipping")
211
+ continue
212
+
213
+ _, new_jobs, _, new_ts = decode_msg_slave(data, delim=EXTRA_MSG_BEGIN_DELIM)
214
+
215
+ # check if timestamp is the same
216
+ if float(new_ts['ts']) != orig_timestamp:
217
+ instance.timeme(f"Timestamp mismatch, skipping")
218
+ return inf, jobs, avg_wait_time # return what we have
219
+
220
+ msg_store[msg_idx] += new_jobs
221
+
222
+ n_msgs_remaining -= 1
223
+ instance.timeme(f"- extra receive, {n_msgs_remaining} remaining")
224
+
225
+ # sort and merge
226
+ for msg_idx in range(total_msg_n):
227
+ if len(msg_store[msg_idx]) == 0:
228
+ instance.log(f"Empty message {msg_idx}")
229
+ jobs += msg_store[msg_idx]
230
+
231
+ n_timeout = 0
232
+
233
+ except BlockingIOError:
214
234
  pass
215
235
 
216
- except TimeoutError as e:
217
- instance.log(f"TIMEOUT")
218
- try_open_socket_as_slave(instance)
236
+ except TimeoutError:
237
+ n_timeout += 1
238
+ instance.log(f"TIMEOUT {n_timeout}/{TIMEOUT_MAX}")
239
+ if n_timeout > TIMEOUT_MAX:
240
+ try_open_socket_as_slave(instance, force=True)
241
+
242
+ time.sleep(random.randint(100, 10000) / 1000)
243
+
244
+ except Exception as e:
245
+ instance.err(f"Exception while receiving data from master: {e}")
246
+ instance.err(traceback.format_exc())
219
247
 
220
248
  return inf, jobs, avg_wait_time
221
249
 
@@ -264,7 +292,53 @@ def display_main(stdscr):
264
292
  return asyncio.run(main(stdscr))
265
293
 
266
294
 
295
+ def run_diagnostics(instance: Singleton):
296
+ hostname = socket.getfqdn().split('.')[0]
297
+ print(f"Diagnostics for nodeocc on host: {hostname}")
298
+ print(f"Base path: {instance.basepath}")
299
+ print(f"Role detected: {'master' if instance.is_master else 'slave'}")
300
+ print("")
301
+
302
+ try:
303
+ port_files = instance.get_port_files()
304
+ print(f"Visible port files: {len(port_files)}")
305
+ for pf in port_files:
306
+ try:
307
+ pid = Path(pf).read_text().strip()
308
+ except Exception:
309
+ pid = "?"
310
+ print(f" - {pf} (pid={pid})")
311
+ except Exception as e:
312
+ print(f"Could not list port files: {e}")
313
+
314
+ print("")
315
+
316
+ if instance.is_master:
317
+ print("Running one master update cycle...")
318
+ try:
319
+ inf, jobs = update_data_master(instance)
320
+ print(f"Master update OK. Nodes={len(inf.nodes)}, Jobs={len(jobs)}, Port={instance.port}")
321
+ except Exception as e:
322
+ print(f"Master update FAILED: {e}")
323
+ print(traceback.format_exc())
324
+ else:
325
+ print("Waiting for one packet from master...")
326
+ try:
327
+ inf, jobs, _ = asyncio.run(get_data_slave(instance))
328
+ if inf is None or jobs is None:
329
+ print("No valid payload received from master.")
330
+ else:
331
+ print(f"Slave receive OK. Jobs in snapshot={len(jobs)}")
332
+ except Exception as e:
333
+ print(f"Slave receive FAILED: {e}")
334
+ print(traceback.format_exc())
335
+
336
+
267
337
  def _main():
338
+ if args.diag:
339
+ run_diagnostics(orig_instance)
340
+ return
341
+
268
342
  if args.master_only:
269
343
  assert orig_instance.is_master, "Instance is not `master`, run with `force_override` to override"
270
344
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: nodeocc
3
- Version: 1.0.23
3
+ Version: 1.0.25
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
@@ -45,24 +45,54 @@ def _node_from_sinfo(x):
45
45
  return n
46
46
 
47
47
 
48
- def _parse_gpu_models(line):
49
- line = line[0].split(',')
50
- if line[0] == '(null)':
48
+ def _parse_gpu_count(gres):
49
+ if not isinstance(gres, str):
50
+ return 0
51
+ gres = gres.strip()
52
+ if gres in ('(null)', ''):
53
+ return 0
54
+
55
+ n_gpu = 0
56
+ for token in gres.split(','):
57
+ token = token.strip()
58
+ if not token.lower().startswith('gpu'):
59
+ continue
60
+ last_field = token.split(':')[-1].split('(')[0]
61
+ match = re.search(r'\d+', last_field)
62
+ n_gpu += int(match.group(0)) if match else 1
63
+ return n_gpu
64
+
65
+
66
+ def _parse_gpu_models(gres):
67
+ if not isinstance(gres, str):
51
68
  return 'n/a'
52
- else:
53
- if len(line) == 1:
54
- return line[0].split(':')[1]
69
+ gres = gres.strip()
70
+ if gres in ('(null)', ''):
71
+ return 'n/a'
72
+
73
+ models = []
74
+ for token in gres.split(','):
75
+ token = token.strip()
76
+ if not token.lower().startswith('gpu'):
77
+ continue
78
+ parts = token.split(':')
79
+ if len(parts) >= 3:
80
+ model = parts[1]
81
+ elif len(parts) == 2:
82
+ model = 'gpu'
55
83
  else:
56
- # return line[0].split(':')[1] + ',' + line[1].split(':')[1]
57
- return ','.join([x.split(':')[1] for x in line])
84
+ model = 'n/a'
85
+ if model not in models:
86
+ models.append(model)
87
+
88
+ return ','.join(models) if models else 'n/a'
58
89
 
59
90
 
60
91
  def _read_nodes(reservations=None, pending_res=None, return_raw=False):
61
92
  sinfo_df = pd.read_csv(StringIO(os.popen(r'sinfo -N -o "%N;%G;%t;%m;%c;%f"').read()), sep=';')
62
93
  sinfo_df = _split_column(sinfo_df, 'NODELIST').drop_duplicates()
63
- sinfo_df.loc[:, 'n_gpu'] = sinfo_df['GRES'].apply(lambda x: 0 if x.split('(')[0].split(':')[0] in ('(null)', '') else int(x.split('(')[0].split(':')[-1]))
64
- # sinfo_df['m_gpu'] = sinfo_df['GRES'].str.split(':').apply(lambda x: 'n/a' if x[0] in ('(null)', '') else x[1])
65
- sinfo_df.loc[:, 'm_gpu'] = sinfo_df['GRES'].str.split(';').apply(_parse_gpu_models)
94
+ sinfo_df.loc[:, 'n_gpu'] = sinfo_df['GRES'].apply(_parse_gpu_count)
95
+ sinfo_df.loc[:, 'm_gpu'] = sinfo_df['GRES'].apply(_parse_gpu_models)
66
96
  sinfo_df['reserved'] = 'no'
67
97
  if reservations is not None:
68
98
  sinfo_df.loc[sinfo_df['NODELIST'].isin(reservations.Nodes.unique()), 'reserved'] = 'yes'
@@ -3,6 +3,8 @@ from . import slurm_list, slurm_viz, styles
3
3
 
4
4
  def update_views(stdscr, instance, filter):
5
5
  if instance.jobs is None or instance.inf is None:
6
+ instance.rens = "No data received from master yet.\nTry running with --debug or --diag."
7
+ instance.nocc = "Waiting for infrastructure snapshot..."
6
8
  return
7
9
 
8
10
  instance.rens = slurm_list.view_list(
@@ -44,12 +44,49 @@ def is_file_readable_byall(file_path):
44
44
  return os.stat(file_path).st_mode & stat.S_IROTH
45
45
 
46
46
 
47
- def try_open_socket_as_slave(instance):
47
+ def is_nodeocc_process(pid):
48
+ try:
49
+ cmdline = ' '.join(psutil.Process(int(pid)).cmdline())
50
+ return ('nodeocc' in cmdline) or ('controller.py' in cmdline)
51
+ except Exception:
52
+ return False
53
+
54
+
55
+ def port_from_portfile(portfile_path):
56
+ try:
57
+ return int(Path(portfile_path).name.split('.')[0].split('master_')[1])
58
+ except Exception:
59
+ return None
60
+
61
+
62
+ def process_has_udp_port(pid, port):
63
+ if port is None:
64
+ return False
65
+ try:
66
+ proc = psutil.Process(int(pid))
67
+ for conn in proc.connections(kind='udp'):
68
+ if conn.laddr and conn.laddr.port == int(port):
69
+ return True
70
+ except Exception:
71
+ return False
72
+ return False
73
+
74
+
75
+ def try_open_socket_as_slave(instance, force=False):
48
76
  if len(instance.get_port_files()) == 0:
49
77
  raise Exception("No master running")
50
78
 
51
- cur_port = [int(f.split('.')[0].split('master_')[1]) for f in instance.get_port_files()
52
- if (pid := Path(f).read_text()) and psutil.pid_exists(int(pid))][0]
79
+ active_ports = []
80
+ for f in instance.get_port_files():
81
+ pid = Path(f).read_text().strip()
82
+ port = port_from_portfile(f)
83
+ if pid and is_nodeocc_process(pid) and process_has_udp_port(pid, port):
84
+ active_ports.append(port)
85
+
86
+ if len(active_ports) == 0:
87
+ raise Exception("No active master process found from port files")
88
+
89
+ cur_port = active_ports[0]
53
90
 
54
91
  if hasattr(instance, 'port'):
55
92
  # check if port file has same name
@@ -57,7 +94,7 @@ def try_open_socket_as_slave(instance):
57
94
  instance.log(f"- MASTER HAS CHANGED PORT: {instance.port} vs {cur_port}")
58
95
 
59
96
  instance.port = cur_port
60
- instance._open_socket_as_slave(instance.port)
97
+ instance._open_socket_as_slave(instance.port, force=force)
61
98
 
62
99
  if instance.try_open_counter > 5:
63
100
  instance.err(f"Could not open socket as slave on port {instance.port}")
@@ -90,21 +127,26 @@ class Singleton:
90
127
  logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S',
91
128
  handlers=[handler])
92
129
 
93
- def clean_port_files(self):
130
+ def clean_port_files(self, force=False):
94
131
  nodename = socket.getfqdn().split('.')[0]
95
132
  bp = os.path.join(self.basepath, nodename)
96
133
  if not os.path.exists(bp):
97
134
  return True
98
135
  for f in os.listdir(bp):
99
- if 'master_' in f and f.endswith('.portfile') and is_file_writable_byall(os.path.join(bp, f)):
100
- # read pid from file
101
- pid = Path(os.path.join(bp, f)).read_text()
102
- # assert no process is running
103
- if psutil.pid_exists(int(pid)):
104
- self.log(f"Process {pid} is still running, killing self")
136
+ fp = os.path.join(bp, f)
137
+ if 'master_' in f and f.endswith('.portfile') and is_file_writable_byall(fp):
138
+ pid = Path(fp).read_text().strip()
139
+ port = port_from_portfile(fp)
140
+ if not pid:
141
+ os.remove(fp)
142
+ continue
143
+ if pid == str(os.getpid()):
144
+ os.remove(fp)
145
+ continue
146
+ if (not force) and is_nodeocc_process(pid) and process_has_udp_port(pid, port):
147
+ self.log(f"Process {pid} is still running, keeping existing master")
105
148
  return False
106
-
107
- os.system(f"rm {os.path.join(bp, f)}")
149
+ os.remove(fp)
108
150
  return True
109
151
 
110
152
  def __init__(self, args=None):
@@ -115,6 +157,7 @@ class Singleton:
115
157
  Singleton.__instance = self
116
158
 
117
159
  self.args = args
160
+ self.basepath = self.args.basepath
118
161
  self.setup_logging()
119
162
 
120
163
  self.try_open_counter = 0
@@ -141,15 +184,17 @@ class Singleton:
141
184
  self.k = -1
142
185
 
143
186
  # check if user group is 'student' or 'tesisti'
144
- user_group = grp.getgrgid(os.getgid()).gr_name
187
+ try:
188
+ user_group = grp.getgrgid(os.getgid()).gr_name
189
+ except KeyError:
190
+ user_group = ''
191
+ self.err(f"Could not resolve gid {os.getgid()} via grp.getgrgid")
145
192
  self.cur_partition = 'stud' if user_group in ['studenti', 'tesisti'] else 'prod'
146
193
 
147
- self.basepath = self.args.basepath
148
-
149
194
  self.is_master = not self.check_existing_master_running()
150
195
 
151
- if self.is_master:
152
- self.port = self.create_socket_as_master()
196
+ if self.is_master or args.override:
197
+ self.port = self.create_socket_as_master(force=args.override)
153
198
  else:
154
199
  try_open_socket_as_slave(self)
155
200
 
@@ -158,9 +203,16 @@ class Singleton:
158
203
  if len(portfiles) > 0:
159
204
  for f in portfiles:
160
205
  # check if process is still running
161
- pid = Path(f).read_text()
162
- if pid and int(pid) != os.getpid():
163
- return psutil.pid_exists(int(pid))
206
+ pid = Path(f).read_text().strip()
207
+ if not pid:
208
+ continue
209
+ try:
210
+ pid_int = int(pid)
211
+ except ValueError:
212
+ continue
213
+ port = port_from_portfile(f)
214
+ if pid_int != os.getpid() and is_nodeocc_process(pid_int) and process_has_udp_port(pid_int, port):
215
+ return True
164
216
  return False
165
217
 
166
218
  def get_port_files(self):
@@ -176,31 +228,12 @@ class Singleton:
176
228
  portfiles = [f for f in portfiles if is_file_readable_byall(f) and is_file_writable_byall(f)]
177
229
  return portfiles
178
230
 
179
- def create_socket_as_master(self):
231
+ def update_port_file(self, force=False):
180
232
  nodename = socket.getfqdn().split('.')[0]
181
233
 
182
- # create udp socket for broadcasting
183
- self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
184
-
185
- self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
186
-
187
- # Enable broadcasting mode
188
- self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
189
-
190
- self.sock.bind(('', 0))
191
-
192
- self.port = self.sock.getsockname()[1]
193
- self.sock.settimeout(2)
194
- self.log(f"Socket created as master on port {self.port}")
195
-
196
234
  # get pid of current process
197
235
  self.pid = os.getpid()
198
236
 
199
- # clean up old port files
200
- kill_ok = self.clean_port_files()
201
- if not kill_ok:
202
- return None
203
-
204
237
  try:
205
238
  # create file to store port with 666 permissions to file
206
239
  # file is in folder basepath/nodename/master_{port}.portfile
@@ -225,12 +258,34 @@ class Singleton:
225
258
  # check if file exists
226
259
  if filepath.exists():
227
260
  # delete file
228
- os.system(f"rm {filepath}")
261
+ os.remove(filepath)
229
262
  return None
230
263
 
264
+ def create_socket_as_master(self, force=False):
265
+ # clean up old port files before creating the socket to avoid inconsistent state
266
+ kill_ok = self.clean_port_files(force=force)
267
+ if not kill_ok:
268
+ return None
269
+
270
+ # create udp socket for broadcasting
271
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
272
+
273
+ self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
274
+
275
+ # Enable broadcasting mode
276
+ self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
277
+
278
+ self.sock.bind(('', 0))
279
+
280
+ self.port = self.sock.getsockname()[1]
281
+ self.sock.settimeout(2)
282
+ self.log(f"Socket created as master on port {self.port}")
283
+
284
+ self.update_port_file(force=force)
285
+
231
286
  return self.port
232
287
 
233
- def _open_socket_as_slave(self, port):
288
+ def _open_socket_as_slave(self, port, force=False):
234
289
  try:
235
290
  # create udp socket for broadcasting
236
291
  self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes