Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/clusterfuzz/_internal/platforms/android/adb.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,3 +915,29 @@ def write_data_to_file(contents, file_path, should_reboot=True):
else:
# Manually revert /system to read-only since we aren't rebooting.
run_shell_command('mount -o ro,remount /system', root=True)


def get_activity_exit_info(app_package: str) -> str:
"""Get dumpsys activity exit-info output for the given application package.
Comment thread
IvanBM18 marked this conversation as resolved.
Example dumpsys output:
package: org.chromium.chrome
Historical Process Exit for uid=10154
ApplicationExitInfo #0:
timestamp=2026-09-03 11:19:03.480 pid=27098 realUid=10154
packageUid=10154 definingUid=10154 user=0
process=org.chromium.chrome reason=2 (SIGNALED)
subreason=0 (UNKNOWN) status=9
importance=100 pss=0.00 rss=0.00 state=empty trace=null
description=null
anrInfo=null


Args:
app_package: Name of the application package.

Returns:
Dumpsys output for the application package.
"""
dumpsys_output = run_shell_command(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how long is the dumpsys activity kept around? And is it written to immediately following a crash?

This approach seems like it would work but it's a bit indirect

@IvanBM18 IvanBM18 Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is persistent in the disk, but it can be deleted using:
adb shell am clear-exit-info [<PACKAGE>]
adb uninstall <app>

['dumpsys', 'activity', 'exit-info', app_package])
return dumpsys_output
5 changes: 5 additions & 0 deletions src/clusterfuzz/_internal/platforms/android/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,8 @@ def log_output(additional_flags=''):
def log_output_before_last_reboot():
"""Return log data from last reboot without noise and some normalization."""
return log_output(additional_flags='-L')


def log_activity_manager_output():
"""Return activity manager log output."""
return adb.run_command(['logcat', '-d', '-s', 'ActivityManager:I'])
169 changes: 169 additions & 0 deletions src/clusterfuzz/_internal/platforms/android/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,26 @@

from dataclasses import dataclass
import os
import re

from clusterfuzz._internal.metrics import logs
from clusterfuzz._internal.platforms import android
from clusterfuzz._internal.system import environment

from . import adb
from . import constants
from . import logger

# Matching: "Start proc <PID>:<PROCESS_NAME>/"
_START_PROC_REGEX = r"Start proc (\d+):(\S+?)/"

# Matching: "reason=<REASON> (<REASON_NAME>) subreason=<SUBREASON>"
# "(<SUBREASON_NAME>) status=<STATUS>"
# e.g.: "reason=5 (APP_CRASH(NATIVE)) subreason=0 (UNKNOWN) status=11" or
# "reason=2 (SIGNALED) subreason=0 (UNKNOWN) status=9"
_REASON_STATUS_REGEX = (r"reason=(\d+)(?:\s*\((.*)\))?\s+subreason=(\d+)"
r"(?:\s*\((.*)\))?\s+status=(\d+)")


@dataclass(frozen=True)
class ProcessExitInfo:
Expand All @@ -36,6 +51,68 @@ class ProcessExitInfo:
status: android.constants.ExitStatus | int


def _to_enum(enum_cls, raw_value: int | str):
"""Converts raw_value to an Enum member, or returns None if invalid."""
try:
return enum_cls(int(raw_value))
except (ValueError, TypeError):
return None
Comment thread
IvanBM18 marked this conversation as resolved.


def _parse_exit_info_from_dumpsys(dumpsys_output: str,
target_pid: int) -> ProcessExitInfo | None:
"""Parses dumpsys activity exit-info output for target_pid.

Args:
dumpsys_output: Output text from `dumpsys activity exit-info`.
target_pid: Process ID to extract exit metadata for.

Returns:
ProcessExitInfo object if metadata for target_pid is found and parsed,
None otherwise.
"""
if not dumpsys_output or target_pid is None:
Comment thread
IvanBM18 marked this conversation as resolved.
return None

current_pid = None
for line in dumpsys_output.splitlines():
pid_match = re.search(r"\bpid=(\d+)", line)
Comment thread
decoNR marked this conversation as resolved.
if pid_match:
current_pid = int(pid_match.group(1))

if current_pid is None or current_pid != target_pid:
continue

reason_match = re.search(_REASON_STATUS_REGEX, line)
if not reason_match:
continue

reason, reason_name, subreason, subreason_name, status = (
reason_match.groups())

parsed_reason = _to_enum(constants.ExitReason, reason)
if parsed_reason is None:
logs.warning(f'[Android] Unexpected process exit reason code {reason} '
f'for PID {target_pid}.')
parsed_reason = constants.ExitReason.UNKNOWN

parsed_status = _to_enum(constants.ExitStatus, status)
if parsed_status is None:
logs.warning(f'[Android] Unexpected process exit status code {status} '
f'for PID {target_pid}.')
parsed_status = int(status)

return ProcessExitInfo(
reason=parsed_reason,
reason_name=reason_name or '',
subreason=int(subreason),
subreason_name=subreason_name or '',
status=parsed_status,
)

return None


def get_device_path(local_path):
"""Returns device path for the given local path."""
root_directory = environment.get_root_directory()
Expand Down Expand Up @@ -106,3 +183,95 @@ def can_testcase_run_on_platform(testcase_platform_id, current_platform_id):
return True

return False


def get_latest_pid_for_package(app_package: str) -> int | None:
"""Gets the latest PID for an application package from logcat.

Args:
app_package: Name of the target application package.

Returns:
PID of the package's latest process if found, None otherwise.
"""
logcat_output = logger.log_activity_manager_output()
if not logcat_output:
logs.info(f'[Android][{app_package}] PID not found, no logcat output')
return None

for line in reversed(logcat_output.splitlines()):
match = re.search(_START_PROC_REGEX, line)
if not match:
continue

pid, process_name = match.groups()
if process_name == app_package or process_name.startswith(
f'{app_package}:'):
return int(pid)
return None


def get_exit_info_for_pid(app_package: str,
target_pid: int) -> ProcessExitInfo | None:
"""Fetches and parses dumpsys activity exit-info output for target_pid.

Args:
app_package: Name of the application package.
target_pid: Process ID to extract exit metadata for.

Returns:
ProcessExitInfo object if metadata for target_pid is found and parsed,
None otherwise.
"""
if target_pid is None:
logs.info(f'[Android][{app_package}] Exit info not found, PID not given')
return None

dumpsys_output = adb.get_activity_exit_info(app_package)
return _parse_exit_info_from_dumpsys(dumpsys_output, target_pid)


def activity_crashed(exit_info: ProcessExitInfo | None) -> bool:
"""Evaluates whether process exit info corresponds to an activity crash.

Args:
exit_info: ProcessExitInfo instance or None.

Returns:
True if exit_info indicates an activity crash, False otherwise.
"""
if not exit_info:
logs.warning('[Android] Exit info empty, not checking for crashes.')
return False

if exit_info.reason in (constants.ExitReason.CRASH_NATIVE,
constants.ExitReason.SIGNALED):
return exit_info.status in (
constants.ExitStatus.SIGSEGV,
constants.ExitStatus.SIGKILL,
constants.ExitStatus.SIGABRT,
constants.ExitStatus.SIGILL,
)
if exit_info.reason == constants.ExitReason.CRASH:
return True
return False


def activity_crashed_by_package(app_package: str) -> bool:
"""Checks whether the latest process for a package crashed.

Args:
app_package: Name of the application package to check.

Returns:
True if the package's latest process crashed, False otherwise.
"""
if not app_package:
logs.warning(f'[Android][{app_package}] App package not given, '
'not checking for crashes.')
return False
Comment thread
IvanBM18 marked this conversation as resolved.

pid = get_latest_pid_for_package(app_package)

exit_info = get_exit_info_for_pid(app_package, pid)
return activity_crashed(exit_info)
Loading
Loading