From 80a4caf36a1692c71888bb92ab0f03ebe7637c73 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 3 Jul 2025 12:23:56 +0200 Subject: [PATCH] Use a separate process to call webbrowser open, redirecting output at the OS level to avoid stdout contamination #257 --- src/serena/agent.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 52ef28c8..96753ef7 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -2,7 +2,7 @@ The Serena Model Context Protocol (MCP) Server """ -import contextlib +import multiprocessing import os import platform import sys @@ -273,11 +273,10 @@ class SerenaAgent: Logger.root.addHandler(dashboard_log_handler) self._dashboard_thread, port = SerenaDashboardAPI(dashboard_log_handler, tool_names).run_in_thread() if self.serena_config.web_dashboard_open_on_launch: - # open the dashboard URL in the default web browser, making sure to redirect output, - # as this can print to stdout (contaminating the MCP server stream) - with open(os.devnull, "w") as fnull: - with contextlib.redirect_stdout(fnull), contextlib.redirect_stderr(fnull): - webbrowser.open(f"http://localhost:{port}/dashboard/index.html") + # open the dashboard URL in the default web browser (using a separate process to control + # output redirection) + process = multiprocessing.Process(target=self._open_dashboard, args=(port,)) + process.start() # log fundamental information log.info(f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()})") @@ -326,6 +325,18 @@ class SerenaAgent: "You should now pass either --project or --project ." ) + @staticmethod + def _open_dashboard(port: int) -> None: + # Redirect stdout and stderr file descriptors to /dev/null, + # making sure that nothing can be written to stdout/stderr, even by subprocesses + null_fd = os.open(os.devnull, os.O_WRONLY) + os.dup2(null_fd, sys.stdout.fileno()) + os.dup2(null_fd, sys.stderr.fileno()) + os.close(null_fd) + + # open the dashboard URL in the default web browser + webbrowser.open(f"http://localhost:{port}/dashboard/index.html") + def get_project_root(self) -> str: """ :return: the root directory of the active project (if any); raises a ValueError if there is no active project