From 6a7fa46a5a5bab933228a9904ee7fd6b9dd052a2 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Wed, 14 Jan 2026 11:45:11 -0800 Subject: [PATCH 1/2] Add timeout and symbol length limits to demangling --- src/launchpad/utils/apple/cwl_demangle.py | 38 +++++++++++++++++++-- tests/integration/test_cwl_demangle.py | 40 +++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/launchpad/utils/apple/cwl_demangle.py b/src/launchpad/utils/apple/cwl_demangle.py index 6fe1019f..c8254c80 100644 --- a/src/launchpad/utils/apple/cwl_demangle.py +++ b/src/launchpad/utils/apple/cwl_demangle.py @@ -4,6 +4,7 @@ import shutil import subprocess import tempfile +import time import uuid from dataclasses import dataclass @@ -13,6 +14,13 @@ logger = get_logger(__name__) +# Default timeout for cwl-demangle subprocess (in seconds) +DEFAULT_DEMANGLE_TIMEOUT = 30 + +# Maximum symbol length to send to cwl-demangle (in characters) +# Symbols longer than this are skipped and used in mangled form +DEFAULT_MAX_SYMBOL_LENGTH = 1500 + @dataclass class CwlDemangleResult: @@ -149,6 +157,25 @@ def _demangle_chunk_worker( if not chunk: return {} + start_time = time.time() + + # Filter out extremely long symbols that cause cwl-demangle to hang + filtered_chunk = [s for s in chunk if len(s) <= DEFAULT_MAX_SYMBOL_LENGTH] + skipped_count = len(chunk) - len(filtered_chunk) + + if skipped_count > 0: + logger.warning( + f"Chunk {chunk_idx}: Skipping {skipped_count} symbols longer than {DEFAULT_MAX_SYMBOL_LENGTH} chars " + f"(will use mangled form for these)" + ) + + # Use filtered chunk for processing + chunk = filtered_chunk + + if not chunk: + logger.warning(f"Chunk {chunk_idx}: All symbols filtered out") + return {} + binary_path = shutil.which("cwl-demangle") if binary_path is None: logger.error("cwl-demangle binary not found in PATH") @@ -178,9 +205,16 @@ def _demangle_chunk_worker( command_parts.append("--continue-on-error") try: - result = subprocess.run(command_parts, capture_output=True, text=True, check=True) + result = subprocess.run( + command_parts, capture_output=True, text=True, check=True, timeout=DEFAULT_DEMANGLE_TIMEOUT + ) + except subprocess.TimeoutExpired: + elapsed = time.time() - start_time + logger.warning(f"Chunk {chunk_idx} timed out after {elapsed:.1f}s") + return {} except subprocess.CalledProcessError: - logger.exception(f"cwl-demangle failed for chunk {chunk_idx}") + elapsed = time.time() - start_time + logger.error(f"Chunk {chunk_idx} failed after {elapsed:.1f}s") return {} batch_result = json.loads(result.stdout) diff --git a/tests/integration/test_cwl_demangle.py b/tests/integration/test_cwl_demangle.py index 2d87b5e4..01a8f5a6 100644 --- a/tests/integration/test_cwl_demangle.py +++ b/tests/integration/test_cwl_demangle.py @@ -100,6 +100,46 @@ def test_environment_variable_disables_parallel(self): demangler = CwlDemangler() assert demangler.use_parallel is False + def test_timeout_configuration(self): + """Test LAUNCHPAD_DEMANGLE_TIMEOUT_SECONDS env var configures timeout.""" + demangler = CwlDemangler() + + # Test with custom timeout + with mock.patch.dict(os.environ, {"LAUNCHPAD_DEMANGLE_TIMEOUT_SECONDS": "30"}): + # Generate a few symbols to trigger sequential processing + symbols = self._generate_symbols(100) + for symbol in symbols: + demangler.add_name(symbol) + + result = demangler.demangle_all() + # Should succeed with custom timeout + assert len(result) == 100 + + def test_max_symbol_length_filtering(self): + """Test LAUNCHPAD_MAX_DEMANGLE_SYMBOL_LENGTH env var filters long symbols.""" + demangler = CwlDemangler() + + # Generate normal symbols + normal_symbols = self._generate_symbols(10) + + # Create an artificially long symbol (>1500 chars) + very_long_symbol = "_$s" + "A" * 2000 + "V" + + for symbol in normal_symbols: + demangler.add_name(symbol) + demangler.add_name(very_long_symbol) + + # Test with default filtering (1500 chars) + result = demangler.demangle_all() + + # Normal symbols should be demangled, long symbol should be skipped + assert len(result) == 10 # Only normal symbols + assert very_long_symbol not in result + + # Verify normal symbols were processed + for symbol in normal_symbols: + assert symbol in result + def _generate_symbols(self, count: int) -> list[str]: """Generate valid Swift mangled symbols.""" letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" From e5d8c3acb0454b829f3eb24ff5e28471fc75a028 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Wed, 14 Jan 2026 11:59:13 -0800 Subject: [PATCH 2/2] add env var backing --- src/launchpad/utils/apple/cwl_demangle.py | 4 ++-- tests/integration/test_cwl_demangle.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/launchpad/utils/apple/cwl_demangle.py b/src/launchpad/utils/apple/cwl_demangle.py index c8254c80..50a2ecc4 100644 --- a/src/launchpad/utils/apple/cwl_demangle.py +++ b/src/launchpad/utils/apple/cwl_demangle.py @@ -15,11 +15,11 @@ logger = get_logger(__name__) # Default timeout for cwl-demangle subprocess (in seconds) -DEFAULT_DEMANGLE_TIMEOUT = 30 +DEFAULT_DEMANGLE_TIMEOUT = int(os.environ.get("LAUNCHPAD_DEMANGLE_TIMEOUT", "30")) # Maximum symbol length to send to cwl-demangle (in characters) # Symbols longer than this are skipped and used in mangled form -DEFAULT_MAX_SYMBOL_LENGTH = 1500 +DEFAULT_MAX_SYMBOL_LENGTH = int(os.environ.get("LAUNCHPAD_MAX_DEMANGLE_SYMBOL_LENGTH", "1500")) @dataclass diff --git a/tests/integration/test_cwl_demangle.py b/tests/integration/test_cwl_demangle.py index 01a8f5a6..b5c74e58 100644 --- a/tests/integration/test_cwl_demangle.py +++ b/tests/integration/test_cwl_demangle.py @@ -101,11 +101,11 @@ def test_environment_variable_disables_parallel(self): assert demangler.use_parallel is False def test_timeout_configuration(self): - """Test LAUNCHPAD_DEMANGLE_TIMEOUT_SECONDS env var configures timeout.""" + """Test LAUNCHPAD_DEMANGLE_TIMEOUT env var configures timeout.""" demangler = CwlDemangler() # Test with custom timeout - with mock.patch.dict(os.environ, {"LAUNCHPAD_DEMANGLE_TIMEOUT_SECONDS": "30"}): + with mock.patch.dict(os.environ, {"LAUNCHPAD_DEMANGLE_TIMEOUT": "30"}): # Generate a few symbols to trigger sequential processing symbols = self._generate_symbols(100) for symbol in symbols: