hivemind-manager 0.1.0__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.
- hivemind_manager-0.1.0/PKG-INFO +27 -0
- hivemind_manager-0.1.0/README.md +19 -0
- hivemind_manager-0.1.0/pyproject.toml +21 -0
- hivemind_manager-0.1.0/setup.cfg +4 -0
- hivemind_manager-0.1.0/src/hivemind_manager.egg-info/PKG-INFO +27 -0
- hivemind_manager-0.1.0/src/hivemind_manager.egg-info/SOURCES.txt +13 -0
- hivemind_manager-0.1.0/src/hivemind_manager.egg-info/dependency_links.txt +1 -0
- hivemind_manager-0.1.0/src/hivemind_manager.egg-info/entry_points.txt +2 -0
- hivemind_manager-0.1.0/src/hivemind_manager.egg-info/top_level.txt +1 -0
- hivemind_manager-0.1.0/src/hm/__init__.py +1 -0
- hivemind_manager-0.1.0/src/hm/cli.py +252 -0
- hivemind_manager-0.1.0/src/hm/config.py +33 -0
- hivemind_manager-0.1.0/src/hm/discovery.py +32 -0
- hivemind_manager-0.1.0/src/hm/process.py +173 -0
- hivemind_manager-0.1.0/src/hm/tailer.py +51 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hivemind-manager
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A developer CLI tool for managing Hivemind services
|
|
5
|
+
Author: dhruv13x
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# Hivemind Manager (hm) CLI
|
|
10
|
+
|
|
11
|
+
`hm` is a developer process manager and supervisor built around `hivemind` for development environments. It dynamically scans, starts, stops, and manages service configurations (`*.hm` files) with automatic dependency resolution.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Install in editable mode in your virtual environment:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e ~/workspace/tools/hivemind_manager
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Dynamic Dependencies
|
|
22
|
+
|
|
23
|
+
Declare dependencies inside your `*.hm` files using comments:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# depends_on: infra
|
|
27
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Hivemind Manager (hm) CLI
|
|
2
|
+
|
|
3
|
+
`hm` is a developer process manager and supervisor built around `hivemind` for development environments. It dynamically scans, starts, stops, and manages service configurations (`*.hm` files) with automatic dependency resolution.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install in editable mode in your virtual environment:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e ~/workspace/tools/hivemind_manager
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Dynamic Dependencies
|
|
14
|
+
|
|
15
|
+
Declare dependencies inside your `*.hm` files using comments:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# depends_on: infra
|
|
19
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hivemind-manager"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A developer CLI tool for managing Hivemind services"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "dhruv13x" }]
|
|
7
|
+
requires-python = ">=3.8"
|
|
8
|
+
dependencies = []
|
|
9
|
+
|
|
10
|
+
[project.scripts]
|
|
11
|
+
hm = "hm.cli:main"
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["setuptools>=61.0"]
|
|
15
|
+
build-backend = "setuptools.build_meta"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.package-dir]
|
|
18
|
+
"" = "src"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["src"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hivemind-manager
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A developer CLI tool for managing Hivemind services
|
|
5
|
+
Author: dhruv13x
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# Hivemind Manager (hm) CLI
|
|
10
|
+
|
|
11
|
+
`hm` is a developer process manager and supervisor built around `hivemind` for development environments. It dynamically scans, starts, stops, and manages service configurations (`*.hm` files) with automatic dependency resolution.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Install in editable mode in your virtual environment:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e ~/workspace/tools/hivemind_manager
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Dynamic Dependencies
|
|
22
|
+
|
|
23
|
+
Declare dependencies inside your `*.hm` files using comments:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# depends_on: infra
|
|
27
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/hivemind_manager.egg-info/PKG-INFO
|
|
4
|
+
src/hivemind_manager.egg-info/SOURCES.txt
|
|
5
|
+
src/hivemind_manager.egg-info/dependency_links.txt
|
|
6
|
+
src/hivemind_manager.egg-info/entry_points.txt
|
|
7
|
+
src/hivemind_manager.egg-info/top_level.txt
|
|
8
|
+
src/hm/__init__.py
|
|
9
|
+
src/hm/cli.py
|
|
10
|
+
src/hm/config.py
|
|
11
|
+
src/hm/discovery.py
|
|
12
|
+
src/hm/process.py
|
|
13
|
+
src/hm/tailer.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hm
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .cli import main
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .config import PROJECT_ROOT, HIVEMIND_BIN
|
|
6
|
+
from .discovery import discover_services
|
|
7
|
+
from .process import (
|
|
8
|
+
read_pid,
|
|
9
|
+
is_running,
|
|
10
|
+
log_file,
|
|
11
|
+
stop_service,
|
|
12
|
+
run_supervised,
|
|
13
|
+
remove_pid
|
|
14
|
+
)
|
|
15
|
+
from .tailer import multi_tail
|
|
16
|
+
|
|
17
|
+
def start(service, follow=True, extra_args=None, started_set=None):
|
|
18
|
+
"""
|
|
19
|
+
Starts a service supervisor. Checks and starts dependencies first if they are not running.
|
|
20
|
+
"""
|
|
21
|
+
if extra_args is None:
|
|
22
|
+
extra_args = []
|
|
23
|
+
if started_set is None:
|
|
24
|
+
started_set = set()
|
|
25
|
+
|
|
26
|
+
services_meta = discover_services()
|
|
27
|
+
if service not in services_meta:
|
|
28
|
+
print(f"Unknown service: {service}")
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
if service in started_set:
|
|
32
|
+
return
|
|
33
|
+
started_set.add(service)
|
|
34
|
+
|
|
35
|
+
# 1. Resolve and start dependencies
|
|
36
|
+
deps = services_meta[service]["dependencies"]
|
|
37
|
+
for dep in deps:
|
|
38
|
+
if dep not in services_meta:
|
|
39
|
+
print(f"Warning: [{service}] depends on unknown service '{dep}'")
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
dep_pid = read_pid(dep)
|
|
43
|
+
if dep_pid and is_running(dep_pid):
|
|
44
|
+
# Dependency already running, no action needed
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
print(f"[{service}] Dependency '{dep}' is not running. Starting '{dep}' first...")
|
|
48
|
+
start(dep, follow=False, started_set=started_set)
|
|
49
|
+
# Brief pause to allow the dependency supervisor & worker to start
|
|
50
|
+
time.sleep(1.5)
|
|
51
|
+
|
|
52
|
+
# 2. Stop existing service instance to prevent collision
|
|
53
|
+
stop_service(service)
|
|
54
|
+
|
|
55
|
+
logfile = log_file(service)
|
|
56
|
+
# Truncate logs
|
|
57
|
+
open(logfile, "w").close()
|
|
58
|
+
|
|
59
|
+
print(f"[{service}] supervisor starting...")
|
|
60
|
+
|
|
61
|
+
# Spawn supervisor pointing back to the entrypoint CLI wrapper
|
|
62
|
+
entrypoint_script = str(Path(sys.argv[0]).resolve())
|
|
63
|
+
proc = subprocess.Popen(
|
|
64
|
+
[sys.executable, entrypoint_script, "_run", service] + extra_args,
|
|
65
|
+
preexec_fn=os_setsid_safely(),
|
|
66
|
+
cwd=str(PROJECT_ROOT),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
from .process import write_pid
|
|
70
|
+
write_pid(service, proc.pid)
|
|
71
|
+
|
|
72
|
+
print(f"[{service}] supervisor started (PID {proc.pid})")
|
|
73
|
+
|
|
74
|
+
if follow:
|
|
75
|
+
multi_tail([service])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def os_setsid_safely():
|
|
79
|
+
"""
|
|
80
|
+
Safely returns os.setsid reference if available (Unix systems).
|
|
81
|
+
"""
|
|
82
|
+
import os
|
|
83
|
+
return getattr(os, "setsid", None)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def ps():
|
|
87
|
+
"""
|
|
88
|
+
Lists running services and unmanaged hivemind processes.
|
|
89
|
+
"""
|
|
90
|
+
services_meta = discover_services()
|
|
91
|
+
print(f"{'SERVICE':<15} {'STATUS':<10} {'PID':<8}")
|
|
92
|
+
print("-" * 40)
|
|
93
|
+
|
|
94
|
+
supervisor_pids = set()
|
|
95
|
+
|
|
96
|
+
for svc in services_meta:
|
|
97
|
+
pid = read_pid(svc)
|
|
98
|
+
if pid and is_running(pid):
|
|
99
|
+
print(f"{svc:<15} {'running':<10} {pid:<8}")
|
|
100
|
+
supervisor_pids.add(str(pid))
|
|
101
|
+
else:
|
|
102
|
+
print(f"{svc:<15} {'stopped':<10} -")
|
|
103
|
+
remove_pid(svc)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
out = subprocess.check_output(["pgrep", "-af", HIVEMIND_BIN], text=True)
|
|
107
|
+
lines = out.strip().split("\n")
|
|
108
|
+
|
|
109
|
+
extra = []
|
|
110
|
+
|
|
111
|
+
for line in lines:
|
|
112
|
+
if not line.strip():
|
|
113
|
+
continue
|
|
114
|
+
parts = line.split()
|
|
115
|
+
pid = parts[0]
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
ppid = subprocess.check_output(
|
|
119
|
+
["ps", "-o", "ppid=", "-p", pid],
|
|
120
|
+
text=True
|
|
121
|
+
).strip()
|
|
122
|
+
except subprocess.CalledProcessError:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
if ppid in supervisor_pids or pid in supervisor_pids:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
extra.append(line)
|
|
129
|
+
|
|
130
|
+
if extra:
|
|
131
|
+
print("\n[unmanaged hivemind processes]")
|
|
132
|
+
for l in extra:
|
|
133
|
+
print(" ", l)
|
|
134
|
+
|
|
135
|
+
except subprocess.CalledProcessError:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def status(service=None):
|
|
140
|
+
"""
|
|
141
|
+
Shows status of all or a specific service.
|
|
142
|
+
"""
|
|
143
|
+
if service is None:
|
|
144
|
+
ps()
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
pid = read_pid(service)
|
|
148
|
+
if pid and is_running(pid):
|
|
149
|
+
print(f"[{service}] running (PID {pid})")
|
|
150
|
+
else:
|
|
151
|
+
print(f"[{service}] stopped")
|
|
152
|
+
remove_pid(service)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def up():
|
|
156
|
+
"""
|
|
157
|
+
Starts all discovered services in dynamic order.
|
|
158
|
+
"""
|
|
159
|
+
services_meta = discover_services()
|
|
160
|
+
started_set = set()
|
|
161
|
+
for svc in services_meta:
|
|
162
|
+
start(svc, follow=False, started_set=started_set)
|
|
163
|
+
print("\nAll services started.\n")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def down():
|
|
167
|
+
"""
|
|
168
|
+
Stops all discovered services.
|
|
169
|
+
"""
|
|
170
|
+
services_meta = discover_services()
|
|
171
|
+
for svc in services_meta:
|
|
172
|
+
stop_service(svc)
|
|
173
|
+
print("\nAll services stopped.\n")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def usage():
|
|
177
|
+
cmd = Path(sys.argv[0]).name
|
|
178
|
+
print(f"""
|
|
179
|
+
Usage:
|
|
180
|
+
{cmd} start <service> [--no-follow]
|
|
181
|
+
{cmd} stop <service>
|
|
182
|
+
{cmd} restart <service>
|
|
183
|
+
{cmd} status [service]
|
|
184
|
+
{cmd} logs <service> [service2...]
|
|
185
|
+
{cmd} ps
|
|
186
|
+
{cmd} up
|
|
187
|
+
{cmd} down
|
|
188
|
+
""")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def main():
|
|
192
|
+
if len(sys.argv) < 2:
|
|
193
|
+
usage()
|
|
194
|
+
sys.exit(1)
|
|
195
|
+
|
|
196
|
+
cmd = sys.argv[1]
|
|
197
|
+
|
|
198
|
+
if cmd == "_run":
|
|
199
|
+
if len(sys.argv) < 3:
|
|
200
|
+
print("Error: service name required for _run")
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
service = sys.argv[2]
|
|
203
|
+
extra_args = sys.argv[3:]
|
|
204
|
+
run_supervised(service, extra_args)
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
if cmd in ("up", "down"):
|
|
208
|
+
if cmd == "up":
|
|
209
|
+
up()
|
|
210
|
+
else:
|
|
211
|
+
down()
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
if cmd == "ps":
|
|
215
|
+
ps()
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
if cmd == "status":
|
|
219
|
+
if len(sys.argv) == 2:
|
|
220
|
+
status(None)
|
|
221
|
+
else:
|
|
222
|
+
status(sys.argv[2])
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if cmd == "logs":
|
|
226
|
+
if len(sys.argv) < 3:
|
|
227
|
+
usage()
|
|
228
|
+
return
|
|
229
|
+
multi_tail(sys.argv[2:])
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
if len(sys.argv) < 3:
|
|
233
|
+
usage()
|
|
234
|
+
sys.exit(1)
|
|
235
|
+
|
|
236
|
+
service = sys.argv[2]
|
|
237
|
+
extra_args = sys.argv[3:]
|
|
238
|
+
|
|
239
|
+
follow = True
|
|
240
|
+
if "--no-follow" in extra_args:
|
|
241
|
+
follow = False
|
|
242
|
+
extra_args.remove("--no-follow")
|
|
243
|
+
|
|
244
|
+
if cmd == "start":
|
|
245
|
+
start(service, follow, extra_args)
|
|
246
|
+
elif cmd == "stop":
|
|
247
|
+
stop_service(service)
|
|
248
|
+
elif cmd == "restart":
|
|
249
|
+
stop_service(service)
|
|
250
|
+
start(service, follow, extra_args)
|
|
251
|
+
else:
|
|
252
|
+
usage()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def find_project_root():
|
|
6
|
+
"""
|
|
7
|
+
Traverses upwards from the current working directory to find a directory
|
|
8
|
+
containing *.hm files. Falls back to the current directory if none are found.
|
|
9
|
+
"""
|
|
10
|
+
current_dir = Path(os.getcwd()).resolve()
|
|
11
|
+
for parent in [current_dir] + list(current_dir.parents):
|
|
12
|
+
if list(parent.glob("*.hm")):
|
|
13
|
+
return parent
|
|
14
|
+
return current_dir
|
|
15
|
+
|
|
16
|
+
PROJECT_ROOT = find_project_root()
|
|
17
|
+
BASE_DIR = PROJECT_ROOT / "hm"
|
|
18
|
+
BASE_DIR.mkdir(exist_ok=True)
|
|
19
|
+
|
|
20
|
+
HIVEMIND_BIN = "hivemind"
|
|
21
|
+
|
|
22
|
+
RESTART_DELAY = 1.0
|
|
23
|
+
MAX_RESTART_DELAY = 10.0
|
|
24
|
+
|
|
25
|
+
COLORS = [
|
|
26
|
+
"\033[36m", # cyan
|
|
27
|
+
"\033[33m", # yellow
|
|
28
|
+
"\033[35m", # magenta
|
|
29
|
+
"\033[32m", # green
|
|
30
|
+
"\033[34m", # blue
|
|
31
|
+
"\033[31m", # red
|
|
32
|
+
]
|
|
33
|
+
RESET = "\033[0m"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from .config import PROJECT_ROOT
|
|
3
|
+
|
|
4
|
+
def discover_services():
|
|
5
|
+
"""
|
|
6
|
+
Scans the project root for *.hm files and dynamically builds the services registry,
|
|
7
|
+
including dependencies parsed from comments inside each file (e.g., # depends_on: infra).
|
|
8
|
+
"""
|
|
9
|
+
services = {}
|
|
10
|
+
for path in sorted(PROJECT_ROOT.glob("*.hm")):
|
|
11
|
+
service_name = path.stem
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
with open(path, "r") as f:
|
|
16
|
+
for line in f:
|
|
17
|
+
# Parse line like "# depends_on: infra, database"
|
|
18
|
+
match = re.match(r"^\s*#\s*depends_on\s*:\s*(.*)$", line, re.IGNORECASE)
|
|
19
|
+
if match:
|
|
20
|
+
deps = match.group(1).split(",")
|
|
21
|
+
for dep in deps:
|
|
22
|
+
dep = dep.strip()
|
|
23
|
+
if dep:
|
|
24
|
+
dependencies.append(dep)
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
services[service_name] = {
|
|
29
|
+
"path": path,
|
|
30
|
+
"dependencies": dependencies
|
|
31
|
+
}
|
|
32
|
+
return services
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import signal
|
|
5
|
+
import subprocess
|
|
6
|
+
from .config import BASE_DIR, PROJECT_ROOT, HIVEMIND_BIN, RESTART_DELAY, MAX_RESTART_DELAY
|
|
7
|
+
|
|
8
|
+
def pid_file(service):
|
|
9
|
+
return BASE_DIR / f"{service}.pid"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def log_file(service):
|
|
13
|
+
return BASE_DIR / f"{service}.log"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_running(pid):
|
|
17
|
+
try:
|
|
18
|
+
os.kill(pid, 0)
|
|
19
|
+
return True
|
|
20
|
+
except OSError:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_pid(service):
|
|
25
|
+
f = pid_file(service)
|
|
26
|
+
if not f.exists():
|
|
27
|
+
return None
|
|
28
|
+
try:
|
|
29
|
+
return int(f.read_text().strip())
|
|
30
|
+
except:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_pid(service, pid):
|
|
35
|
+
pid_file(service).write_text(str(pid))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def remove_pid(service):
|
|
39
|
+
pid_file(service).unlink(missing_ok=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def stop_service(service):
|
|
43
|
+
"""
|
|
44
|
+
Terminates the specified service.
|
|
45
|
+
First tries to gracefully stop using the stored PID.
|
|
46
|
+
Then scans the system for orphaned or unmanaged processes of that service and stops them as well.
|
|
47
|
+
"""
|
|
48
|
+
pid = read_pid(service)
|
|
49
|
+
stopped_any = False
|
|
50
|
+
|
|
51
|
+
if pid:
|
|
52
|
+
if is_running(pid):
|
|
53
|
+
print(f"[{service}] stopping (PID {pid})")
|
|
54
|
+
try:
|
|
55
|
+
os.killpg(pid, signal.SIGTERM)
|
|
56
|
+
stopped_any = True
|
|
57
|
+
except ProcessLookupError:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
for _ in range(20):
|
|
61
|
+
if not is_running(pid):
|
|
62
|
+
break
|
|
63
|
+
time.sleep(0.2)
|
|
64
|
+
|
|
65
|
+
if is_running(pid):
|
|
66
|
+
print(f"[{service}] force killing")
|
|
67
|
+
try:
|
|
68
|
+
os.killpg(pid, signal.SIGKILL)
|
|
69
|
+
except ProcessLookupError:
|
|
70
|
+
pass
|
|
71
|
+
remove_pid(service)
|
|
72
|
+
|
|
73
|
+
# Terminate any leftover orphaned/unmanaged processes running the .hm file for this service
|
|
74
|
+
try:
|
|
75
|
+
out = subprocess.check_output(["pgrep", "-af", HIVEMIND_BIN], text=True)
|
|
76
|
+
for line in out.strip().split("\n"):
|
|
77
|
+
if not line.strip():
|
|
78
|
+
continue
|
|
79
|
+
parts = line.split()
|
|
80
|
+
p_pid = int(parts[0])
|
|
81
|
+
cmdline = " ".join(parts[1:])
|
|
82
|
+
|
|
83
|
+
if f"{service}.hm" in cmdline:
|
|
84
|
+
print(f"[{service}] found orphaned/unmanaged process (PID {p_pid}): {cmdline}")
|
|
85
|
+
print(f"[{service}] terminating process group for PID {p_pid}...")
|
|
86
|
+
try:
|
|
87
|
+
pgid = os.getpgid(p_pid)
|
|
88
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
89
|
+
# Wait up to 1 second for the process to exit
|
|
90
|
+
for _ in range(10):
|
|
91
|
+
try:
|
|
92
|
+
os.kill(p_pid, 0)
|
|
93
|
+
except OSError:
|
|
94
|
+
break
|
|
95
|
+
time.sleep(0.1)
|
|
96
|
+
# Force kill if still running
|
|
97
|
+
try:
|
|
98
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
99
|
+
except OSError:
|
|
100
|
+
pass
|
|
101
|
+
stopped_any = True
|
|
102
|
+
except OSError:
|
|
103
|
+
try:
|
|
104
|
+
os.kill(p_pid, 0) # check existence
|
|
105
|
+
os.kill(p_pid, signal.SIGTERM)
|
|
106
|
+
for _ in range(10):
|
|
107
|
+
try:
|
|
108
|
+
os.kill(p_pid, 0)
|
|
109
|
+
except OSError:
|
|
110
|
+
break
|
|
111
|
+
time.sleep(0.1)
|
|
112
|
+
os.kill(p_pid, signal.SIGKILL)
|
|
113
|
+
stopped_any = True
|
|
114
|
+
except OSError:
|
|
115
|
+
pass
|
|
116
|
+
except subprocess.CalledProcessError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
if not pid and not stopped_any:
|
|
120
|
+
print(f"[{service}] not running")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def run_supervised(service, extra_args):
|
|
124
|
+
"""
|
|
125
|
+
Supervisor process loop. Monitors a single hivemind worker and restarts it if it crashes.
|
|
126
|
+
"""
|
|
127
|
+
logfile = log_file(service)
|
|
128
|
+
|
|
129
|
+
stop_flag = False
|
|
130
|
+
child_proc = None
|
|
131
|
+
|
|
132
|
+
def handle_exit(signum, frame):
|
|
133
|
+
nonlocal stop_flag, child_proc
|
|
134
|
+
stop_flag = True
|
|
135
|
+
if child_proc and child_proc.poll() is None:
|
|
136
|
+
try:
|
|
137
|
+
os.killpg(child_proc.pid, signal.SIGTERM)
|
|
138
|
+
except ProcessLookupError:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
signal.signal(signal.SIGTERM, handle_exit)
|
|
142
|
+
signal.signal(signal.SIGINT, handle_exit)
|
|
143
|
+
|
|
144
|
+
delay = RESTART_DELAY
|
|
145
|
+
|
|
146
|
+
while not stop_flag:
|
|
147
|
+
with open(logfile, "ab") as f:
|
|
148
|
+
child_proc = subprocess.Popen(
|
|
149
|
+
[HIVEMIND_BIN, f"{service}.hm"] + extra_args,
|
|
150
|
+
stdout=f,
|
|
151
|
+
stderr=subprocess.STDOUT,
|
|
152
|
+
preexec_fn=os.setsid,
|
|
153
|
+
cwd=str(PROJECT_ROOT),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
print(f"[{service}] worker started (PID {child_proc.pid})")
|
|
157
|
+
|
|
158
|
+
exit_code = child_proc.wait()
|
|
159
|
+
|
|
160
|
+
if stop_flag:
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
print(f"[{service}] worker exited (code {exit_code})")
|
|
164
|
+
|
|
165
|
+
if exit_code == 0:
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
print(f"[{service}] restarting in {delay:.1f}s...")
|
|
169
|
+
time.sleep(delay)
|
|
170
|
+
|
|
171
|
+
delay = min(delay * 2, MAX_RESTART_DELAY)
|
|
172
|
+
|
|
173
|
+
print(f"[{service}] supervisor exiting")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
from .config import COLORS, RESET
|
|
5
|
+
from .process import log_file, read_pid, is_running
|
|
6
|
+
|
|
7
|
+
def tail_worker(service, color):
|
|
8
|
+
"""
|
|
9
|
+
Tails the log file for a specific service and color-prefixes output.
|
|
10
|
+
Terminates when the service stops running.
|
|
11
|
+
"""
|
|
12
|
+
logfile = log_file(service)
|
|
13
|
+
|
|
14
|
+
while not logfile.exists():
|
|
15
|
+
time.sleep(0.1)
|
|
16
|
+
|
|
17
|
+
with open(logfile, "r") as f:
|
|
18
|
+
f.seek(0, os.SEEK_END)
|
|
19
|
+
|
|
20
|
+
while True:
|
|
21
|
+
line = f.readline()
|
|
22
|
+
if line:
|
|
23
|
+
print(line, end="")
|
|
24
|
+
else:
|
|
25
|
+
# auto-stop if service dead
|
|
26
|
+
pid = read_pid(service)
|
|
27
|
+
if not pid or not is_running(pid):
|
|
28
|
+
print(f"{color}[{service}] stopped{RESET}")
|
|
29
|
+
break
|
|
30
|
+
time.sleep(0.2)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def multi_tail(services):
|
|
34
|
+
"""
|
|
35
|
+
Spawns log tailing threads for multiple services.
|
|
36
|
+
"""
|
|
37
|
+
print(f"Tailing logs: {', '.join(services)}\n")
|
|
38
|
+
|
|
39
|
+
threads = []
|
|
40
|
+
|
|
41
|
+
for i, svc in enumerate(services):
|
|
42
|
+
color = COLORS[i % len(COLORS)]
|
|
43
|
+
t = threading.Thread(target=tail_worker, args=(svc, color), daemon=True)
|
|
44
|
+
t.start()
|
|
45
|
+
threads.append(t)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
while True:
|
|
49
|
+
time.sleep(1)
|
|
50
|
+
except KeyboardInterrupt:
|
|
51
|
+
print("\nStopped log tail")
|