1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
|
"""Starting, stopping and reloading the node: systemd here, the Startup
launcher or the service task on Windows."""
import sys
from meshbay_node.cli.api import _daemon_api, _systemctl_user
from meshbay_node.config import DEFAULT_CONFIG_PATH, load_config
def reload(args) -> None:
if sys.platform == "win32":
# No systemd, no SIGHUP: the daemon exposes a hot reload on its
# own loopback API (the same one ops.reload_config drives).
cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
_daemon_api(cfg, "/api/reload", method="POST")
print("sent reload to the running node")
return
_systemctl_user(
"reload", "meshbay-node",
not_running_hint="Node is not running as a systemd unit — start it "
"with: systemctl --user start meshbay-node",
success="sent reload to meshbay-node",
watch="watch the result: journalctl --user -u meshbay-node -f")
return
def _await_daemon(cfg, since: float, timeout: float = 30.0) -> dict | None:
"""The status of a daemon started at or after `since`, or None.
The token file is rewritten at every start, so one older than `since`
belongs to the instance that was just stopped — answering from it would
report the old daemon as the new one.
"""
import json
import time
import urllib.request
token_file = cfg.data_dir / "ui-token"
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
if token_file.stat().st_mtime >= since:
token = token_file.read_text(encoding="utf-8").strip()
url = f"http://127.0.0.1:{cfg.node.ui_port}/api/status?t={token}"
with urllib.request.urlopen(url, timeout=2) as r:
return json.loads(r.read())
except (OSError, ValueError):
pass
time.sleep(0.5)
return None
def _running_status(cfg) -> dict | None:
"""The status of whatever node answers right now, or None."""
return _await_daemon(cfg, since=0, timeout=0.1)
class NodeStillRunning(RuntimeError):
pass
def _stop_node(cfg) -> str:
"""Stop the node, whichever session it runs in: through its own control API
first, so it shuts down properly, then by force. Returns how it went, and
raises NodeStillRunning when a node process is still there at the end --
never "stopped" about a process nobody saw go."""
import time
from meshbay_node.platform import (
_pid_alive,
autostart_end,
kill_pid,
node_pids,
request_graceful_stop,
service_end,
service_state,
service_status,
)
answering = _running_status(cfg)
pid = answering.get("pid") if answering else None
if request_graceful_stop(cfg.data_dir, cfg.node.ui_port):
how = "stopped"
elif answering is None and not node_pids():
how = "not running"
else:
# Asked and not gone: its control API closes first, so it may answer
# nothing while its process is still there.
how = "stopped (forced)"
if service_status()["installed"]:
# Also leaves the task "Ready": a /run while it still reads "Running"
# is dropped (MultipleInstances IgnoreNew), leaving no node at all.
service_end()
deadline = time.monotonic() + 15
while service_state().lower() == "running" and time.monotonic() < deadline:
time.sleep(0.25)
if pid is not None and _pid_alive(int(pid)):
kill_pid(int(pid)) # the one that answered, by pid, wherever it came from
autostart_end() # a node in this session that would not stop
deadline = time.monotonic() + 5
while (left := node_pids()) and time.monotonic() < deadline:
time.sleep(0.25)
if left:
raise NodeStillRunning(
f"meshbay-node is still running (pid {', '.join(map(str, left))}) and could "
"not be stopped from here -- it was probably started from an "
"administrator prompt. Stop it there, or end it in Task Manager.")
return how
def _start_and_confirm(args, action: str) -> None:
"""Start the node the way this machine is set up to, and report what
actually answered -- never "started" about a node nobody checked."""
import time
from meshbay_node import __version__
from meshbay_node.platform import autostart_run, log_file, service_run, service_status
cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
if action == "restart":
try:
_stop_node(cfg)
except NodeStillRunning as e:
# Starting another would find the port taken and quit at once.
print(f"Could not restart the node: {e}")
sys.exit(1)
else:
already = _running_status(cfg)
if already is not None:
print(f"already running — node {already.get('version', '?')}, "
f"{already.get('status', '?')}")
return
since = time.time() - 1
service = service_status()["installed"]
try:
service_run() if service else autostart_run()
except RuntimeError as e:
print(f"Could not start the node: {e}")
sys.exit(1)
status = _await_daemon(cfg, since)
if status is None:
print("no node answered within 30s of being started"
+ (" by the service task." if service else "."))
print(f"See the node's log: {log_file()}")
sys.exit(1)
running = status.get("version", "?")
print(f"{'restarted' if action == 'restart' else 'started'} — node {running}, "
f"{status.get('status', '?')}")
if running != __version__:
print(f"warning: that is node {running} but this is {__version__} — "
"its files were not replaced, or another copy was started.")
def _print_stop(args) -> None:
try:
print(_stop_node(load_config(args.config or DEFAULT_CONFIG_PATH)))
except NodeStillRunning as e:
print(e)
sys.exit(1)
def _stop_node_or_warn(args) -> None:
try:
_stop_node(load_config(args.config or DEFAULT_CONFIG_PATH))
except NodeStillRunning as e:
print(f"warning: {e}")
def restart_daemon(args) -> None:
if sys.platform == "win32":
_start_and_confirm(args, "restart")
return
_systemctl_user(
"restart", "meshbay-node",
not_running_hint="meshbay-node is not installed as a systemd unit — "
"see packaging/systemd/",
success="meshbay-node restarted via systemd",
watch="check status: systemctl --user status meshbay-node\n"
"watch logs: journalctl --user -u meshbay-node -f")
return
def autostart(args) -> None:
from meshbay_node import platform as _plat
if sys.platform != "win32":
print("autostart is Windows-only — elsewhere use "
"'systemctl --user enable --now meshbay-node'.")
sys.exit(1)
sub = args.subcommand or "status"
service_mode = _plat.service_status()["installed"]
if sub == "install":
if service_mode:
# Both would start the node: at boot, then again at sign-in.
print("The node already runs as a background service. Remove it "
"first (meshbay-node service remove, elevated) to start it at "
"sign-in instead.")
sys.exit(1)
_plat.autostart_install()
print("Installed the Startup launcher — meshbay-node starts at "
"each sign-in (no window, no admin).")
print("Start it now with: meshbay-node autostart start")
elif sub == "remove":
_plat.autostart_remove()
print("Removed the Startup launcher.")
elif sub == "start":
_start_and_confirm(args, "start")
elif sub == "stop":
_print_stop(args)
elif sub == "status":
st = _plat.autostart_status()
if st["installed"]:
print("autostart installed — runs meshbay-node at sign-in")
elif service_mode:
print("autostart not used — the node runs as a background service")
else:
print("autostart not installed — meshbay-node autostart install")
else:
print("autostart: install | remove | start | stop | status")
sys.exit(1)
return
def service(args) -> None:
from meshbay_node import platform as _plat
if sys.platform != "win32":
print("service mode is Windows-only — elsewhere use "
"'systemctl --user enable --now meshbay-node'.")
sys.exit(1)
sub = args.subcommand or "status"
if sub == "install":
# A node already running in this session holds the control API's port:
# the service's own would exit at once, leaving the old one in charge.
_stop_node_or_warn(args)
try:
_plat.service_install()
except RuntimeError as e:
print(f"Could not install: {e}")
if "denied" in str(e).lower():
print("Run this from an elevated (Administrator) prompt.")
sys.exit(1)
print(f"Registered the {_plat.TASK_NAME!r} scheduled task — it starts "
"meshbay-node at boot, as this user, whether or not you have "
"signed in yet (no password stored).")
print("Start it now with: meshbay-node service start")
elif sub == "remove":
# Deleting a task does not end its running instance: stop the node
# first, or it runs on in session 0 with nothing left to stop it.
_stop_node_or_warn(args)
_plat.service_remove()
print(f"Removed the {_plat.TASK_NAME!r} scheduled task.")
elif sub == "start":
if not _plat.service_status()["installed"]:
print("service not installed — meshbay-node service install "
"(needs an elevated prompt)")
sys.exit(1)
_start_and_confirm(args, "start")
elif sub == "stop":
_print_stop(args)
elif sub == "status":
st = _plat.service_status()
if st["installed"]:
print(f"service installed — {st['state'] or 'unknown state'}")
else:
print("service not installed — meshbay-node service install "
"(needs an elevated prompt)")
else:
print("service: install | remove | start | stop | status")
sys.exit(1)
return
|