wnm 0.0.12__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.
Potentially problematic release.
This version of wnm might be problematic. Click here for more details.
- wnm/__init__.py +3 -0
- wnm/__main__.py +236 -0
- wnm/actions.py +45 -0
- wnm/common.py +23 -0
- wnm/config.py +673 -0
- wnm/decision_engine.py +388 -0
- wnm/executor.py +1299 -0
- wnm/firewall/__init__.py +13 -0
- wnm/firewall/base.py +71 -0
- wnm/firewall/factory.py +95 -0
- wnm/firewall/null_firewall.py +71 -0
- wnm/firewall/ufw_manager.py +118 -0
- wnm/migration.py +42 -0
- wnm/models.py +459 -0
- wnm/process_managers/__init__.py +23 -0
- wnm/process_managers/base.py +203 -0
- wnm/process_managers/docker_manager.py +371 -0
- wnm/process_managers/factory.py +83 -0
- wnm/process_managers/launchd_manager.py +592 -0
- wnm/process_managers/setsid_manager.py +340 -0
- wnm/process_managers/systemd_manager.py +529 -0
- wnm/reports.py +286 -0
- wnm/utils.py +407 -0
- wnm/wallets.py +177 -0
- wnm-0.0.12.dist-info/METADATA +367 -0
- wnm-0.0.12.dist-info/RECORD +29 -0
- wnm-0.0.12.dist-info/WHEEL +5 -0
- wnm-0.0.12.dist-info/entry_points.txt +2 -0
- wnm-0.0.12.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SetsidManager: Manage nodes as background processes using setsid.
|
|
3
|
+
|
|
4
|
+
This manager runs nodes as simple background processes without requiring
|
|
5
|
+
sudo privileges or systemd. Suitable for development and non-systemd environments.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import signal
|
|
12
|
+
import subprocess
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import psutil
|
|
17
|
+
|
|
18
|
+
from wnm.common import DEAD, RESTARTING, RUNNING, STOPPED, UPGRADING
|
|
19
|
+
from wnm.config import BOOTSTRAP_CACHE_DIR
|
|
20
|
+
from wnm.models import Node
|
|
21
|
+
from wnm.process_managers.base import NodeProcess, ProcessManager
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SetsidManager(ProcessManager):
|
|
25
|
+
"""Manage nodes as background processes via setsid"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, session_factory=None, firewall_type: str = None):
|
|
28
|
+
"""
|
|
29
|
+
Initialize SetsidManager.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
session_factory: SQLAlchemy session factory (optional, for status updates)
|
|
33
|
+
firewall_type: Type of firewall to use (defaults to auto-detect)
|
|
34
|
+
"""
|
|
35
|
+
super().__init__(firewall_type)
|
|
36
|
+
self.S = session_factory
|
|
37
|
+
|
|
38
|
+
def _get_pid_file(self, node: Node) -> Path:
|
|
39
|
+
"""Get path to PID file for a node"""
|
|
40
|
+
return Path(node.root_dir) / "node.pid"
|
|
41
|
+
|
|
42
|
+
def _write_pid_file(self, node: Node, pid: int):
|
|
43
|
+
"""Write PID to file"""
|
|
44
|
+
pid_file = self._get_pid_file(node)
|
|
45
|
+
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
pid_file.write_text(str(pid))
|
|
47
|
+
|
|
48
|
+
def _read_pid_file(self, node: Node) -> int | None:
|
|
49
|
+
"""Read PID from file, returns None if file doesn't exist or is invalid"""
|
|
50
|
+
pid_file = self._get_pid_file(node)
|
|
51
|
+
try:
|
|
52
|
+
if pid_file.exists():
|
|
53
|
+
pid = int(pid_file.read_text().strip())
|
|
54
|
+
# Verify process exists
|
|
55
|
+
if psutil.pid_exists(pid):
|
|
56
|
+
return pid
|
|
57
|
+
# Stale PID file, remove it
|
|
58
|
+
pid_file.unlink()
|
|
59
|
+
except (ValueError, OSError) as e:
|
|
60
|
+
logging.debug(f"Failed to read PID file {pid_file}: {e}")
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def create_node(self, node: Node, binary_path: str) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Create and start a new node as a background process.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
node: Node database record with configuration
|
|
69
|
+
binary_path: Path to the antnode binary
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if node was created successfully
|
|
73
|
+
"""
|
|
74
|
+
logging.info(f"Creating setsid node {node.id}")
|
|
75
|
+
|
|
76
|
+
# Create directories
|
|
77
|
+
node_dir = Path(node.root_dir)
|
|
78
|
+
log_dir = node_dir / "logs"
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
node_dir.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
except OSError as err:
|
|
84
|
+
logging.error(f"Failed to create directories: {err}")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
# Copy binary to node directory
|
|
88
|
+
binary_dest = node_dir / "antnode"
|
|
89
|
+
try:
|
|
90
|
+
shutil.copy2(binary_path, binary_dest)
|
|
91
|
+
binary_dest.chmod(0o755)
|
|
92
|
+
except (OSError, shutil.Error) as err:
|
|
93
|
+
logging.error(f"Failed to copy binary: {err}")
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
# Start the node
|
|
97
|
+
return self.start_node(node)
|
|
98
|
+
|
|
99
|
+
def start_node(self, node: Node) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Start a node as a background process.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
node: Node database record
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
True if node started successfully
|
|
108
|
+
"""
|
|
109
|
+
logging.info(f"Starting setsid node {node.id}")
|
|
110
|
+
|
|
111
|
+
# Check if already running
|
|
112
|
+
if self._read_pid_file(node):
|
|
113
|
+
logging.warning(f"Node {node.id} already running")
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
# Prepare command
|
|
117
|
+
binary = Path(node.root_dir) / "antnode"
|
|
118
|
+
if not binary.exists():
|
|
119
|
+
logging.error(f"Binary not found: {binary}")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
log_dir = Path(node.root_dir) / "logs"
|
|
123
|
+
|
|
124
|
+
cmd = [
|
|
125
|
+
str(binary),
|
|
126
|
+
"--bootstrap-cache-dir",
|
|
127
|
+
BOOTSTRAP_CACHE_DIR,
|
|
128
|
+
"--root-dir",
|
|
129
|
+
node.root_dir,
|
|
130
|
+
"--port",
|
|
131
|
+
str(node.port),
|
|
132
|
+
"--enable-metrics-server",
|
|
133
|
+
"--metrics-server-port",
|
|
134
|
+
str(node.metrics_port),
|
|
135
|
+
"--log-output-dest",
|
|
136
|
+
str(log_dir),
|
|
137
|
+
"--max-log-files",
|
|
138
|
+
"1",
|
|
139
|
+
"--max-archived-log-files",
|
|
140
|
+
"1",
|
|
141
|
+
"--rewards-address",
|
|
142
|
+
node.wallet,
|
|
143
|
+
node.network,
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
# Start process in background using setsid
|
|
147
|
+
try:
|
|
148
|
+
# Use setsid to detach from terminal
|
|
149
|
+
process = subprocess.Popen(
|
|
150
|
+
["setsid"] + cmd,
|
|
151
|
+
stdout=subprocess.DEVNULL,
|
|
152
|
+
stderr=subprocess.DEVNULL,
|
|
153
|
+
stdin=subprocess.DEVNULL,
|
|
154
|
+
start_new_session=True,
|
|
155
|
+
env={
|
|
156
|
+
**os.environ,
|
|
157
|
+
**({"CUSTOM_ENV": node.environment} if node.environment else {}),
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Give process a moment to start
|
|
162
|
+
time.sleep(0.5)
|
|
163
|
+
|
|
164
|
+
# Check if process started successfully
|
|
165
|
+
if process.poll() is not None:
|
|
166
|
+
logging.error(
|
|
167
|
+
f"Process exited immediately with code {process.returncode}"
|
|
168
|
+
)
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
# Get actual PID of the antnode process (not setsid wrapper)
|
|
172
|
+
# The setsid process becomes the session leader, we need to find the child
|
|
173
|
+
time.sleep(1) # Give process time to spawn
|
|
174
|
+
|
|
175
|
+
# Find the antnode process by command line
|
|
176
|
+
for proc in psutil.process_iter(["pid", "cmdline"]):
|
|
177
|
+
try:
|
|
178
|
+
cmdline = proc.info["cmdline"]
|
|
179
|
+
if (
|
|
180
|
+
cmdline
|
|
181
|
+
and "antnode" in cmdline[0]
|
|
182
|
+
and str(node.port) in " ".join(cmdline)
|
|
183
|
+
):
|
|
184
|
+
pid = proc.info["pid"]
|
|
185
|
+
self._write_pid_file(node, pid)
|
|
186
|
+
logging.info(f"Node {node.id} started with PID {pid}")
|
|
187
|
+
break
|
|
188
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
189
|
+
continue
|
|
190
|
+
else:
|
|
191
|
+
logging.warning(
|
|
192
|
+
f"Could not find PID for node {node.id}, using setsid PID"
|
|
193
|
+
)
|
|
194
|
+
self._write_pid_file(node, process.pid)
|
|
195
|
+
|
|
196
|
+
except (subprocess.SubprocessError, OSError) as err:
|
|
197
|
+
logging.error(f"Failed to start node: {err}")
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
# Open firewall port (best effort, may fail without sudo)
|
|
201
|
+
self.enable_firewall_port(node.port)
|
|
202
|
+
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
def stop_node(self, node: Node) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
Stop a node process.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
node: Node database record
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
True if node stopped successfully
|
|
214
|
+
"""
|
|
215
|
+
logging.info(f"Stopping setsid node {node.id}")
|
|
216
|
+
|
|
217
|
+
pid = self._read_pid_file(node)
|
|
218
|
+
if not pid:
|
|
219
|
+
logging.warning(f"No PID found for node {node.id}")
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
# Try graceful shutdown first
|
|
224
|
+
process = psutil.Process(pid)
|
|
225
|
+
process.terminate()
|
|
226
|
+
|
|
227
|
+
# Wait up to 10 seconds for graceful shutdown
|
|
228
|
+
try:
|
|
229
|
+
process.wait(timeout=10)
|
|
230
|
+
except psutil.TimeoutExpired:
|
|
231
|
+
logging.warning(f"Node {node.id} did not terminate gracefully, killing")
|
|
232
|
+
process.kill()
|
|
233
|
+
process.wait(timeout=5)
|
|
234
|
+
|
|
235
|
+
# Remove PID file
|
|
236
|
+
pid_file = self._get_pid_file(node)
|
|
237
|
+
if pid_file.exists():
|
|
238
|
+
pid_file.unlink()
|
|
239
|
+
|
|
240
|
+
except psutil.NoSuchProcess:
|
|
241
|
+
logging.debug(f"Process {pid} already terminated")
|
|
242
|
+
except psutil.AccessDenied as err:
|
|
243
|
+
logging.error(f"Access denied stopping process {pid}: {err}")
|
|
244
|
+
return False
|
|
245
|
+
except Exception as err:
|
|
246
|
+
logging.error(f"Failed to stop node: {err}")
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
# Close firewall port
|
|
250
|
+
self.disable_firewall_port(node.port)
|
|
251
|
+
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
def restart_node(self, node: Node) -> bool:
|
|
255
|
+
"""
|
|
256
|
+
Restart a node.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
node: Node database record
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
True if node restarted successfully
|
|
263
|
+
"""
|
|
264
|
+
logging.info(f"Restarting setsid node {node.id}")
|
|
265
|
+
|
|
266
|
+
self.stop_node(node)
|
|
267
|
+
time.sleep(1) # Brief pause between stop and start
|
|
268
|
+
return self.start_node(node)
|
|
269
|
+
|
|
270
|
+
def get_status(self, node: Node) -> NodeProcess:
|
|
271
|
+
"""
|
|
272
|
+
Get current status of a node.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
node: Node database record
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
NodeProcess with current status
|
|
279
|
+
"""
|
|
280
|
+
pid = self._read_pid_file(node)
|
|
281
|
+
|
|
282
|
+
# Check if root directory exists
|
|
283
|
+
if not os.path.isdir(node.root_dir):
|
|
284
|
+
return NodeProcess(node_id=node.id, pid=None, status=DEAD)
|
|
285
|
+
|
|
286
|
+
if pid:
|
|
287
|
+
try:
|
|
288
|
+
process = psutil.Process(pid)
|
|
289
|
+
# Verify it's actually our node process
|
|
290
|
+
if process.is_running():
|
|
291
|
+
return NodeProcess(node_id=node.id, pid=pid, status=RUNNING)
|
|
292
|
+
except psutil.NoSuchProcess:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
return NodeProcess(node_id=node.id, pid=None, status=STOPPED)
|
|
296
|
+
|
|
297
|
+
def remove_node(self, node: Node) -> bool:
|
|
298
|
+
"""
|
|
299
|
+
Stop and remove a node.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
node: Node database record
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
True if node was removed successfully
|
|
306
|
+
"""
|
|
307
|
+
logging.info(f"Removing setsid node {node.id}")
|
|
308
|
+
|
|
309
|
+
# Stop the node first
|
|
310
|
+
self.stop_node(node)
|
|
311
|
+
|
|
312
|
+
# Remove node directory
|
|
313
|
+
try:
|
|
314
|
+
node_dir = Path(node.root_dir)
|
|
315
|
+
if node_dir.exists():
|
|
316
|
+
shutil.rmtree(node_dir)
|
|
317
|
+
except (OSError, shutil.Error) as err:
|
|
318
|
+
logging.error(f"Failed to remove node directory: {err}")
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
return True
|
|
322
|
+
|
|
323
|
+
def survey_nodes(self, machine_config) -> list:
|
|
324
|
+
"""
|
|
325
|
+
Survey all setsid-managed antnode processes.
|
|
326
|
+
|
|
327
|
+
Setsid nodes are typically not used for migration scenarios.
|
|
328
|
+
This returns an empty list as setsid processes are created
|
|
329
|
+
fresh by WNM and don't pre-exist.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
machine_config: Machine configuration object
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Empty list (setsid nodes don't pre-exist for migration)
|
|
336
|
+
"""
|
|
337
|
+
logging.info(
|
|
338
|
+
"Setsid survey not implemented (setsid nodes created fresh by WNM)"
|
|
339
|
+
)
|
|
340
|
+
return []
|