Skip to content

Commit 4a32e2f

Browse files
committed
♻️ Refactor bug report handling for better code organization
Extract bug report functionality into dedicated helper methods - Create _analyze_temp_bugreport helper to summarize bug reports - Add _execute_bugreport_core for common execution logic - Implement _execute_bugreport_temp for temporary file handling - Improve error handling and process termination - Use context manager for automatic temp file cleanup - Enhance output messages with clearer file information
1 parent 5b4c35b commit 4a32e2f

File tree

1 file changed

+120
-104
lines changed

1 file changed

+120
-104
lines changed

droidmind/tools/diagnostics.py

Lines changed: 120 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,129 @@ class DiagAction(Enum):
2424
DUMP_HEAP = "dump_heap"
2525

2626

27+
async def _analyze_temp_bugreport(serial: str, ctx: Context, actual_output_path: str, file_size_mb: float) -> str:
28+
"""Analyzes a temporary bug report file and returns a summary."""
29+
await ctx.info("Analyzing bug report contents (summary)...")
30+
# Using a raw string for the unzip command pattern to avoid issues with backslashes
31+
zip_list_cmd = f'unzip -l "{actual_output_path}"'
32+
proc = await asyncio.create_subprocess_shell(
33+
zip_list_cmd,
34+
stdout=asyncio.subprocess.PIPE,
35+
stderr=asyncio.subprocess.PIPE,
36+
)
37+
zip_stdout, zip_stderr = await proc.communicate()
38+
39+
if proc.returncode != 0:
40+
await ctx.warning(f"Could not list contents of temporary bugreport zip: {zip_stderr.decode().strip()}")
41+
zip_contents_summary = "(Could not retrieve zip contents)"
42+
else:
43+
zip_contents_summary = zip_stdout.decode().strip()
44+
45+
summary = [
46+
f"# Bug Report for {serial}",
47+
f"Bug report ({file_size_mb:.2f} MB) was processed from a temporary location.",
48+
f"Original temporary path: `{actual_output_path}`",
49+
"",
50+
"## Bug Report Contents (Summary)",
51+
"```",
52+
zip_contents_summary[:2000] + ("..." if len(zip_contents_summary) > 2000 else ""),
53+
"```",
54+
"",
55+
"Note: The temporary file has been cleaned up. If you need to keep the bug report, specify an 'output_path'.",
56+
]
57+
return "\\n".join(summary)
58+
59+
60+
async def _execute_bugreport_core(
61+
device: Device,
62+
serial: str,
63+
ctx: Context,
64+
bugreport_cmd_arg: str,
65+
actual_output_path: str,
66+
timeout_seconds: int,
67+
is_temporary: bool,
68+
) -> str:
69+
"""Core logic for executing adb bugreport, handling results, and optionally summarizing."""
70+
await ctx.info(f"Running command: adb -s {serial} {bugreport_cmd_arg} {actual_output_path}")
71+
process = None # Define process to ensure it's available in except block
72+
try:
73+
process = await asyncio.create_subprocess_exec(
74+
"adb",
75+
"-s",
76+
serial,
77+
bugreport_cmd_arg,
78+
actual_output_path,
79+
stdout=asyncio.subprocess.PIPE,
80+
stderr=asyncio.subprocess.PIPE,
81+
)
82+
83+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout_seconds)
84+
stderr_str = stderr.decode().strip() if stderr else ""
85+
86+
if process.returncode != 0:
87+
await ctx.error(f"Bug report failed with exit code {process.returncode}")
88+
await ctx.error(f"Error: {stderr_str}")
89+
return f"Failed to capture bug report: {stderr_str}"
90+
91+
if not os.path.exists(actual_output_path):
92+
await ctx.error("Bug report file was not created")
93+
return "Failed to capture bug report: No output file was created."
94+
95+
file_size = os.path.getsize(actual_output_path)
96+
file_size_mb = file_size / (1024 * 1024)
97+
await ctx.info(f"✅ Bug report captured successfully! File size: {file_size_mb:.2f} MB")
98+
99+
if not is_temporary:
100+
return f"Bug report saved to: {actual_output_path} ({file_size_mb:.2f} MB)"
101+
102+
# Analyze and summarize the temporary bug report
103+
return await _analyze_temp_bugreport(serial, ctx, actual_output_path, file_size_mb)
104+
105+
except TimeoutError:
106+
if process and process.returncode is None: # Process is still running
107+
try:
108+
process.terminate()
109+
await asyncio.wait_for(process.wait(), timeout=5)
110+
except TimeoutError:
111+
process.kill()
112+
except ProcessLookupError: # pragma: no cover
113+
pass # Process already ended
114+
except Exception: # pragma: no cover
115+
logger.exception("Error terminating bugreport process during timeout.")
116+
await ctx.error(f"Bug report timed out after {timeout_seconds} seconds")
117+
return f"Bug report capture timed out after {timeout_seconds} seconds. Try again with a longer timeout value."
118+
119+
120+
async def _execute_bugreport_temp(
121+
device: Device, serial: str, ctx: Context, bugreport_cmd_arg: str, timeout_seconds: int
122+
) -> str:
123+
"""Handles bug report capture to a temporary directory and summarizes."""
124+
timestamp = (await device.run_shell("date +%Y%m%d_%H%M%S")).strip()
125+
with tempfile.TemporaryDirectory(prefix="droidmind_bugreport_") as temp_dir_name:
126+
filename = f"bugreport_{serial}_{timestamp}.zip"
127+
actual_output_path = os.path.join(temp_dir_name, filename)
128+
await ctx.info(
129+
f"No output path specified; bug report will be summarized from temporary file: {actual_output_path}"
130+
)
131+
132+
return await _execute_bugreport_core(
133+
device, serial, ctx, bugreport_cmd_arg, actual_output_path, timeout_seconds, is_temporary=True
134+
)
135+
# temp_dir_name is cleaned up automatically by 'with' statement
136+
137+
27138
async def _capture_bugreport_impl(
28139
serial: str, ctx: Context, output_path: str = "", include_screenshots: bool = True, timeout_seconds: int = 300
29140
) -> str:
30141
"""Implementation for capturing a bug report from a device."""
31142
try:
32-
# Get the device
33143
device = await get_device_manager().get_device(serial)
34144
if not device:
35145
await ctx.error(f"Device {serial} not connected or not found.")
36146
return f"Error: Device {serial} not found."
37147

38-
# Log the operation with medium risk level
39148
log_command_execution("capture_bugreport", RiskLevel.MEDIUM)
40149

41-
# Inform the user
42150
await ctx.info(f"🔍 Capturing bug report from device {serial}...")
43151
await ctx.info("This may take a few minutes depending on the device's state.")
44152

@@ -49,110 +157,18 @@ async def _capture_bugreport_impl(
49157
await ctx.info("Excluding screenshots to reduce bug report size.")
50158
bugreport_cmd_arg = "bugreportz"
51159

52-
# Create a temporary directory if no output path is specified
53-
temp_dir_obj = None
54-
actual_output_path = output_path
55-
56-
if not actual_output_path:
57-
temp_dir_obj = tempfile.TemporaryDirectory(prefix="droidmind_bugreport_")
58-
actual_output_path = os.path.join(
59-
temp_dir_obj.name, f"bugreport_{serial}_{await device.run_shell('date +%Y%m%d_%H%M%S')}.zip"
60-
)
61-
await ctx.info(f"No output path specified, saving to temporary file: {actual_output_path}")
62-
else:
63-
# Ensure the output directory exists if a path is provided
64-
os.makedirs(os.path.dirname(os.path.abspath(actual_output_path)), exist_ok=True)
65-
66-
# Run the bugreport command and save to the specified path
67-
await ctx.info(f"Running command: adb -s {serial} {bugreport_cmd_arg} {actual_output_path}")
160+
if not output_path:
161+
# Scenario: Temporary file. This will use 'with' inside the helper.
162+
return await _execute_bugreport_temp(device, serial, ctx, bugreport_cmd_arg, timeout_seconds)
68163

69-
process = await asyncio.create_subprocess_exec(
70-
"adb",
71-
"-s",
72-
serial,
73-
bugreport_cmd_arg,
74-
actual_output_path,
75-
stdout=asyncio.subprocess.PIPE,
76-
stderr=asyncio.subprocess.PIPE,
164+
# Scenario: User-specified file.
165+
# Ensure the output directory exists if a path is provided
166+
abs_output_path = os.path.abspath(output_path)
167+
os.makedirs(os.path.dirname(abs_output_path), exist_ok=True)
168+
return await _execute_bugreport_core(
169+
device, serial, ctx, bugreport_cmd_arg, abs_output_path, timeout_seconds, is_temporary=False
77170
)
78171

79-
# Set up a timeout
80-
try:
81-
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout_seconds)
82-
stderr_str = stderr.decode().strip() if stderr else ""
83-
84-
if process.returncode != 0:
85-
await ctx.error(f"Bug report failed with exit code {process.returncode}")
86-
await ctx.error(f"Error: {stderr_str}")
87-
if temp_dir_obj:
88-
temp_dir_obj.cleanup()
89-
return f"Failed to capture bug report: {stderr_str}"
90-
91-
if not os.path.exists(actual_output_path):
92-
await ctx.error("Bug report file was not created")
93-
if temp_dir_obj:
94-
temp_dir_obj.cleanup()
95-
return "Failed to capture bug report: No output file was created."
96-
97-
file_size = os.path.getsize(actual_output_path)
98-
file_size_mb = file_size / (1024 * 1024)
99-
await ctx.info(f"✅ Bug report captured successfully! File size: {file_size_mb:.2f} MB")
100-
101-
if not temp_dir_obj:
102-
return f"Bug report saved to: {actual_output_path} ({file_size_mb:.2f} MB)"
103-
104-
# If we created a temp file, analyze the bug report and return a summary
105-
await ctx.info("Analyzing bug report contents (summary)...")
106-
zip_list_cmd = f'unzip -l \\"{actual_output_path}\\"'
107-
proc = await asyncio.create_subprocess_shell(
108-
zip_list_cmd,
109-
stdout=asyncio.subprocess.PIPE,
110-
stderr=asyncio.subprocess.PIPE,
111-
)
112-
zip_stdout, _ = await proc.communicate()
113-
zip_contents_summary = zip_stdout.decode().strip()
114-
115-
summary = [
116-
f"# Bug Report for {serial}",
117-
f"Temporary file saved to: `{actual_output_path}` ({file_size_mb:.2f} MB)",
118-
"",
119-
"## Bug Report Contents (Summary)",
120-
"```",
121-
zip_contents_summary[:2000] + ("..." if len(zip_contents_summary) > 2000 else ""),
122-
"```",
123-
"",
124-
"To extract specific information from this bug report, you can use:",
125-
f'* `unzip -o \\"{actual_output_path}\\" -d <extract_dir>` to extract all files',
126-
(
127-
f'* `unzip -o \\"{actual_output_path}\\" bugreport-*.txt -d <extract_dir>` '
128-
"to extract just the main report"
129-
),
130-
"",
131-
"Note: This is a temporary file and will be cleaned up.",
132-
]
133-
final_summary = "\\n".join(summary)
134-
temp_dir_obj.cleanup()
135-
return final_summary
136-
137-
except TimeoutError:
138-
if process and process.returncode is None:
139-
try:
140-
process.terminate()
141-
await asyncio.wait_for(process.wait(), timeout=5)
142-
except TimeoutError:
143-
process.kill()
144-
except ProcessLookupError:
145-
pass
146-
await ctx.error(f"Bug report timed out after {timeout_seconds} seconds")
147-
if temp_dir_obj:
148-
temp_dir_obj.cleanup()
149-
return (
150-
f"Bug report capture timed out after {timeout_seconds} seconds. Try again with a longer timeout value."
151-
)
152-
finally:
153-
if temp_dir_obj and os.path.exists(temp_dir_obj.name):
154-
temp_dir_obj.cleanup()
155-
156172
except Exception as e:
157173
logger.exception("Error capturing bug report: %s", e)
158174
await ctx.error(f"Error capturing bug report: {e}")

0 commit comments

Comments
 (0)