-
Notifications
You must be signed in to change notification settings - Fork 19
Fix Flash engine startup on Windows #145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -574,21 +574,27 @@ def _emergency_cleanup() -> None: | |
|
|
||
| atexit.register(_emergency_cleanup) | ||
|
|
||
| _prev_sigterm: Any = None | ||
| _prev_sighup: Any = None | ||
| _signals_installed = False | ||
| try: | ||
| _prev_sigterm = signal.getsignal(signal.SIGTERM) | ||
| _prev_sighup = signal.getsignal(signal.SIGHUP) | ||
| _previous_signal_handlers: dict[int, Any] = {} | ||
| signals_to_install = [signal.SIGTERM] | ||
|
|
||
| sighup = getattr(signal, "SIGHUP", None) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: add unit tests for this platform-aware signal handling. You can use |
||
| if sighup is not None: | ||
| signals_to_install.append(sighup) | ||
|
|
||
| try: | ||
| def _signal_handler(signum: int, frame: Any) -> None: | ||
| sys.exit(128 + signum) | ||
|
|
||
| signal.signal(signal.SIGTERM, _signal_handler) | ||
| signal.signal(signal.SIGHUP, _signal_handler) | ||
| _signals_installed = True | ||
| for signum in signals_to_install: | ||
| _previous_signal_handlers[signum] = signal.getsignal(signum) | ||
| signal.signal(signum, _signal_handler) | ||
| except ValueError: | ||
| pass | ||
| for signum, previous_handler in _previous_signal_handlers.items(): | ||
| try: | ||
| signal.signal(signum, previous_handler) | ||
| except ValueError: | ||
| pass | ||
| _previous_signal_handlers.clear() | ||
|
|
||
| engine: Engine | None = None | ||
| state = _EventLoopState() | ||
|
|
@@ -918,9 +924,11 @@ def _on_context_overflow(messages: list[dict]) -> list[dict] | None: | |
| except Exception: | ||
| pass | ||
| atexit.unregister(_emergency_cleanup) | ||
| if _signals_installed: | ||
| signal.signal(signal.SIGTERM, _prev_sigterm) | ||
| signal.signal(signal.SIGHUP, _prev_sighup) | ||
| for signum, previous_handler in _previous_signal_handlers.items(): | ||
| try: | ||
| signal.signal(signum, previous_handler) | ||
| except ValueError: | ||
| pass | ||
|
|
||
|
|
||
| def _resolve_cdp_port( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| """Tests for MCPServer.start process command resolution.""" | ||
| from __future__ import annotations | ||
|
|
||
| import io | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| from webqa_agent.executor.flash.core import mcp_client | ||
| from webqa_agent.executor.flash.core.mcp_client import MCPError, MCPServer | ||
|
|
||
|
|
||
| class _FakeProc: | ||
| def __init__(self) -> None: | ||
| self.stdin = io.BytesIO() | ||
| self.stdout = io.BytesIO() | ||
| self.stderr = io.BytesIO() | ||
| self.pid = 12345 | ||
|
|
||
| def wait(self, timeout=None): | ||
| return 0 | ||
|
|
||
| def terminate(self) -> None: | ||
| pass | ||
|
|
||
| def kill(self) -> None: | ||
| pass | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def isolated_start(monkeypatch): | ||
| popen_calls = [] | ||
|
|
||
| def fake_popen(cmd, **kwargs): | ||
| popen_calls.append((list(cmd), dict(kwargs))) | ||
| return _FakeProc() | ||
|
|
||
| monkeypatch.setattr(mcp_client.subprocess, 'Popen', fake_popen) | ||
| monkeypatch.setattr(MCPServer, '_read_stdout', lambda self: None) | ||
| monkeypatch.setattr(MCPServer, '_drain_stderr', lambda self: None) | ||
| monkeypatch.setattr(MCPServer, '_initialize', lambda self, timeout_s: None) | ||
| monkeypatch.setattr(MCPServer, '_list_tools', lambda self, timeout_s: []) | ||
| return popen_calls | ||
|
|
||
|
|
||
| def test_start_uses_resolved_command_from_path(isolated_start, monkeypatch): | ||
| resolved = r'C:\Users\me\AppData\Roaming\npm\chrome-devtools-mcp.CMD' | ||
| monkeypatch.setattr(mcp_client.shutil, 'which', lambda command: resolved) | ||
|
|
||
| server = MCPServer( | ||
| name='browser', | ||
| command='chrome-devtools-mcp', | ||
| args=['--headless'], | ||
| ) | ||
| server.start() | ||
| server.shutdown() | ||
|
|
||
| assert isolated_start[0][0][0] == resolved | ||
| assert isolated_start[0][0][1:] == ['--headless'] | ||
|
|
||
|
|
||
| def test_start_raises_when_non_absolute_command_is_not_found( | ||
| isolated_start, monkeypatch, | ||
| ): | ||
| monkeypatch.setattr(mcp_client.shutil, 'which', lambda command: None) | ||
|
|
||
| server = MCPServer(name='browser', command='missing-mcp-server') | ||
| with pytest.raises(MCPError, match="command not found on PATH"): | ||
| server.start() | ||
|
|
||
| assert isolated_start == [] | ||
|
|
||
|
|
||
| def test_start_keeps_absolute_command_when_path_lookup_fails( | ||
| isolated_start, monkeypatch, tmp_path: Path, | ||
| ): | ||
| absolute_command = str(tmp_path / 'custom-mcp-server') | ||
| monkeypatch.setattr(mcp_client.shutil, 'which', lambda command: None) | ||
|
|
||
| server = MCPServer( | ||
| name='custom', | ||
| command=absolute_command, | ||
| args=['--stdio'], | ||
| ) | ||
| server.start() | ||
| server.shutdown() | ||
|
|
||
| assert isolated_start[0][0] == [absolute_command, '--stdio'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: add unit tests for command resolution. Mock
shutil.whichandsubprocess.Popenand assert Popen receives the resolved full path (covering the Windows.CMDshim case), the not-found + non-absolute case raisesMCPError, and the absolute-path fallback passes the original command through unchanged.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, good point. I’ll add unit tests covering both the Windows-like case where signal.SIGHUP is unavailable and the POSIX case where both SIGTERM and SIGHUP are installed and restored.