fix: find Hestia behind bounded process metadata

This commit is contained in:
VAIO73
2026-10-17 11:27:16 +00:00
parent 1f75147d51
commit 20acf0852a
2 changed files with 59 additions and 5 deletions
+49 -1
View File
@@ -202,6 +202,30 @@ else
exit 1
fi
'''
HESTIA_PANEL_SCRIPT = r'''
# The generic process collector intentionally stops after a bounded number of
# records and ignores shared listener ownership. Hestia's master/worker pair
# can therefore be outside that generic result. This narrow fallback never
# accepts a name alone: it requires Hestia's exact executable and its panel
# listener on the fixed local port.
[ -x /usr/local/hestia/nginx/sbin/hestia-nginx ] || { printf 'END\n'; exit 0; }
command -v pgrep >/dev/null && command -v readlink >/dev/null && command -v ss >/dev/null && command -v head >/dev/null || exit 1
n=0
for pid in $(pgrep -x hestia-nginx 2>/dev/null | head -n 9); do
case "$pid" in *[!0-9]*|'') continue;; esac
n=$((n+1)); [ "$n" -le 8 ] || { printf 'LIMIT\n'; exit 0; }
exe=$(timeout -s KILL 1 readlink "/proc/$pid/exe") || continue
[ "$exe" = /usr/local/hestia/nginx/sbin/hestia-nginx ] || continue
cwd=$(timeout -s KILL 1 readlink "/proc/$pid/cwd") || continue
if timeout -s KILL 1 ss -H -lntp | awk -v pid="$pid" '
{port=$4; sub(/^.*:/,"",port)
if (port == 8083 && index($0, "pid=" pid ",")) found=1}
END {exit(found ? 0 : 1)}'; then
printf 'H\t%s\t%s\t%s\t8083\n' "$pid" "$exe" "$cwd"
fi
done
printf 'END\n'
'''
FOOTPRINT_SCRIPT = r'''
[ -f "$1" ] && [ -r "$1" ] || exit 1
printf 'FILE\n'
@@ -393,6 +417,15 @@ def parse_records(text, kind):
records.append({'pid': pid, 'port': int(port)})
else:
raise ProbeError('invalid_data')
elif kind == 'H' and len(parts) == 5 and parts[0] == 'H':
_, pid, exe, directory, port = parts
if (pid.isdigit() and exe == _HESTIA_NGINX
and (directory == '/' or _path(directory))
and port == str(_HESTIA_PANEL_PORT)):
records.append({'pid': pid, 'executable': exe, 'root': directory,
'port': _HESTIA_PANEL_PORT})
else:
raise ProbeError('invalid_data')
else:
raise ProbeError('invalid_data')
return records, lines[-1] == 'LIMIT', len(lines) - 1
@@ -487,7 +520,7 @@ def _guard(vmid, deadline, run):
def scan(vmid, run=bounded_run):
deadline = time.monotonic() + TOTAL_SECONDS
_guard(vmid, deadline, run)
records, issues, consumed, processes, listeners = [], [], 0, [], []
records, issues, consumed, processes, listeners, hestia_panels = [], [], 0, [], [], []
for kind, script in (('S', SERVICE_SCRIPT), ('P', PROCESS_SCRIPT), ('L', PORT_SCRIPT)):
try:
if consumed >= MAX_RECORDS:
@@ -512,6 +545,16 @@ def scan(vmid, run=bounded_run):
issues.append('metadata_limit')
except ProbeError as exc:
issues.append(str(exc))
try:
# This is outside the generic record budget: it returns only an exact,
# fixed Hestia signature, not additional arbitrary process metadata.
hestia_panels, limited, _ = parse_records(run(
['pct', 'exec', str(vmid), '--', 'timeout', '-s', 'KILL', '2',
'sh', '-c', HESTIA_PANEL_SCRIPT, 'discovery'], deadline), 'H')
if limited:
issues.append('metadata_limit')
except ProbeError as exc:
issues.append(str(exc))
for process in processes:
exe, directory = process['executable'], process['root']
related = [record for record in records if record['executable'] == exe and (
@@ -531,6 +574,11 @@ def scan(vmid, run=bounded_run):
and _NAME.fullmatch(Path(exe).name)):
records.append({'name': Path(exe).name, 'path': exe, 'root': directory,
'process': exe, 'executable': exe, 'listening_ports': ports})
for panel in hestia_panels:
records.append({'name': 'Hestia', 'path': panel['executable'], 'root': _HESTIA_ROOT,
'process': panel['executable'], 'process_root': panel['root'],
'executable': panel['executable'],
'listening_ports': [panel['port']]})
items, seen = [], set()
for record in records:
key = (record['path'], record['root'])
@@ -45,7 +45,7 @@ class DiscoveryFixtures(unittest.TestCase):
self.calls = []
def runner(self, services=None, processes='END\n', footprint='FILE\n', manifest=None,
status='status: running', ports='END\n'):
status='status: running', ports='END\n', hestia_panels='END\n'):
def run(argv, deadline):
self.calls.append(argv)
self.assertLessEqual(deadline - time.monotonic(), native.TOTAL_SECONDS)
@@ -60,6 +60,8 @@ class DiscoveryFixtures(unittest.TestCase):
return processes
if script == native.PORT_SCRIPT:
return ports
if script == native.HESTIA_PANEL_SCRIPT:
return hestia_panels
if script == native.MANIFEST_FOOTPRINT_SCRIPT:
if manifest is not None:
return manifest
@@ -144,21 +146,25 @@ class DiscoveryFixtures(unittest.TestCase):
def test_hestia_panel_is_recognized_only_from_its_exact_runtime_and_port(self):
hestia = '/usr/local/hestia/nginx/sbin/hestia-nginx'
process = f'P\t391\t{hestia}\t/\nEND\n'
panel = f'H\t391\t{hestia}\t/\t8083\nEND\n'
result = native.scan(101, self.runner(
services='END\n', processes=process, ports='L\t391\t8083\nEND\n'))
services='END\n', processes='LIMIT\n', ports='END\n', hestia_panels=panel))
item, = result['candidates']
self.assertEqual(item['name'], 'Hestia')
self.assertEqual(item['installation_path'], hestia)
self.assertEqual(item['listening_ports'], [8083])
self.assertIn({'kind': 'working_directory', 'value': '/'}, item['evidence'])
self.assertIn('metadata_limit', result['issues'])
self.calls.clear()
result = native.scan(101, self.runner(
services='END\n', processes=process, ports='L\t391\t8443\nEND\n'))
services='END\n', processes='LIMIT\n', ports='END\n'))
self.assertEqual(result['candidates'], [])
self.assertFalse(any(native.FOOTPRINT_SCRIPT in call for call in self.calls))
with self.assertRaises(native.ProbeError):
native.parse_records(f'H\t391\t{hestia}\t/\t8443\nEND\n', 'H')
def test_node_and_npm_wrappers_use_only_bounded_manifest_roots(self):
cases = [
('keynest', '/usr/bin/node', '/opt/keynest'),