Skip to content

Commit 893208a

Browse files
committed
fix: improve count_records reliability for tmux console
The previous implementation used execute_query which relies on _send_command_to_tmux without waiting for command completion. This caused race conditions where the count value wasn't captured. Changes: - Use unique markers with token_hex for reliable output extraction - Wait for end marker to appear in tmux output (up to 30 seconds) - Parse output with exact marker matching to avoid matching echoed commands - Add fallback scanning in case of buffer noise This fix was essential for running cleanup_openproject.py which needs accurate count values to track deletion progress.
1 parent 08ff5db commit 893208a

File tree

1 file changed

+74
-4
lines changed

1 file changed

+74
-4
lines changed

src/clients/openproject_client.py

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,11 +1534,81 @@ def count_records(self, model: str) -> int:
15341534
QueryExecutionError: If the count query fails
15351535
15361536
"""
1537-
result = self.execute_query(f"{model}.count")
1537+
import shutil
1538+
import subprocess
1539+
1540+
# Use unique markers to extract count reliably from tmux output
1541+
marker_id = secrets.token_hex(8)
1542+
start_marker = f"J2O_COUNT_START_{marker_id}"
1543+
end_marker = f"J2O_COUNT_END_{marker_id}"
1544+
1545+
# Simple inline command that prints markers around the count
1546+
query = f'puts "{start_marker}"; puts {model}.count; puts "{end_marker}"'
1547+
1548+
# Get tmux target
1549+
target = self.rails_client._get_target() # noqa: SLF001
1550+
tmux = shutil.which("tmux") or "tmux"
1551+
1552+
# Send the command
1553+
escaped_command = self.rails_client._escape_command(query) # noqa: SLF001
1554+
subprocess.run( # noqa: S603
1555+
[tmux, "send-keys", "-t", target, escaped_command, "Enter"],
1556+
capture_output=True,
1557+
text=True,
1558+
check=True,
1559+
)
1560+
1561+
# Wait for the end marker to appear (up to 30 seconds)
1562+
max_wait = 30
1563+
start_time = time.time()
1564+
result = ""
1565+
1566+
while time.time() - start_time < max_wait:
1567+
time.sleep(0.3)
1568+
cap = subprocess.run( # noqa: S603
1569+
[tmux, "capture-pane", "-p", "-S", "-100", "-t", target],
1570+
capture_output=True,
1571+
text=True,
1572+
check=True,
1573+
)
1574+
result = cap.stdout
1575+
if end_marker in result:
1576+
break
1577+
1578+
# Parse output - find lines that exactly match our markers
1579+
lines = result.split("\n")
1580+
start_idx = -1
1581+
end_idx = -1
1582+
1583+
for i, line in enumerate(lines):
1584+
stripped = line.strip()
1585+
# Require exact match (not substring) to avoid matching echoed commands
1586+
if stripped == start_marker:
1587+
start_idx = i
1588+
elif stripped == end_marker and start_idx != -1:
1589+
end_idx = i
1590+
break
1591+
1592+
if start_idx != -1 and end_idx != -1:
1593+
# Extract content between markers
1594+
for line in lines[start_idx + 1 : end_idx]:
1595+
stripped = line.strip()
1596+
if stripped.isdigit():
1597+
return int(stripped)
1598+
1599+
# Fallback: scan for the count in output after our query
1600+
# Look for the last occurrence of the marker pattern followed by a number
1601+
in_our_output = False
1602+
for line in reversed(lines):
1603+
stripped = line.strip()
1604+
if stripped == end_marker:
1605+
in_our_output = True
1606+
elif in_our_output and stripped.isdigit():
1607+
return int(stripped)
1608+
elif stripped == start_marker:
1609+
break
15381610

1539-
if isinstance(result, str) and result.isdigit():
1540-
return int(result)
1541-
msg = "Unable to parse count result."
1611+
msg = f"Unable to parse count result for {model}: end_marker={end_marker in result}"
15421612
raise QueryExecutionError(msg)
15431613

15441614
# -------------------------------------------------------------

0 commit comments

Comments
 (0)