Skip to main content
Glama
seletz
by seletz
shell_manager.py7.21 kB
""" Odoo Shell Manager ================== Manages a persistent Odoo shell subprocess with threaded I/O handling. """ import os import queue import subprocess import threading import time from typing import Dict, Any, Optional class OdooShellManager: """ Manages a persistent Odoo shell subprocess. This class handles the lifecycle of an Odoo shell process, including: - Starting the shell with appropriate configuration - Managing input/output communication through queues - Executing code and capturing results - Handling process lifecycle :param odoo_bin_path: Path to the odoo-bin executable :type odoo_bin_path: str :param addons_path: Comma-separated list of addon directories :type addons_path: str :param db_name: Name of the Odoo database to connect to :type db_name: str :param config_file: Optional path to Odoo configuration file :type config_file: Optional[str] """ def __init__(self, odoo_bin_path: str, addons_path: str, db_name: str, config_file: Optional[str] = None): self.odoo_bin_path = odoo_bin_path self.addons_path = addons_path self.db_name = db_name self.config_file: Optional[str] = config_file self.process: Optional[subprocess.Popen] = None self.input_queue: queue.Queue[str] = queue.Queue() self.output_queue: queue.Queue[str] = queue.Queue() self.session_vars: Dict[str, Any] = {} def start_shell(self) -> None: """ Start the Odoo shell subprocess. Constructs the command line arguments and starts the Odoo shell process with appropriate configuration. Also starts input/output handling threads and waits for the initial shell prompt. :raises TimeoutError: If the shell doesn't start within the expected timeframe """ cmd = [ self.odoo_bin_path, 'shell', '--addons-path', self.addons_path, '--database', self.db_name, '--no-http' ] if self.config_file: cmd.extend(['--config', self.config_file]) self.process = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True ) # Start threads to handle input/output threading.Thread(target=self._input_thread, daemon=True).start() threading.Thread(target=self._output_thread, daemon=True).start() # Wait for initial prompt self._wait_for_prompt() def _input_thread(self) -> None: """ Handle input to Odoo shell. Runs in a separate thread to send code from the input queue to the Odoo shell process stdin. """ while self.process and self.process.poll() is None: try: code = self.input_queue.get(timeout=1) if code: self.process.stdin.write(code + '\n') self.process.stdin.flush() except queue.Empty: continue def _output_thread(self) -> None: """ Handle output from Odoo shell. Runs in a separate thread to read output from the Odoo shell process stdout and place it in the output queue for consumption. """ buffer = [] while self.process and self.process.poll() is None: try: char = self.process.stdout.read(1) if char: buffer.append(char) if char == '\n' or len(buffer) > 1000: output = ''.join(buffer) self.output_queue.put(output) buffer = [] except Exception as e: print(f"Error reading shell output: {e}") break def _wait_for_prompt(self) -> None: """ Wait for the Odoo shell prompt to appear. Monitors the output queue until a recognizable shell prompt is detected, indicating that the shell is ready to accept commands. :raises TimeoutError: If no prompt is detected within 10 seconds """ output = "" while True: try: chunk = self.output_queue.get(timeout=10) output += chunk # Look for the typical Odoo shell prompt if ">>>" in output or "In [" in output: break except queue.Empty: raise TimeoutError("Timeout waiting for Odoo shell prompt") def execute_code(self, code: str, timeout: int = 30) -> str: """ Execute code in the Odoo shell and return output. Sends the provided code to the Odoo shell process and collects the resulting output until a new prompt appears or timeout occurs. :param code: Python code to execute in the Odoo shell context :type code: str :param timeout: Maximum time to wait for execution completion in seconds :type timeout: int :return: The output from executing the code :rtype: str :raises TimeoutError: If execution doesn't complete within timeout """ if not self.process or self.process.poll() is not None: self.start_shell() # Clear output queue while not self.output_queue.empty(): self.output_queue.get() # Send code self.input_queue.put(code) # Collect output output_lines = [] start_time = time.time() while True: try: chunk = self.output_queue.get(timeout=1) output_lines.append(chunk) # Check if we got a new prompt (indicating completion) if (">>>" in chunk or "In [" in chunk) and len(output_lines) > 1: break # Timeout check if time.time() - start_time > timeout: raise TimeoutError(f"Command execution timed out after {timeout} seconds") except queue.Empty: if time.time() - start_time > timeout: raise TimeoutError(f"Command execution timed out after {timeout} seconds") continue return ''.join(output_lines).strip() def stop(self) -> None: """ Stop the Odoo shell. Terminates the Odoo shell process and waits for it to exit cleanly. """ if self.process: self.process.terminate() self.process.wait() self.process = None def __enter__(self): """Context manager entry.""" self.start_shell() return self def __exit__(self, exc_type, exc_val, exc_tb): """Context manager exit.""" self.stop()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/seletz/mcp-odoo-shell'

If you have feedback or need assistance with the MCP directory API, please join our Discord server