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.
- {nodeocc-1.0.23 → nodeocc-1.0.25}/PKG-INFO +2 -2
- {nodeocc-1.0.23 → nodeocc-1.0.25}/pyproject.toml +1 -1
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/controller/controller.py +118 -44
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/PKG-INFO +2 -2
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/readers/slurmreader.py +41 -11
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/__init__.py +2 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/curses_multiwindow.py +98 -43
- {nodeocc-1.0.23 → nodeocc-1.0.25}/README.md +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/setup.cfg +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/setup.py +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/__init__.py +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/model/infrastructure.py +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/model/job.py +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/SOURCES.txt +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/dependency_links.txt +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/entry_points.txt +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/requires.txt +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/nodeocc.egg-info/top_level.txt +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/readers/compute_wait_time.py +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/slurm_list.py +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/slurm_viz.py +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/styles.py +0 -0
- {nodeocc-1.0.23 → nodeocc-1.0.25}/src/view/utils.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nodeocc"
|
|
7
|
-
version = "1.0.
|
|
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.
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
|
@@ -45,24 +45,54 @@ def _node_from_sinfo(x):
|
|
|
45
45
|
return n
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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(
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
pid = Path(
|
|
102
|
-
|
|
103
|
-
if
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
163
|
-
|
|
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
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|