diff --git a/.gitignore b/.gitignore index a25a1049..d3af4cd6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ __pycache__/ *.pem *.db *.socket +*.installed # Ignore all object files and shared libraries *.o diff --git a/webserver/app.py b/webserver/app.py index 7f402997..1bdb7c70 100644 --- a/webserver/app.py +++ b/webserver/app.py @@ -28,16 +28,15 @@ MAX_FILE_SIZE ) -# from logger import get_logger, LogParser +from logger import get_logger, LogParser +logger, _ = get_logger("logger", use_buffer=True) app = flask.Flask(__name__) app.secret_key = str(os.urandom(16)) login_manager = flask_login.LoginManager() login_manager.init_app(app) -# logger = get_logger(use_buffer=True) - runtime_manager = RuntimeManager( runtime_path="./build/plc_main", plc_socket="/run/runtime/plc_runtime.socket", @@ -63,7 +62,15 @@ def handle_stop_plc(data: dict) -> dict: def handle_runtime_logs(data: dict) -> dict: - response = runtime_manager.get_logs() + if "id" in data: + min_id = int(data["id"]) + else: + min_id = None + if "level" in data: + level = data["level"] + else: + level = None + response = runtime_manager.get_logs(min_id=min_id, level=level) return {"runtime-logs": response} @@ -200,12 +207,8 @@ def run_https(): # logger.info("Generating https certificate...") print("Generating https certificate...") # TODO: remove this temporary print once logger is functional again cert_gen.generate_self_signed_cert(cert_file=CERT_FILE, key_file=KEY_FILE) - - # Check if the certificate is valid - if not cert_gen.is_certificate_valid(CERT_FILE): - # logger.error("Invalid certificate. Cannot start https application") - print("Invalid certificate. Cannot start https application") # TODO: remove this temporary print once logger is functional again - sys.exit(1) + else: + logger.warning("Credentials already generated!") context = (CERT_FILE, KEY_FILE) app_restapi.run( @@ -226,8 +229,8 @@ def run_https(): # logger.info("HTTP server stopped by KeyboardInterrupt") pass finally: + logger.info("Runtime manager stopped") runtime_manager.stop() - # logger.info("Runtime manager stopped") if __name__ == "__main__": diff --git a/webserver/config.py b/webserver/config.py index de2dfaf3..9fa4ad17 100644 --- a/webserver/config.py +++ b/webserver/config.py @@ -4,15 +4,15 @@ from pathlib import Path from dotenv import load_dotenv +from logger import get_logger, LogParser + +logger, buffer = get_logger("logger", use_buffer=True) # Always resolve .env relative to the repo root to guarantee it is found -ENV_PATH = Path(__file__).resolve().parent.parent / ".env" -DB_PATH = Path(__file__).resolve().parent.parent / "restapi.db" +ENV_PATH = Path(__file__).resolve().parent.parent / "webserver/.env" +DB_PATH = Path(__file__).resolve().parent.parent / "webserver/restapi.db" BASE_DIR = os.path.abspath(os.path.dirname(__file__)) -# logger = logging.getLogger("logger") - - # Function to validate environment variable values def is_valid_env(var_name, value): if var_name == "SQLALCHEMY_DATABASE_URI": @@ -35,18 +35,18 @@ def generate_env_file(): f.write(f"PEPPER={pepper}\n") os.chmod(ENV_PATH, 0o600) - logger.info(f".env file created at {ENV_PATH}") + logger.info(".env file created at %s", ENV_PATH) # Ensure the database file exists and is writable # Deletion is required because new secrets will change the database saved hashes if os.path.exists(DB_PATH): os.remove(DB_PATH) - logger.info(f"Deleted existing database file: {DB_PATH}") + logger.warning("Deleted existing database file: %s", DB_PATH) # Load .env file if not os.path.isfile(ENV_PATH): - logger.warning(".env file not found, creating one...") + logger.info(".env file not found, creating one...") generate_env_file() load_dotenv(dotenv_path=ENV_PATH, override=False) @@ -58,7 +58,7 @@ def generate_env_file(): if not val or not is_valid_env(var, val): raise RuntimeError(f"Environment variable '{var}' is invalid or missing") except RuntimeError as e: - logger.error(f"{e}") + logger.error("%s", e) # Need to regenerate .env file and remove the database as well response = ( input( @@ -68,11 +68,11 @@ def generate_env_file(): .lower() ) if response == "y": - logger.info("Regenerating .env with new valid values...") + print("Regenerating .env with new valid values...") generate_env_file() load_dotenv(ENV_PATH) else: - logger.error("Exiting due to invalid environment configuration.") + print("Exiting due to invalid environment configuration.") exit(1) diff --git a/webserver/logger/__init__.py b/webserver/logger/__init__.py index 70cba6d4..4f3af912 100644 --- a/webserver/logger/__init__.py +++ b/webserver/logger/__init__.py @@ -1,33 +1,29 @@ import logging -import logging.config from .logger import get_logger from .parser import LogParser from .bufferhandler import BufferHandler +from .formatter import JsonFormatter -__all__ = ["get_logger", "LogParser", "BufferHandler"] +__all__ = ["get_logger", "LogParser", "BufferHandler", "JsonFormatter"] __version__ = "0.1" __author__ = "Autonomy" __license__ = "MIT" __description__ = "RestAPI interface for runtime core" +# Single global buffer for all logs +shared_buffer_handler = BufferHandler() -# Configure logging once -logging.config.dictConfig( - { - "version": 1, - "formatters": { - "default": { - "format": "[%(levelname)s] %(asctime)s - %(name)s - %(message)s", - "datefmt": "%H:%M:%S", - } - }, - "handlers": { - "console": { - "class": "logging.StreamHandler", - "formatter": "default", - "level": "DEBUG", - } - }, - "root": {"level": "DEBUG", "handlers": ["console"]}, - } -) +formatter = JsonFormatter() +shared_buffer_handler.setFormatter(formatter) + +def get_logger(name="runtime", use_buffer: bool = False): + """Return a logger that shares the same buffer handler.""" + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + logger.propagate = False + + if use_buffer: + if not any(isinstance(h, BufferHandler) for h in logger.handlers): + logger.addHandler(shared_buffer_handler) + + return logger, shared_buffer_handler diff --git a/webserver/logger/bufferhandler.py b/webserver/logger/bufferhandler.py index a093b0a8..b81d5944 100644 --- a/webserver/logger/bufferhandler.py +++ b/webserver/logger/bufferhandler.py @@ -2,8 +2,8 @@ from collections import deque from typing import List, Optional import json -import re -from datetime import datetime +from datetime import datetime, timezone +from threading import Lock class BufferHandler(logging.Handler): @@ -11,51 +11,89 @@ class BufferHandler(logging.Handler): Custom logging handler that stores log records in memory (FIFO). Logs are formatted using the attached formatter (JSON). """ + _instance = None + _lock = Lock() def __init__(self, capacity: int = 1000): super().__init__() self.buffer = deque(maxlen=capacity) def emit(self, record: logging.LogRecord) -> None: - try: - self.buffer.append(self.format(record)) - except Exception: - self.handleError(record) + with self._lock: + try: + self.buffer.append(self.format(record)) + except Exception: + self.handleError(record) + + def filter_logs(self, logs, level=None, min_id=None, max_id=None): + result = logs + if level is not None: + result = [log for log in result if log.get("level") == level] + if min_id is not None: + result = [log for log in result if log.get("id", 0) >= min_id] + if max_id is not None: + result = [log for log in result if log.get("id", 0) <= max_id] + return result - def get_logs(self, count: Optional[int] = None) -> List[str]: + def get_logs(self, count: Optional[int] = None, + min_id: Optional[int] = None, + level: Optional[str] = None) -> List[str]: """Retrieve logs from buffer.""" - if count is None or count > len(self.buffer): - return list(self.buffer) - return list(self.buffer)[-count:] - - def normalize_buffer_logs(self, buffer_records): - """ - Takes a list of log strings from buffer and returns a list of clean JSON dicts. - """ - result = [] - json_extract = re.compile(r'(\{.*\})') # match JSON inside log line - - for record in buffer_records: - match = json_extract.search(record) - if not match: - continue + with self._lock: + filtered_logs = [json.loads(item) for item in self.buffer] + # json_output = json.dumps(filtered_logs, indent=2) + filtered_logs = self.filter_logs(filtered_logs, level=level, min_id=min_id) + if count is not None and count < len(filtered_logs): + filtered_logs = filtered_logs[-count:] + return filtered_logs + def normalize_timestamp_no_microseconds(self, ts: str) -> str: + """Normalize ISO 8601 timestamp to remove microseconds.""" + dt = datetime.fromisoformat(ts) + return dt.replace(microsecond=0).strftime("%Y-%m-%dT%H:%M:%S%z") + + def normalize_logs(self, json_logs: List[dict]) -> List[dict]: + """Normalize a list of log entries (dicts).""" + normalized = [] + for data in json_logs: try: - raw_json = json.loads(match.group(1)) - # Convert unix timestamp → readable datetime - ts = int(raw_json.get("timestamp", 0)) - dt = datetime.utcfromtimestamp(ts).isoformat() + "Z" - - entry = { - "timestamp": dt, - "level": raw_json.get("level", "INFO"), - "message": raw_json.get("message", "") - } - result.append(entry) - except (json.JSONDecodeError, ValueError): - continue + # Normalize timestamp (convert unix timestamp → ISO 8601) + ts = data.get("timestamp") - return result + # If it's numeric (e.g., 1759843183), convert it to ISO 8601 UTC + if ts and str(ts).isdigit(): + ts_dt = datetime.fromtimestamp(int(ts), tz=timezone.utc) + data["timestamp"] = ts_dt.isoformat() + + # If it's ISO 8601 but has microseconds, strip them + if "timestamp" in data: + data["timestamp"] = self.normalize_timestamp_no_microseconds(data["timestamp"]) + + # Ensure minimal required fields + data.setdefault("level", "INFO") + data.setdefault("message", "") + + normalized.append(data) + + except (json.JSONDecodeError, TypeError, ValueError) as e: + # If something is not JSON, safely wrap it + normalized.append({ + "id": None, + "timestamp": datetime.now(timezone.utc).isoformat(), + "level": "ERROR", + "message": f"Malformed log: {data} ({e})", + }) + + return normalized + + @classmethod + def get_instance(cls): + """Singleton accessor.""" + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = cls() + return cls._instance def clear(self) -> None: self.buffer.clear() diff --git a/webserver/logger/formatter.py b/webserver/logger/formatter.py index 5096fbcb..86441b3c 100644 --- a/webserver/logger/formatter.py +++ b/webserver/logger/formatter.py @@ -1,19 +1,35 @@ +from datetime import datetime, timezone import logging -import time import json + class JsonFormatter(logging.Formatter): """Format log records as JSON strings.""" + log_id = 0 + + def format(self, record): + msg = record.getMessage() + self.log_id += 1 - def format(self, record: logging.LogRecord) -> str: - log_dict = { - "timestamp": str(int(record.created)), # epoch seconds + # Try to detect pre-formatted JSON + if msg.strip().startswith("{") and msg.strip().endswith("}"): + try: + parsed = json.loads(msg) + # Already JSON — just make sure timestamp exists + if "timestamp" not in parsed: + parsed["timestamp"] = datetime.now(timezone.utc).isoformat() + parsed["id"] = self.log_id + return json.dumps(parsed) + + except json.JSONDecodeError: + pass # continue to default formatting + + # Not JSON, so create our standard JSON structure + log_entry = { + "id": self.log_id, + "timestamp": datetime.now(timezone.utc).isoformat(), "level": record.levelname, - "message": record.getMessage() + "message": msg, } + return json.dumps(log_entry) - # Include optional fields if present - if hasattr(record, "source"): - log_dict["source"] = record.source - - return json.dumps(log_dict, ensure_ascii=False) diff --git a/webserver/logger/logger.py b/webserver/logger/logger.py index 93b4a25e..d28e026b 100644 --- a/webserver/logger/logger.py +++ b/webserver/logger/logger.py @@ -1,4 +1,5 @@ import logging +import sys from .formatter import JsonFormatter from .bufferhandler import BufferHandler @@ -11,28 +12,15 @@ def get_logger(name: str = "logger", collector_logger = logging.getLogger(name) collector_logger.setLevel(logging.DEBUG) - handler = logging.StreamHandler() + handler = logging.StreamHandler(sys.stdout) handler.setFormatter(JsonFormatter()) collector_logger.addHandler(handler) buffer_handler = None - + # Use buffer handler for log messages if use_buffer: - # Use buffer handler for log messages buffer_handler = BufferHandler() - buffer_handler.setFormatter( - logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) + buffer_handler.setFormatter(JsonFormatter()) collector_logger.addHandler(buffer_handler) - if use_buffer: - # Find buffer handler again if it already exists - if buffer_handler is None: - for h in collector_logger.handlers: - if isinstance(h, BufferHandler): - buffer_handler = h - break - return collector_logger, buffer_handler - else: - return collector_logger, None - - # return collector_logger + return collector_logger, buffer_handler diff --git a/webserver/plcapp_management.py b/webserver/plcapp_management.py index 3905ee66..26375ea7 100644 --- a/webserver/plcapp_management.py +++ b/webserver/plcapp_management.py @@ -7,13 +7,14 @@ from typing import Final from runtimemanager import RuntimeManager +from logger import get_logger, LogParser + +logger, _ = get_logger("runtime", use_buffer=True) -# logger = logging.getLogger("logger") MAX_FILE_SIZE: Final[int] = 10 * 1024 * 1024 # 10 MB per file MAX_TOTAL_SIZE: Final[int] = 50 * 1024 * 1024 # 50 MB total DISALLOWED_EXT = (".exe", ".dll", ".sh", ".bat", ".js", ".vbs", ".scr") -ALLOWED_FILENAME = "create_standard_function_txt.sh" class BuildStatus(Enum): IDLE = auto() @@ -67,8 +68,8 @@ def analyze_zip(zip_path) -> tuple[bool, list]: # Check uncompressed size if uncompressed_size > MAX_FILE_SIZE: - # logger.warning("File too large: %s (%d bytes)", - # filename, uncompressed_size) + logger.warning("File too large: %s (%d bytes)", + filename, uncompressed_size) safe = False # Check compression ratio (ZIP bomb detection) @@ -78,12 +79,10 @@ def analyze_zip(zip_path) -> tuple[bool, list]: safe = False # Check disallowed extensions - # TODO remove this additional BASH SCRIPT check - if ALLOWED_FILENAME not in filename: - if ext in DISALLOWED_EXT: - print("Disallowed extension: %s", - filename) - safe = False + if ext in DISALLOWED_EXT: + logger.warning("Disallowed extension: %s", + filename) + safe = False total_size += uncompressed_size valid_files.append(info) @@ -94,10 +93,10 @@ def analyze_zip(zip_path) -> tuple[bool, list]: # total_size) safe = False - # if safe: - # logger.info("ZIP file looks safe to extract (based on static checks).") - # else: - # logger.warning("ZIP file failed safety checks.") + if safe: + logger.debug("ZIP file looks safe to extract (based on static checks).") + else: + logger.warning("ZIP file failed safety checks.") return safe, valid_files @@ -148,7 +147,7 @@ def safe_extract(zip_path, dest_dir, valid_files): with zf.open(info) as src, open(out_path, "wb") as dst: dst.write(src.read()) - # logger.info("Extracted: %s", out_path) + logger.debug("Extracted: %s", out_path) def run_compile(runtime_manager: RuntimeManager, cwd: str = "core/generated"): """Run compile script synchronously (wait for completion) and update status/logs.""" diff --git a/webserver/restapi.py b/webserver/restapi.py index 0a6667b5..06e817e7 100644 --- a/webserver/restapi.py +++ b/webserver/restapi.py @@ -1,4 +1,3 @@ -import logging import os from typing import Callable, Optional @@ -13,9 +12,9 @@ ) from flask_sqlalchemy import SQLAlchemy from werkzeug.security import check_password_hash, generate_password_hash -# from logger import get_logger, LogParser +from logger import get_logger, LogParser -# logger = get_logger(use_buffer=True) +logger, buffer = get_logger("logger", use_buffer=True) env = os.getenv("FLASK_ENV", "development") @@ -37,9 +36,14 @@ @jwt.token_in_blocklist_loader def check_if_token_revoked(jwt_header, jwt_payload): - jti = jwt_payload["jti"] - return jti in jwt_blacklist - + try: + jti = jwt_payload["jti"] + return jti in jwt_blacklist + except KeyError as e: + logger.error("Error revoking JWT: %s", e) + except Exception as e: + logger.error("Error revoking JWT: %s", e) + return False class User(db.Model): # type: ignore[name-defined] __tablename__ = "users" @@ -57,7 +61,6 @@ def set_password(self, password: str) -> str: self.password_hash = generate_password_hash( password, method=self.derivation_method ) - # logger.debug("Password set for user %s | %s", self.username, self.password_hash) return self.password_hash def check_password(self, password: str) -> bool: @@ -82,13 +85,13 @@ def user_lookup_callback(_jwt_header, jwt_data): def register_callback_get(callback: Callable[[str, dict], dict]): global _handler_callback_get _handler_callback_get = callback - # logger.info("GET Callback registered successfully for rest_blueprint!") + logger.debug("GET Callback registered successfully for rest_blueprint!") def register_callback_post(callback: Callable[[str, dict], dict]): global _handler_callback_post _handler_callback_post = callback - # logger.info("POST Callback registered successfully for rest_blueprint!") + logger.debug("POST Callback registered successfully for rest_blueprint!") @restapi_bp.route("/create-user", methods=["POST"]) @@ -97,8 +100,8 @@ def create_user(): try: users_exist = User.query.first() is not None except Exception as e: - # logger.error("Error checking for users: %s", e) - return jsonify({"msg": "User creation error"}), 401 + logger.error("Error checking for users: %s", e) + return jsonify({"msg": f"User creation error: {e}"}), 401 # if there are no users, we don't need to verify JWT if users_exist and verify_jwt_in_request(optional=True) is None: @@ -131,8 +134,8 @@ def get_user_info(user_id): try: user = User.query.get(user_id) except Exception as e: - # logger.error("Error retrieving user: %s", e) - return jsonify({"msg": "User retrieval error"}), 500 + logger.error("Error retrieving user: %s", e) + return jsonify({"msg": f"User retrieval error: {e}"}), 500 if not user: return jsonify({"msg": "User not found"}), 404 @@ -162,7 +165,7 @@ def get_users_info(): try: users = User.query.all() except Exception as e: - # logger.error("Error retrieving users: %s", e) + logger.error("Error retrieving users: %s", e) return jsonify({"msg": "User retrieval error"}), 500 return jsonify([user.to_dict() for user in users]), 200 @@ -182,7 +185,7 @@ def change_password(user_id): try: user = User.query.get(user_id) except Exception as e: - # logger.error("Error retrieving user: %s", e) + logger.error("Error retrieving user: %s", e) return jsonify({"msg": "User retrieval error"}), 500 if not user: @@ -207,8 +210,8 @@ def delete_user(user_id): try: user = User.query.get(user_id) except Exception as e: - # logger.error("Error retrieving user: %s", e) - return jsonify({"msg": "User retrieval error"}), 500 + logger.error("Error retrieving user: %s", e) + return jsonify({"msg": f"User retrieval error: {e}"}), 500 if not user: return jsonify({"msg": "User not found"}), 404 @@ -227,10 +230,10 @@ def login(): try: user = User.query.filter_by(username=username).one_or_none() - # logger.debug("User found: %s", user) + logger.debug("User found: %s", user) except Exception as e: - # logger.error("Error retrieving user: %s", e) - return jsonify({"msg": "User retrieval error"}), 500 + logger.error("Error retrieving user: %s", e) + return jsonify({"msg": f"User retrieval error: {e}"}), 500 if not user or not user.check_password(password): return jsonify("Wrong username or password"), 401 @@ -248,13 +251,16 @@ def logout(): def revoke_jwt(): - jti = get_jwt()["jti"] + # Add the JWT ID to the blacklist try: - # Add the JWT ID to the blacklist + jti = get_jwt()["jti"] jwt_blacklist.add(jti) + except KeyError as e: + logger.error("Error revoking JWT: %s", e) + except AttributeError as e: + logger.error("Error revoking JWT: %s", e) except Exception as e: - # logger.error("Error revoking JWT: %s", e) - pass + logger.error("Error revoking JWT: %s", e) @restapi_bp.route("/", methods=["GET"]) @@ -267,7 +273,6 @@ def restapi_plc_get(command): data = request.args.to_dict() result = _handler_callback_get(command, data) return jsonify(result), 200 - except Exception as e: # logger.error("Error in restapi_plc_get: %s", e) return jsonify({"error": str(e)}), 500 @@ -281,7 +286,6 @@ def restapi_plc_post(command): try: data = request.get_json(silent=True) or {} - result = _handler_callback_post(command, data) return jsonify(result), 200 except Exception as e: diff --git a/webserver/runtimemanager.py b/webserver/runtimemanager.py index 7bf6c42b..ab197fdb 100644 --- a/webserver/runtimemanager.py +++ b/webserver/runtimemanager.py @@ -9,7 +9,7 @@ from unixclient import SyncUnixClient from logger import get_logger, LogParser -logger, buffer = get_logger(use_buffer=True) +logger, buffer = get_logger("logger", use_buffer=True) class RuntimeManager: @@ -204,12 +204,13 @@ def stop(self): self._safe_close_runtime_socket() - def get_logs(self): + def get_logs(self, min_id=None, level=None): """ Get current logs from the runtime """ try: - _logs = buffer.normalize_buffer_logs(buffer.get_logs()) + _logs = buffer.normalize_logs( + buffer.get_logs(min_id=min_id, level=level)) return _logs except AttributeError as e: logger.error("Failed to get logs from buffer: %s", e) diff --git a/webserver/unixclient.py b/webserver/unixclient.py index f1382e5c..99900cc8 100644 --- a/webserver/unixclient.py +++ b/webserver/unixclient.py @@ -3,10 +3,9 @@ import re from typing import Optional from threading import Lock +from logger import get_logger, LogParser -# from logger import get_logger, LogParser - -# logger = get_logger(use_buffer=True) +logger, _ = get_logger(use_buffer=True) mutex = Lock() @@ -14,14 +13,6 @@ class SyncUnixClient: def __init__(self, socket_path="/run/runtime/plc_runtime.socket"): self.socket_path = socket_path self.sock: Optional[socket.socket] = None - - def validate_message(self, message: str) -> bool: - """Validate message format""" - if not message or len(message) > 100: - return False - if not re.match(r"^[\w\s.,!?\-]+$", message): - return False - return True def is_connected(self): with mutex: @@ -35,14 +26,13 @@ def connect(self): raise FileNotFoundError(f"Socket not found: {self.socket_path}") try: - # logger.info("Connecting to socket %s", self.socket_path) + logger.debug("Connecting to socket %s", self.socket_path) self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.settimeout(1.0) # 1s timeout on blocking calls self.sock.connect(self.socket_path) - # logger.info("Connected to server socket %s", self.socket_path) + logger.debug("Connected to server socket %s", self.socket_path) except Exception as e: - # logger.error("Failed to connect: %s", e) - raise + logger.error("Failed to connect: %s", e) def send_message(self, msg: str): if not self.sock: @@ -54,8 +44,7 @@ def send_message(self, msg: str): self.sock.sendall(data) # logger.info("Sent message: %s", data) except Exception as e: - # logger.error("Error sending message: %s", e) - raise + logger.error("Error sending message: %s", e) def recv_message(self, timeout: float = 0.5) -> Optional[str]: """Receive message from the server""" @@ -70,10 +59,10 @@ def recv_message(self, timeout: float = 0.5) -> Optional[str]: # logger.warning("Connection closed by server") return None message = data.decode("utf-8").strip() - # logger.info("Received message: %s", message) + logger.debug("Received message: %s", message) return message except socket.timeout: - # logger.debug("Timeout waiting for message") + logger.warning("Timeout waiting for message") return None except Exception as e: # logger.error("Error receiving message: %s", e) @@ -81,7 +70,7 @@ def recv_message(self, timeout: float = 0.5) -> Optional[str]: def close(self): if self.sock: - # logger.info("Closing connection") + logger.debug("Closing connection") try: self.sock.close() finally: diff --git a/webserver/unixserver.py b/webserver/unixserver.py index c849025e..b936722c 100644 --- a/webserver/unixserver.py +++ b/webserver/unixserver.py @@ -1,10 +1,9 @@ -import json import socket import threading import os from logger import get_logger, LogParser -logger, buffer = get_logger(use_buffer=True) +logger, _ = get_logger("runtime", use_buffer=True) parser = LogParser(logger) @@ -15,7 +14,6 @@ def __init__(self, socket_path="/run/runtime/log_runtime.socket"): self.clients = [] self.lock = threading.Lock() self.running = False - # self.parser = LogParser(logger) def start(self): """Start the Unix socket server""" @@ -41,7 +39,6 @@ def start(self): logger.error("Failed to start server: %s", e) except Exception as e: logger.error("Failed to start server (unexpected): %s", e) - raise def _accept_clients(self): """Accept incoming client connections""" @@ -53,8 +50,7 @@ def _accept_clients(self): threading.Thread(target=self._handle_client, args=(client_sock,), daemon=True).start() logger.info("Client connected") except (OSError, socket.error) as e: - if self.running: - logger.error("Socket error: %s", e) + logger.error("Socket error: %s", e) except Exception as e: logger.error("Error accepting client: %s", e) @@ -93,4 +89,6 @@ def stop(self): except OSError: if os.path.exists(self.socket_path): logger.error("Failed to remove socket file") + except Exception as e: + logger.error("Error during server shutdown: %s", e) logger.info("Log server stopped")