From cd9365d183c13a3522788648d0a85b702bc8886b Mon Sep 17 00:00:00 2001 From: sdubey-ks Date: Tue, 9 Dec 2025 15:40:30 +0530 Subject: [PATCH] Added fixes for service mode REST Api execution in background mode --- keepercommander/__main__.py | 13 ++- keepercommander/service/api/command.py | 18 +++- .../service/core/service_manager.py | 100 +++++++++++++++--- .../service/util/request_validation.py | 11 +- 4 files changed, 121 insertions(+), 21 deletions(-) diff --git a/keepercommander/__main__.py b/keepercommander/__main__.py index 30a3faf23..ff2dd2f30 100644 --- a/keepercommander/__main__.py +++ b/keepercommander/__main__.py @@ -222,6 +222,17 @@ def main(from_package=False): if from_package: sys.excepthook = handle_exceptions + # Check if we're running a service wrapper script (for background service mode in PyInstaller executable) + # If so, execute the script directly without argument parsing + if len(sys.argv) > 1 and sys.argv[1].endswith('service_wrapper.py'): + import runpy + try: + runpy.run_path(sys.argv[1], run_name='__main__') + return + except Exception as e: + logging.error(f"Failed to run service wrapper script: {e}") + sys.exit(1) + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) opts, flags = parser.parse_known_args(sys.argv[1:]) @@ -323,7 +334,7 @@ def main(from_package=False): if isinstance(params.timedelay, int) and params.timedelay >= 1 and params.commands: cli.runcommands(params) else: - if opts.command in {'shell', 'login', '-'}: + if opts.command in {'shell', '-'}: if opts.command == '-': params.batch_mode = True elif opts.command and os.path.isfile(opts.command): diff --git a/keepercommander/service/api/command.py b/keepercommander/service/api/command.py index 2cb6c832a..e77bdba46 100644 --- a/keepercommander/service/api/command.py +++ b/keepercommander/service/api/command.py @@ -38,12 +38,17 @@ def execute_command_direct(**kwargs) -> Tuple[Union[Response, bytes], int]: if json_error: return json_error - command, validation_error = RequestValidator.validate_and_escape_command(request.json) + # Get JSON data safely + request_data = request.get_json(force=True, silent=True) + if not request_data: + return jsonify({"status": "error", "error": "Invalid or empty JSON"}), 400 + + command, validation_error = RequestValidator.validate_and_escape_command(request_data) if validation_error: return validation_error # Process file data if present - processed_command, temp_files = RequestValidator.process_file_data(request.json, command) + processed_command, temp_files = RequestValidator.process_file_data(request_data, command) response, status_code = CommandExecutor.execute(processed_command) @@ -78,12 +83,17 @@ def execute_command(**kwargs) -> Tuple[Response, int]: if json_error: return json_error - command, validation_error = RequestValidator.validate_and_escape_command(request.json) + # Get JSON data safely + request_data = request.get_json(force=True, silent=True) + if not request_data: + return jsonify({"status": "error", "error": "Invalid or empty JSON"}), 400 + + command, validation_error = RequestValidator.validate_and_escape_command(request_data) if validation_error: return validation_error # Process file data if present - processed_command, temp_files = RequestValidator.process_file_data(request.json, command) + processed_command, temp_files = RequestValidator.process_file_data(request_data, command) # Submit to queue and return request ID immediately try: diff --git a/keepercommander/service/core/service_manager.py b/keepercommander/service/core/service_manager.py index d598207f3..1bdce6694 100644 --- a/keepercommander/service/core/service_manager.py +++ b/keepercommander/service/core/service_manager.py @@ -126,38 +126,112 @@ def filter(self, record): if config_data.get("run_mode") == "background": - base_dir = os.path.dirname(os.path.abspath(__file__)) service_module = "keepercommander.service.core.service_app" # Use module path instead of file path - python_executable = sys.executable - - # Create logs directory for subprocess output - log_dir = os.path.join(base_dir, "logs") + + # Detect if running as PyInstaller executable + is_frozen = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') + + # Create logs directory for subprocess output in user-writable location + # Use utils.get_default_path() to avoid permission issues in packaged apps + log_dir = os.path.join(utils.get_default_path(), "service_logs") os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, "service_subprocess.log") try: - if sys.platform == "win32": - subprocess.DETACHED_PROCESS = 0x00000008 - with open(log_file, 'w') as log_f: + python_executable = sys.executable + + if is_frozen: + # Running as PyInstaller executable - create a wrapper script and execute it + # PyInstaller executables can execute Python files, but we need to use the right approach + wrapper_script = os.path.join(log_dir, "service_wrapper.py") + wrapper_content = """# Service wrapper for PyInstaller executable +import sys +import os + +# Set flag to indicate we're running in service mode (bypass main entry point) +os.environ['KEEPER_SERVICE_BACKGROUND'] = '1' + +# Ensure we can import the service module +if hasattr(sys, '_MEIPASS'): + if sys._MEIPASS not in sys.path: + sys.path.insert(0, sys._MEIPASS) + +# Clear sys.argv to avoid argument parsing issues +sys.argv = [sys.argv[0]] + +# Import and run the service module directly +from keepercommander.service.app import create_app +from keepercommander.service.config.service_config import ServiceConfig +from keepercommander.service.core.service_manager import ServiceManager + +flask_app = create_app() + +service_config = ServiceConfig() +config_data = service_config.load_config() + +try: + from keepercommander.service.core.globals import ensure_params_loaded + print("Pre-loading Keeper parameters for background mode...") + ensure_params_loaded() + print("Keeper parameters loaded successfully") +except Exception as e: + print("Warning: Failed to pre-load parameters during startup: " + str(e)) + print("Parameters will be loaded on first API call if needed") + +if not (port := config_data.get("port")): + print("Error: Service configuration is incomplete. Please configure the service port in service_config") + sys.exit(1) + +ssl_context = ServiceManager.get_ssl_context(config_data) + +flask_app.run( + host='0.0.0.0', + port=port, + ssl_context=ssl_context +) +""" + with open(wrapper_script, 'w') as f: + f.write(wrapper_content) + # Use the executable to run the Python script directly + # PyInstaller executables can execute .py files if we use the right method + cmd = [python_executable, wrapper_script] + else: + # Running as Python script - use -m flag + cmd = [python_executable, '-m', service_module] + + # Open log file in append mode and keep it open for the subprocess + # Use 'a' mode to append, and don't close the file handle immediately + log_f = open(log_file, 'a', buffering=1) # Line buffering + + try: + if sys.platform == "win32": + subprocess.DETACHED_PROCESS = 0x00000008 cls = subprocess.Popen( - [python_executable, '-m', service_module], + cmd, creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP, stdout=log_f, stderr=subprocess.STDOUT, # Combine stderr with stdout + stdin=subprocess.DEVNULL, # Close stdin to avoid issues cwd=os.getcwd(), # Use current working directory to access config files env=os.environ.copy() # Inherit environment variables ) - else: - # For macOS and Linux - improved subprocess handling - with open(log_file, 'w') as log_f: + else: + # For macOS and Linux - improved subprocess handling cls = subprocess.Popen( - [python_executable, '-m', service_module], + cmd, stdout=log_f, stderr=subprocess.STDOUT, # Combine stderr with stdout + stdin=subprocess.DEVNULL, # Close stdin to avoid issues preexec_fn=os.setpgrp, cwd=os.getcwd(), # Use current working directory to access config files env=os.environ.copy() # Inherit environment variables ) + # Don't close log_f here - let the subprocess handle it + # The file will be closed when the subprocess exits + except Exception: + # Only close on error + log_f.close() + raise logger.debug(f"Service subprocess logs available at: {log_file}") print(f"Commander Service started with PID: {cls.pid}") diff --git a/keepercommander/service/util/request_validation.py b/keepercommander/service/util/request_validation.py index f2750f98b..c97bac739 100644 --- a/keepercommander/service/util/request_validation.py +++ b/keepercommander/service/util/request_validation.py @@ -127,8 +127,13 @@ def validate_request_json() -> Optional[Tuple]: logger.info("Request validation failed: Content-Type must be application/json") return jsonify({"status": "error", "error": "Content-Type must be application/json"}), 400 - if not request.json: - logger.info("Request validation failed: Invalid or empty JSON") - return jsonify({"status": "error", "error": "Invalid or empty JSON"}), 400 + try: + json_data = request.get_json(force=True, silent=False) + if json_data is None: + logger.info("Request validation failed: Invalid or empty JSON") + return jsonify({"status": "error", "error": "Invalid or empty JSON"}), 400 + except Exception as e: + logger.warning(f"Request validation failed: JSON parsing error - {e}") + return jsonify({"status": "error", "error": f"Invalid JSON format: {str(e)}"}), 400 return None