Initial commit: enterprise digital platform with portal SSO, DigiServer, IT Assets, NetworkView, Server Monitor
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# services package
|
||||
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
module_manager.py — start/stop/status helpers for EDP sub-services.
|
||||
|
||||
Used exclusively in development mode (start-dev.sh launcher). Each service
|
||||
writes its PID to .dev-logs/<name>.pid so the portal can signal it.
|
||||
Module state (enabled/disabled) is persisted in the portal's SQLite DB
|
||||
(ModuleConfig table) and mirrored to .dev-module-state.json so that
|
||||
start-dev.sh can respect it on the next launch.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ── Directory helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
def _root() -> str:
|
||||
"""Return the Enterprise_digital-platform root directory at runtime."""
|
||||
# This file lives at: portal/app/services/module_manager.py
|
||||
# → services/ → app/ → portal/ → ROOT
|
||||
return os.path.dirname(
|
||||
os.path.dirname(
|
||||
os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _log_dir() -> str:
|
||||
return os.path.join(_root(), '.dev-logs')
|
||||
|
||||
|
||||
def _pid_path(app_id: str) -> str:
|
||||
return os.path.join(_log_dir(), f'{app_id}.pid')
|
||||
|
||||
|
||||
def _state_path() -> str:
|
||||
return os.path.join(_root(), '.dev-module-state.json')
|
||||
|
||||
|
||||
# ── PID file helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
def _read_pid(app_id: str) -> Optional[int]:
|
||||
path = _pid_path(app_id)
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
try:
|
||||
with open(path) as fp:
|
||||
return int(fp.read().strip())
|
||||
except (ValueError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def _write_pid(app_id: str, pid: int) -> None:
|
||||
os.makedirs(_log_dir(), exist_ok=True)
|
||||
with open(_pid_path(app_id), 'w') as fp:
|
||||
fp.write(str(pid))
|
||||
|
||||
|
||||
def _clear_pid(app_id: str) -> None:
|
||||
path = _pid_path(app_id)
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# ── Module state file ─────────────────────────────────────────────────────────
|
||||
|
||||
def write_module_state(app_id: str, enabled: bool) -> None:
|
||||
"""Mirror enabled/disabled state to .dev-module-state.json for start-dev.sh."""
|
||||
path = _state_path()
|
||||
state: dict = {}
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
with open(path) as fp:
|
||||
state = json.load(fp)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
state = {}
|
||||
state[app_id] = enabled
|
||||
with open(path, 'w') as fp:
|
||||
json.dump(state, fp, indent=2)
|
||||
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
def is_running(app_id: str) -> bool:
|
||||
"""Return True if the service process is alive."""
|
||||
pid = _read_pid(app_id)
|
||||
if pid is None:
|
||||
return False
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
_clear_pid(app_id)
|
||||
return False
|
||||
except PermissionError:
|
||||
return True # process exists, owned by another uid
|
||||
|
||||
|
||||
def stop_service(app_id: str) -> bool:
|
||||
"""Send SIGTERM to the service. Returns True if signal was delivered."""
|
||||
pid = _read_pid(app_id)
|
||||
if pid is None:
|
||||
return False
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
_clear_pid(app_id)
|
||||
log.info('Sent SIGTERM to %s (PID %d)', app_id, pid)
|
||||
return True
|
||||
except ProcessLookupError:
|
||||
_clear_pid(app_id)
|
||||
return False
|
||||
|
||||
|
||||
def start_service(app_id: str) -> bool:
|
||||
"""Spawn the service as a detached background process. Returns True on success."""
|
||||
root = _root()
|
||||
spec = _get_spec(app_id, root)
|
||||
if spec is None:
|
||||
log.warning('No service spec for app_id=%s', app_id)
|
||||
return False
|
||||
|
||||
log_file = os.path.join(_log_dir(), f'{app_id}.log')
|
||||
os.makedirs(_log_dir(), exist_ok=True)
|
||||
|
||||
env = {**os.environ, **spec.get('env', {})}
|
||||
with open(log_file, 'a') as lf:
|
||||
proc = subprocess.Popen(
|
||||
spec['cmd'],
|
||||
env=env,
|
||||
cwd=spec.get('cwd'),
|
||||
stdout=lf,
|
||||
stderr=lf,
|
||||
start_new_session=True, # detach from the Flask process group
|
||||
)
|
||||
_write_pid(app_id, proc.pid)
|
||||
log.info('Started %s as PID %d', app_id, proc.pid)
|
||||
return True
|
||||
|
||||
|
||||
# ── Service command specs ─────────────────────────────────────────────────────
|
||||
|
||||
def _get_spec(app_id: str, root: str) -> Optional[dict]:
|
||||
"""Return the launch spec for a given app_id, or None if unknown."""
|
||||
jwt = os.environ.get('PORTAL_JWT_SECRET', 'change-this-jwt-secret-in-production')
|
||||
|
||||
specs: dict = {
|
||||
'digiserver': {
|
||||
'cmd': [
|
||||
f'{root}/digiserver-v2/.venv/bin/gunicorn',
|
||||
'--bind', '0.0.0.0:5002',
|
||||
'--workers', '2',
|
||||
'--timeout', '120',
|
||||
'--chdir', f'{root}/digiserver-v2',
|
||||
'wsgi:application',
|
||||
],
|
||||
'env': {
|
||||
'FLASK_ENV': 'development',
|
||||
'ADMIN_USERNAME': os.environ.get('ADMIN_USERNAME', 'admin'),
|
||||
'ADMIN_PASSWORD': os.environ.get('ADMIN_PASSWORD', 'admin123'),
|
||||
'PORTAL_JWT_SECRET': jwt,
|
||||
},
|
||||
'cwd': f'{root}/digiserver-v2',
|
||||
},
|
||||
'itassets': {
|
||||
'cmd': [
|
||||
f'{root}/IT_asset_management/.venv/bin/python',
|
||||
f'{root}/IT_asset_management/run.py',
|
||||
],
|
||||
'env': {
|
||||
'FLASK_ENV': 'development',
|
||||
'SQLALCHEMY_DATABASE_URI': f'sqlite:///{root}/IT_asset_management/data/itassets.db',
|
||||
'PORTAL_JWT_SECRET': jwt,
|
||||
'FLASK_APP': 'run.py',
|
||||
},
|
||||
'cwd': f'{root}/IT_asset_management',
|
||||
},
|
||||
'networkview': {
|
||||
'cmd': ['node', f'{root}/NetworkView/backend/src/index.js'],
|
||||
'env': {
|
||||
'PORT': '3001',
|
||||
'PORTAL_JWT_SECRET': jwt,
|
||||
'NODE_ENV': 'development',
|
||||
'DB_PATH': f'{root}/NetworkView/data/networkview.db',
|
||||
},
|
||||
'cwd': f'{root}/NetworkView/backend',
|
||||
},
|
||||
}
|
||||
return specs.get(app_id)
|
||||
Reference in New Issue
Block a user