From f7f348d69eb5524911bbacbb3ec620786b82f881 Mon Sep 17 00:00:00 2001 From: Nitin Nakka Date: Thu, 5 Feb 2026 19:19:01 +0530 Subject: [PATCH 01/10] SuspendResume validation test Signed-off-by: Nitin Nakka --- .../Kernel/Baseport/SuspendResume/README.md | 241 +++++++++++++++ .../Baseport/SuspendResume/SuspendResume.yaml | 15 + .../Kernel/Baseport/SuspendResume/run.sh | 278 ++++++++++++++++++ 3 files changed, 534 insertions(+) create mode 100644 Runner/suites/Kernel/Baseport/SuspendResume/README.md create mode 100644 Runner/suites/Kernel/Baseport/SuspendResume/SuspendResume.yaml create mode 100644 Runner/suites/Kernel/Baseport/SuspendResume/run.sh diff --git a/Runner/suites/Kernel/Baseport/SuspendResume/README.md b/Runner/suites/Kernel/Baseport/SuspendResume/README.md new file mode 100644 index 00000000..8362eb54 --- /dev/null +++ b/Runner/suites/Kernel/Baseport/SuspendResume/README.md @@ -0,0 +1,241 @@ +# SuspendResume Validation Test +Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +SPDX-License-Identifier: BSD-3-Clause-Clear + +## Overview +This test case validates the system suspend/resume functionality on the target device using ADB-based remote control. It triggers a suspend cycle, waits for the device to resume, and validates the suspend/resume operation through multiple checks including suspend statistics, kernel logs, and Qualcomm-specific power management statistics. + +## Test Performs: +1. Connects to device via ADB and obtains root access +2. Remounts filesystems as read-write +3. Mounts debugfs for accessing kernel statistics +4. Captures initial suspend count from `/sys/power/suspend_stats/success` +5. Triggers suspend using `rtcwake` (30-second timer) and `systemctl suspend` +6. Waits for device to resume (40-second timeout) +7. Validates suspend/resume cycle through three checks: + - **Check 1**: Verifies suspend count incremented + - **Check 2**: Verifies suspend entry markers in dmesg + - **Check 3**: Verifies resume markers in dmesg +8. Collects comprehensive debug statistics: + - Kernel suspend statistics + - Qualcomm power management statistics (AOSD, ADSP, CDSP, DDR, CXSD) + - Complete qcom_stats and suspend_stats dumps + +## Usage +Instructions: +1. **Copy repo to Host Machine**: Clone or download the repository to your host machine where ADB is installed. +2. **Connect Device**: Ensure the target device is connected via ADB and visible with `adb devices`. +3. **Run Test**: Execute the test script which will remotely control the device via ADB. + +Run the SuspendResume test using: +--- + +#### Quick Example +```sh +git clone +cd + +# Ensure device is connected +adb devices + +# Run the test +cd Runner/suites/Kernel/Baseport/SuspendResume +./run.sh +``` + +--- + +## Prerequisites +1. **ADB**: The `adb` command must be available on the host machine +2. **Single Device Connection**: Exactly one target device must be connected via ADB + - The test will automatically detect and validate device count + - Multiple devices will cause the test to skip with an error message +3. **Root Access**: Device must allow `adb root` for root access +4. **Kernel Support**: Device must support: + - `rtcwake` command for RTC-based wakeup + - `systemctl suspend` for triggering suspend + - Suspend statistics in `/sys/power/suspend_stats/` + - Debugfs support for accessing kernel debug information +5. **RTC Device**: `/dev/rtc0` must be present and functional +6. **Framework Files**: `init_env` and `functestlib.sh` must be present and correctly configured + +## Configuration +The test uses the following default configuration (can be modified in run.sh): +```sh +SUSPEND_DURATION=30 # seconds to suspend +WAIT_TIMEOUT=40 # seconds to wait for device to resume +``` + +## Result Format +Test result will be saved in `SuspendResume.res` as: + +## Output +A .res file is generated in the same directory: +- `SuspendResume PASS` - All validation checks passed +- `SuspendResume FAIL` - One or more validation checks failed +- `SuspendResume SKIP` - ADB not available or prerequisites not met + +## Validation Checks + +### Check 1: Suspend Count Increment +Verifies that `/sys/power/suspend_stats/success` incremented after suspend/resume cycle. + +### Check 2: Suspend Entry Markers +Searches dmesg for suspend entry indicators: +- `PM: suspend entry` +- `Freezing user space processes` + +### Check 3: Resume Markers +Searches dmesg for resume indicators: +- `PM: suspend exit` +- `Restarting tasks` + +## Debug Statistics Collected + +The test collects comprehensive power management statistics: + +### Kernel Suspend Stats +- `/sys/kernel/debug/suspend_stats` - Overall suspend statistics + +### Qualcomm Power Stats +- `/sys/kernel/debug/qcom_stats/aosd` - Always-On Subsystem Domain stats +- `/sys/kernel/debug/qcom_stats/adsp` - Audio DSP stats +- `/sys/kernel/debug/qcom_stats/adsp_island` - ADSP Island mode stats +- `/sys/kernel/debug/qcom_stats/cdsp` - Compute DSP stats +- `/sys/kernel/debug/qcom_stats/ddr` - DDR stats +- `/sys/kernel/debug/qcom_stats/cxsd` - CX Subsystem Domain stats + +### Complete Dumps +- All entries in `/sys/kernel/debug/qcom_stats/` +- All entries in `/sys/power/suspend_stats/` + +## Skip Criteria +The test will be skipped if: +1. `adb` command is not found on the host machine +2. No ADB devices are connected +3. Multiple ADB devices are connected (only one device is allowed) +4. Device is not responding to ADB commands + +## Failure Criteria +The test will fail if: +1. Device does not resume within the timeout period (40 seconds) +2. Suspend count does not increment +3. Suspend entry markers are not found in kernel log +4. Resume markers are not found in kernel log + +## Sample Log - Success +``` +[INFO] 2026-02-01 20:30:00 - ----------------------------------------------------------------------------------------- +[INFO] 2026-02-01 20:30:00 - -------------------Starting SuspendResume Testcase (ADB-based)---------------------------- +[INFO] 2026-02-01 20:30:00 - === Test Initialization === +[INFO] 2026-02-01 20:30:00 - Checking for connected ADB devices... +[INFO] 2026-02-01 20:30:01 - Detected 1 device(s) +[INFO] 2026-02-01 20:30:01 - Single device detected - proceeding with test +[INFO] 2026-02-01 20:30:01 - Waiting for device to be ready... +[INFO] 2026-02-01 20:30:02 - Obtaining root access... +[INFO] 2026-02-01 20:30:04 - Remounting filesystems as read-write... +[INFO] 2026-02-01 20:30:05 - Mounting debugfs... +[INFO] 2026-02-01 20:30:06 - Capturing pre-suspend state... +[INFO] 2026-02-01 20:30:06 - Initial suspend count: 5 +[INFO] 2026-02-01 20:30:06 - Triggering suspend for 30 seconds... +[INFO] 2026-02-01 20:30:06 - Command: rtcwake -d /dev/rtc0 -m no -s 30 && systemctl suspend +[INFO] 2026-02-01 20:30:11 - Waiting for device to resume (timeout: 40s)... +[PASS] 2026-02-01 20:30:38 - Device resumed successfully +[INFO] 2026-02-01 20:30:41 - Post-resume phase: Validating suspend/resume cycle +[INFO] 2026-02-01 20:30:42 - Current suspend count: 6 +[PASS] 2026-02-01 20:30:42 - Validation 1 PASSED: Suspend count increased from 5 to 6 +[INFO] 2026-02-01 20:30:42 - Checking for suspend entry markers in kernel log... +[PASS] 2026-02-01 20:30:43 - Validation 2 PASSED: Suspend entry markers found +[INFO] 2026-02-01 20:30:43 - Checking for resume markers in kernel log... +[PASS] 2026-02-01 20:30:44 - Validation 3 PASSED: Resume markers found +[INFO] 2026-02-01 20:30:44 - Collecting debug statistics... +[PASS] 2026-02-01 20:30:50 - SuspendResume : Test Passed - Suspend/Resume cycle completed successfully +``` + +## Sample Log - Failure (Device Not Resuming) +``` +[INFO] 2026-02-01 20:30:00 - ----------------------------------------------------------------------------------------- +[INFO] 2026-02-01 20:30:00 - -------------------Starting SuspendResume Testcase (ADB-based)---------------------------- +[INFO] 2026-02-01 20:30:00 - === Test Initialization === +[INFO] 2026-02-01 20:30:00 - Checking for connected ADB devices... +[INFO] 2026-02-01 20:30:01 - Detected 1 device(s) +[INFO] 2026-02-01 20:30:01 - Single device detected - proceeding with test +[INFO] 2026-02-01 20:30:01 - Waiting for device to be ready... +[INFO] 2026-02-01 20:30:02 - Obtaining root access... +[INFO] 2026-02-01 20:30:04 - Remounting filesystems as read-write... +[INFO] 2026-02-01 20:30:05 - Mounting debugfs... +[INFO] 2026-02-01 20:30:06 - Capturing pre-suspend state... +[INFO] 2026-02-01 20:30:06 - Initial suspend count: 5 +[INFO] 2026-02-01 20:30:06 - Triggering suspend for 30 seconds... +[INFO] 2026-02-01 20:30:06 - Command: rtcwake -d /dev/rtc0 -m no -s 30 && systemctl suspend +[INFO] 2026-02-01 20:30:11 - Waiting for device to resume (timeout: 40s)... +[INFO] 2026-02-01 20:30:16 - Still waiting... (10s elapsed) +[INFO] 2026-02-01 20:30:26 - Still waiting... (20s elapsed) +[INFO] 2026-02-01 20:30:36 - Still waiting... (30s elapsed) +[INFO] 2026-02-01 20:30:46 - Still waiting... (40s elapsed) +[FAIL] 2026-02-01 20:30:51 - SuspendResume : Device did not resume within 40s timeout +``` + +## Sample Log - Skip (Multiple Devices) +``` +[INFO] 2026-02-01 20:30:00 - ----------------------------------------------------------------------------------------- +[INFO] 2026-02-01 20:30:00 - -------------------Starting SuspendResume Testcase (ADB-based)---------------------------- +[INFO] 2026-02-01 20:30:00 - === Test Initialization === +[INFO] 2026-02-01 20:30:00 - Checking for connected ADB devices... +[INFO] 2026-02-01 20:30:01 - Detected 2 device(s) +[FAIL] 2026-02-01 20:30:01 - Multiple ADB devices connected (2 devices) - please connect only one device +[INFO] 2026-02-01 20:30:01 - Connected devices: +List of devices attached +ABC123456789 device +DEF987654321 device +``` + +## Sample Log - Skip (No Devices) +``` +[INFO] 2026-02-01 20:30:00 - ----------------------------------------------------------------------------------------- +[INFO] 2026-02-01 20:30:00 - -------------------Starting SuspendResume Testcase (ADB-based)---------------------------- +[INFO] 2026-02-01 20:30:00 - === Test Initialization === +[INFO] 2026-02-01 20:30:00 - Checking for connected ADB devices... +[INFO] 2026-02-01 20:30:01 - Detected 0 device(s) +[FAIL] 2026-02-01 20:30:01 - No ADB devices connected - please connect a device +``` + +## Integration with LAVA +This test is designed to work with LAVA's ADB support framework. The YAML configuration file (`SuspendResume.yaml`) defines the test metadata and execution steps for LAVA integration. + +## Troubleshooting + +### Device Not Resuming +- Check if RTC device (`/dev/rtc0`) is functional +- Verify `rtcwake` command works manually +- Check if suspend is supported: `cat /sys/power/state` +- Increase `WAIT_TIMEOUT` if device takes longer to resume + +### ADB Connection Issues +- Verify device is visible: `adb devices` +- Ensure only one device is connected (disconnect other devices if multiple are shown) +- Try `adb kill-server && adb start-server` +- Check USB connection and drivers + +### Multiple Devices Connected +- The test requires exactly one device to be connected +- Disconnect all but one device before running the test +- Use `adb devices` to verify only one device is listed + +### Permission Issues +- Ensure `adb root` works on your device +- Some devices may require unlocked bootloader for root access + +### Missing Statistics +- Verify debugfs is mounted: `adb shell mount | grep debugfs` +- Check if qcom_stats are available: `adb shell ls /sys/kernel/debug/qcom_stats/` + +## Notes +- The test uses a 30-second suspend duration by default +- A 40-second timeout is used to wait for device resume +- All commands are executed remotely via ADB +- The test is non-destructive and safe to run multiple times +- Debug statistics collection is best-effort and won't fail the test if unavailable + +## License +SPDX-License-Identifier: BSD-3-Clause-Clear diff --git a/Runner/suites/Kernel/Baseport/SuspendResume/SuspendResume.yaml b/Runner/suites/Kernel/Baseport/SuspendResume/SuspendResume.yaml new file mode 100644 index 00000000..caa36d75 --- /dev/null +++ b/Runner/suites/Kernel/Baseport/SuspendResume/SuspendResume.yaml @@ -0,0 +1,15 @@ +metadata: + name: SuspendResume + format: "Lava-Test Test Definition 1.0" + description: "Suspend/Resume validation with kernel debug stats collection" + os: + - linux + scope: + - functional + +run: + steps: + - REPO_PATH=$PWD + - cd Runner/suites/Kernel/Baseport/SuspendResume + - ./run.sh || true + - $REPO_PATH/Runner/utils/send-to-lava.sh SuspendResume.res || true diff --git a/Runner/suites/Kernel/Baseport/SuspendResume/run.sh b/Runner/suites/Kernel/Baseport/SuspendResume/run.sh new file mode 100644 index 00000000..3dd01ef2 --- /dev/null +++ b/Runner/suites/Kernel/Baseport/SuspendResume/run.sh @@ -0,0 +1,278 @@ +#!/bin/sh + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause-Clear + +# Robustly find and source init_env +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +INIT_ENV="" +SEARCH="$SCRIPT_DIR" +while [ "$SEARCH" != "/" ]; do + if [ -f "$SEARCH/init_env" ]; then + INIT_ENV="$SEARCH/init_env" + break + fi + SEARCH=$(dirname "$SEARCH") +done + +if [ -z "$INIT_ENV" ]; then + echo "[ERROR] Could not find init_env (starting at $SCRIPT_DIR)" >&2 + exit 1 +fi + +# Only source if not already loaded (idempotent) +if [ -z "$__INIT_ENV_LOADED" ]; then + # shellcheck disable=SC1090 + . "$INIT_ENV" +fi +# Always source functestlib.sh, using $TOOLS exported by init_env +# shellcheck disable=SC1090,SC1091 +. "$TOOLS/functestlib.sh" + +TESTNAME="SuspendResume" +test_path=$(find_test_case_by_name "$TESTNAME") +cd "$test_path" || exit 1 +# shellcheck disable=SC2034 +res_file="./$TESTNAME.res" + +log_info "-----------------------------------------------------------------------------------------" +log_info "-------------------Starting $TESTNAME Testcase (ADB-based)----------------------------" +log_info "=== Test Initialization ===" + +# ============================================================================ +# ADB-BASED SUSPEND/RESUME TEST +# ============================================================================ +# This version uses adb commands to control the device remotely, which is +# compatible with LAVA's upcoming adb support framework. +# ============================================================================ + +# Configuration +SUSPEND_DURATION=30 # seconds to suspend +WAIT_TIMEOUT=40 # seconds to wait for device to resume + +# Check if adb is available +if ! command -v adb >/dev/null 2>&1; then + log_fail "adb command not found - this test requires adb to be installed" + echo "$TESTNAME SKIP" > "$res_file" + exit 0 +fi + +# Check that exactly one device is connected +log_info "Checking for connected ADB devices..." + +# Count devices using grep (handles Windows line endings better than awk) +DEVICE_COUNT=$(adb devices 2>/dev/null | grep -v "List of devices" | grep "device" | wc -l | tr -d ' \t\r\n') + +# Ensure DEVICE_COUNT is a valid integer +if [ -z "$DEVICE_COUNT" ]; then + DEVICE_COUNT=0 +fi + +log_info "Detected $DEVICE_COUNT device(s)" + +if [ "$DEVICE_COUNT" -eq 0 ]; then + log_fail "No ADB devices connected - please connect a device" + echo "$TESTNAME SKIP" > "$res_file" + exit 0 +elif [ "$DEVICE_COUNT" -gt 1 ]; then + log_fail "Multiple ADB devices connected ($DEVICE_COUNT devices) - please connect only one device" + log_info "Connected devices:" + adb devices + echo "$TESTNAME SKIP" > "$res_file" + exit 0 +fi + +log_info "Single device detected - proceeding with test" +log_info "Waiting for device to be ready..." +adb wait-for-device + +# Get root access +log_info "Obtaining root access..." +adb root +sleep 2 +adb wait-for-device + +# Remount filesystems as read-write +log_info "Remounting filesystems as read-write..." +adb shell "mount -o remount,rw /" 2>/dev/null || true +adb shell "mount -o remount,rw /usr" 2>/dev/null || true + +# Mount debugfs +log_info "Mounting debugfs..." +adb shell "mount -t debugfs none /sys/kernel/debug" 2>/dev/null || true + +# ============================================================================ +# PRE-SUSPEND: Capture initial state +# ============================================================================ + +log_info "Capturing pre-suspend state..." + +# Get initial suspend count +INITIAL_SUSPEND_COUNT=$(adb shell "cat /sys/power/suspend_stats/success 2>/dev/null" | tr -d '\r\n' || echo "0") +log_info "Initial suspend count: $INITIAL_SUSPEND_COUNT" + +# Verify suspend stats are accessible +if [ -z "$INITIAL_SUSPEND_COUNT" ] || [ "$INITIAL_SUSPEND_COUNT" = "0" ]; then + log_warn "Suspend stats may not be available or this is first suspend" +fi + +# ============================================================================ +# TRIGGER SUSPEND +# ============================================================================ + +log_info "Triggering suspend for $SUSPEND_DURATION seconds..." +log_info "Command: rtcwake -d /dev/rtc0 -m no -s $SUSPEND_DURATION && systemctl suspend" + +# Execute suspend command (this will disconnect adb) +adb shell "rtcwake -d /dev/rtc0 -m no -s $SUSPEND_DURATION && systemctl suspend" & +SUSPEND_PID=$! + +# Give the suspend command time to execute +sleep 5 + +# ============================================================================ +# WAIT FOR RESUME +# ============================================================================ + +log_info "Waiting for device to resume (timeout: ${WAIT_TIMEOUT}s)..." + +# Wait for device to come back online using counter-based timeout +WAIT_COUNT=0 +DEVICE_RESUMED=0 + +while [ $WAIT_COUNT -lt $WAIT_TIMEOUT ]; do + # Try to check if device is responsive (non-blocking check) + if adb shell "echo test" >/dev/null 2>&1; then + DEVICE_RESUMED=1 + log_pass "Device resumed successfully after ${WAIT_COUNT}s" + break + fi + sleep 1 + WAIT_COUNT=$((WAIT_COUNT + 1)) + + # Log progress every 10 seconds + if [ $((WAIT_COUNT % 10)) -eq 0 ]; then + log_info "Still waiting... (${WAIT_COUNT}s elapsed)" + fi +done + +# Clean up background process +kill $SUSPEND_PID 2>/dev/null || true +wait $SUSPEND_PID 2>/dev/null || true + +if [ $DEVICE_RESUMED -eq 0 ]; then + log_fail "$TESTNAME : Device did not resume within ${WAIT_TIMEOUT}s timeout" + echo "$TESTNAME FAIL" > "$res_file" + exit 1 +fi + +# Give system a moment to stabilize after resume +sleep 3 + +# ============================================================================ +# POST-RESUME: Validate suspend/resume cycle +# ============================================================================ + +log_info "Post-resume phase: Validating suspend/resume cycle" + +# Remount debugfs again (may have been unmounted during suspend) +adb shell "mount -t debugfs none /sys/kernel/debug" 2>/dev/null || true + +# Get current suspend count +CURRENT_SUSPEND_COUNT=$(adb shell "cat /sys/power/suspend_stats/success 2>/dev/null" | tr -d '\r\n' || echo "0") +log_info "Current suspend count: $CURRENT_SUSPEND_COUNT" + +# ============================================================================ +# VALIDATION CHECKS +# ============================================================================ + +VALIDATION_PASSED=1 + +# Validation Check 1: Verify suspend count incremented +if [ "$CURRENT_SUSPEND_COUNT" -le "$INITIAL_SUSPEND_COUNT" ]; then + log_fail "Validation 1 FAILED: Suspend count did not increase (expected > $INITIAL_SUSPEND_COUNT, got $CURRENT_SUSPEND_COUNT)" + VALIDATION_PASSED=0 +else + log_pass "Validation 1 PASSED: Suspend count increased from $INITIAL_SUSPEND_COUNT to $CURRENT_SUSPEND_COUNT" +fi + +# Validation Check 2: Verify suspend entry markers in dmesg +log_info "Checking for suspend entry markers in kernel log..." +SUSPEND_ENTRY=$(adb shell "dmesg | grep -E 'PM: suspend entry|Freezing user space processes'" | tail -5) +if [ -z "$SUSPEND_ENTRY" ]; then + log_fail "Validation 2 FAILED: Suspend entry markers not found in kernel log" + VALIDATION_PASSED=0 +else + log_pass "Validation 2 PASSED: Suspend entry markers found" + echo "$SUSPEND_ENTRY" | while IFS= read -r line; do + log_info " $line" + done +fi + +# Validation Check 3: Verify resume markers in dmesg +log_info "Checking for resume markers in kernel log..." +RESUME_MARKERS=$(adb shell "dmesg | grep -E 'PM: suspend exit|Restarting tasks'" | tail -5) +if [ -z "$RESUME_MARKERS" ]; then + log_fail "Validation 3 FAILED: Resume markers not found in kernel log" + VALIDATION_PASSED=0 +else + log_pass "Validation 3 PASSED: Resume markers found" + echo "$RESUME_MARKERS" | while IFS= read -r line; do + log_info " $line" + done +fi + +# ============================================================================ +# DEBUG INFO COLLECTION +# ============================================================================ + +log_info "Collecting debug statistics..." + +# Collect kernel suspend statistics +log_info "=== Suspend Stats ===" +adb shell "cat /sys/kernel/debug/suspend_stats" 2>/dev/null || log_warn "Could not read suspend_stats" + +# Collect Qualcomm-specific power statistics +log_info "=== Qualcomm Power Stats ===" + +log_info "--- AOSD Stats ---" +adb shell "cat /sys/kernel/debug/qcom_stats/aosd" 2>/dev/null || log_warn "Could not read aosd stats" + +log_info "--- ADSP Stats ---" +adb shell "cat /sys/kernel/debug/qcom_stats/adsp" 2>/dev/null || log_warn "Could not read adsp stats" + +log_info "--- ADSP Island Stats ---" +adb shell "cat /sys/kernel/debug/qcom_stats/adsp_island" 2>/dev/null || log_warn "Could not read adsp_island stats" + +log_info "--- CDSP Stats ---" +adb shell "cat /sys/kernel/debug/qcom_stats/cdsp" 2>/dev/null || log_warn "Could not read cdsp stats" + +log_info "--- DDR Stats ---" +adb shell "cat /sys/kernel/debug/qcom_stats/ddr" 2>/dev/null || log_warn "Could not read ddr stats" + +log_info "--- CXSD Stats ---" +adb shell "cat /sys/kernel/debug/qcom_stats/cxsd" 2>/dev/null || log_warn "Could not read cxsd stats" + +# Dump all qcom_stats entries +log_info "=== All Qcom Stats (grep) ===" +adb shell "cd /sys/kernel/debug/qcom_stats && grep -r . 2>/dev/null" || log_warn "Could not grep qcom_stats" + +# Dump all suspend_stats entries +log_info "=== All Suspend Stats (grep) ===" +adb shell "cd /sys/power/suspend_stats && grep -r . 2>/dev/null" || log_warn "Could not grep suspend_stats" + +# ============================================================================ +# FINAL RESULT +# ============================================================================ + +if [ $VALIDATION_PASSED -eq 1 ]; then + log_pass "$TESTNAME : Test Passed - Suspend/Resume cycle completed successfully" + echo "$TESTNAME PASS" > "$res_file" + exit 0 +else + log_fail "$TESTNAME : Test Failed - One or more validation checks failed" + echo "$TESTNAME FAIL" > "$res_file" + exit 1 +fi + +log_info "-------------------Completed $TESTNAME Testcase----------------------------" From b88bdbc92ecf185e8b7b3f366e4dff08316890a2 Mon Sep 17 00:00:00 2001 From: Nitin Nakka Date: Fri, 6 Feb 2026 19:33:15 +0530 Subject: [PATCH 02/10] Move SuspendResume suite to host-tools/SuspendResume Signed-off-by: Nitin Nakka --- .../suites/Kernel/Baseport => host-tools}/SuspendResume/README.md | 0 .../Baseport => host-tools}/SuspendResume/SuspendResume.yaml | 0 .../suites/Kernel/Baseport => host-tools}/SuspendResume/run.sh | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {Runner/suites/Kernel/Baseport => host-tools}/SuspendResume/README.md (100%) rename {Runner/suites/Kernel/Baseport => host-tools}/SuspendResume/SuspendResume.yaml (100%) rename {Runner/suites/Kernel/Baseport => host-tools}/SuspendResume/run.sh (100%) diff --git a/Runner/suites/Kernel/Baseport/SuspendResume/README.md b/host-tools/SuspendResume/README.md similarity index 100% rename from Runner/suites/Kernel/Baseport/SuspendResume/README.md rename to host-tools/SuspendResume/README.md diff --git a/Runner/suites/Kernel/Baseport/SuspendResume/SuspendResume.yaml b/host-tools/SuspendResume/SuspendResume.yaml similarity index 100% rename from Runner/suites/Kernel/Baseport/SuspendResume/SuspendResume.yaml rename to host-tools/SuspendResume/SuspendResume.yaml diff --git a/Runner/suites/Kernel/Baseport/SuspendResume/run.sh b/host-tools/SuspendResume/run.sh similarity index 100% rename from Runner/suites/Kernel/Baseport/SuspendResume/run.sh rename to host-tools/SuspendResume/run.sh From 9adb4740e6f69f8f7a88d7ab44bc3727efd3faf6 Mon Sep 17 00:00:00 2001 From: Nitin Nakka Date: Fri, 6 Feb 2026 20:46:14 +0530 Subject: [PATCH 03/10] Modified readme and yaml files based on new path Signed-off-by: Nitin Nakka --- host-tools/SuspendResume/README.md | 10 ++++++---- host-tools/SuspendResume/SuspendResume.yaml | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/host-tools/SuspendResume/README.md b/host-tools/SuspendResume/README.md index 8362eb54..40419bac 100644 --- a/host-tools/SuspendResume/README.md +++ b/host-tools/SuspendResume/README.md @@ -24,7 +24,7 @@ This test case validates the system suspend/resume functionality on the target d ## Usage Instructions: 1. **Copy repo to Host Machine**: Clone or download the repository to your host machine where ADB is installed. -2. **Connect Device**: Ensure the target device is connected via ADB and visible with `adb devices`. +2. **Connect Device**: Ensure exactly **one** target device is connected via ADB and visible with `adb devices`. 3. **Run Test**: Execute the test script which will remotely control the device via ADB. Run the SuspendResume test using: @@ -35,14 +35,16 @@ Run the SuspendResume test using: git clone cd -# Ensure device is connected +# Ensure exactly one device is connected adb devices -# Run the test -cd Runner/suites/Kernel/Baseport/SuspendResume +# Run the test from the new location +cd host-tools/SuspendResume ./run.sh ``` +**Note:** The test requires exactly one ADB device to be connected. If multiple devices are detected, the test will skip with an error message. + --- ## Prerequisites diff --git a/host-tools/SuspendResume/SuspendResume.yaml b/host-tools/SuspendResume/SuspendResume.yaml index caa36d75..7612db0d 100644 --- a/host-tools/SuspendResume/SuspendResume.yaml +++ b/host-tools/SuspendResume/SuspendResume.yaml @@ -1,7 +1,7 @@ metadata: name: SuspendResume format: "Lava-Test Test Definition 1.0" - description: "Suspend/Resume validation with kernel debug stats collection" + description: "ADB-based Suspend/Resume validation with kernel debug stats collection" os: - linux scope: @@ -10,6 +10,6 @@ metadata: run: steps: - REPO_PATH=$PWD - - cd Runner/suites/Kernel/Baseport/SuspendResume + - cd host-tools/SuspendResume - ./run.sh || true - $REPO_PATH/Runner/utils/send-to-lava.sh SuspendResume.res || true From 0032b44b4f0381bac9826002389cab5214525db3 Mon Sep 17 00:00:00 2001 From: Nitin Nakka Date: Mon, 9 Feb 2026 19:26:24 +0530 Subject: [PATCH 04/10] fix: Set executable permission on SuspendResume/run.sh for LAVA Signed-off-by: Nitin Nakka --- host-tools/SuspendResume/run.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 host-tools/SuspendResume/run.sh diff --git a/host-tools/SuspendResume/run.sh b/host-tools/SuspendResume/run.sh old mode 100644 new mode 100755 From bd69403fd75287d61c45f7589c4ae02c552433ca Mon Sep 17 00:00:00 2001 From: Nitin Nakka Date: Tue, 10 Feb 2026 13:55:58 +0530 Subject: [PATCH 05/10] Added init_env file for host-tools folder, run-test.sh file for running tests similar to runner if more tests are added in future and modified the readme file Signed-off-by: Nitin Nakka --- host-tools/SuspendResume/README.md | 45 ++++++-- host-tools/init_env | 121 ++++++++++++++++++++ host-tools/run-test.sh | 172 +++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+), 8 deletions(-) create mode 100644 host-tools/init_env create mode 100644 host-tools/run-test.sh diff --git a/host-tools/SuspendResume/README.md b/host-tools/SuspendResume/README.md index 40419bac..0cc75349 100644 --- a/host-tools/SuspendResume/README.md +++ b/host-tools/SuspendResume/README.md @@ -22,15 +22,44 @@ This test case validates the system suspend/resume functionality on the target d - Complete qcom_stats and suspend_stats dumps ## Usage -Instructions: -1. **Copy repo to Host Machine**: Clone or download the repository to your host machine where ADB is installed. -2. **Connect Device**: Ensure exactly **one** target device is connected via ADB and visible with `adb devices`. -3. **Run Test**: Execute the test script which will remotely control the device via ADB. -Run the SuspendResume test using: ---- +### Directory Structure +This test is located in the `host-tools` directory structure: +``` +qcom-linux-testkit/ +├── Runner/ # Main test framework +│ ├── utils/ # Shared utilities (functestlib.sh, etc.) +│ └── suites/ # Standard test suites +└── host-tools/ # Host-based tests (ADB, remote control) + ├── init_env # Environment setup (finds Runner/utils) + ├── run-test.sh # Test orchestrator for host-tools tests + └── SuspendResume/ # This test + ├── run.sh + ├── README.md + └── SuspendResume.yaml +``` + +### Running the Test + +#### Method 1: Direct Execution (Recommended for LAVA) +```sh +cd host-tools/SuspendResume +./run.sh +``` + +#### Method 2: Using Test Orchestrator +```sh +cd host-tools +./run-test.sh SuspendResume +``` + +#### Method 3: Run All Host-Tools Tests +```sh +cd host-tools +./run-test.sh all +``` -#### Quick Example +### Quick Example ```sh git clone cd @@ -38,7 +67,7 @@ cd # Ensure exactly one device is connected adb devices -# Run the test from the new location +# Run the test cd host-tools/SuspendResume ./run.sh ``` diff --git a/host-tools/init_env b/host-tools/init_env new file mode 100644 index 00000000..5f52cdb8 --- /dev/null +++ b/host-tools/init_env @@ -0,0 +1,121 @@ +#!/bin/sh + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause-Clear + +# Idempotency guard: only initialize ONCE per shell session +[ -n "$__INIT_ENV_LOADED" ] && return +__INIT_ENV_LOADED=1 + +# (Optional) Remove/comment log line below to keep CI logs clean: +# echo "[INFO] init_env loaded." + +# --- Robust root detection --- +if [ -z "$ROOT_DIR" ]; then + # Fast path: current working directory is root + if [ -d "./utils" ] && [ -d "./suites" ]; then + ROOT_DIR="$(pwd)" + # Check if we're in host-tools and Runner exists as sibling + elif [ -d "../Runner/utils" ] && [ -d "../Runner/suites" ]; then + ROOT_DIR="$(cd ../Runner && pwd)" + else + # Fallback: walk up from this script's location + _script_dir="$(cd "$(dirname "$0")" && pwd)" + _search="$_script_dir" + while [ "$_search" != "/" ]; do + if [ -d "$_search/utils" ] && [ -d "$_search/suites" ]; then + ROOT_DIR="$_search" + break + fi + # Also check for Runner subdirectory + if [ -d "$_search/Runner/utils" ] && [ -d "$_search/Runner/suites" ]; then + ROOT_DIR="$_search/Runner" + break + fi + _search=$(dirname "$_search") + done + fi +fi + +# --- Validate and export key environment paths --- +if [ -z "${ROOT_DIR:-}" ] || [ ! -d "$ROOT_DIR/utils" ] || [ ! -f "$ROOT_DIR/utils/functestlib.sh" ]; then + echo "[ERROR] Could not detect testkit root (missing utils/ or functestlib.sh)" >&2 + exit 1 +fi + +export ROOT_DIR +export TOOLS="$ROOT_DIR/utils" +export __RUNNER_SUITES_DIR="$ROOT_DIR/suites" +export __RUNNER_UTILS_BIN_DIR="$ROOT_DIR/common" + +# --- Ensure TOOLS is usable in all shells --- +case ":$PATH:" in + *":$TOOLS:"*) : ;; + *) + PATH="$TOOLS:$PATH" + export PATH + ;; +esac + +# --- Source functestlib.sh to make functions available --- +# Note: functestlib.sh must be sourced for run-test.sh to work properly +if [ -f "$TOOLS/functestlib.sh" ]; then + # shellcheck disable=SC1090,SC1091 + . "$TOOLS/functestlib.sh" +fi + +############################################################################### +# Stdout/stderr capture (per-test folder) +# +# Controls (set BEFORE sourcing this file): +# RUN_STDOUT_ENABLE = 1 | 0 (default: 1) +# RUN_STDOUT_TAG = (default: basename of $PWD) +# RUN_STDOUT_FILE = (default: $PWD/_stdout_.log) +# +# Behavior: +# - Writes the capture file into the CURRENT DIRECTORY (usually the test dir). +# - No global logs/stdout directory is created/used. +############################################################################### +_runner_stdout_cleanup() { + st=$? + # restore original fds (if they were saved) + exec 1>&3 2>&4 + if [ -n "${__TEE_PID:-}" ]; then + kill "$__TEE_PID" 2>/dev/null + fi + if [ -n "${PIPE:-}" ]; then + rm -f "$PIPE" 2>/dev/null + fi + exit "$st" +} + +if [ "${RUN_STDOUT_ENABLE:-1}" -eq 1 ] && [ -z "${__RUN_STDOUT_ACTIVE:-}" ]; then + _tag="${RUN_STDOUT_TAG:-$(basename "$(pwd)")}" + _ts="$(date +%Y%m%d-%H%M%S)" + RUN_STDOUT_FILE="${RUN_STDOUT_FILE:-$(pwd)/${_tag}_stdout_${_ts}.log}" + export RUN_STDOUT_FILE + + # Save original stdout/stderr + exec 3>&1 4>&2 + + if command -v tee >/dev/null 2>&1; then + PIPE="$(mktemp -u "/tmp/stdout_pipe.XXXXXX")" + if mkfifo "$PIPE" 2>/dev/null; then + ( tee -a "$RUN_STDOUT_FILE" >&3 ) < "$PIPE" & + __TEE_PID=$! + exec > "$PIPE" 2>&1 + __RUN_STDOUT_ACTIVE=1 + trap _runner_stdout_cleanup EXIT INT TERM + else + # Fallback: file-only capture + exec >> "$RUN_STDOUT_FILE" 2>&1 + __RUN_STDOUT_ACTIVE=1 + trap _runner_stdout_cleanup EXIT INT TERM + fi + else + # Fallback: file-only capture + exec >> "$RUN_STDOUT_FILE" 2>&1 + __RUN_STDOUT_ACTIVE=1 + trap _runner_stdout_cleanup EXIT INT TERM + fi +fi diff --git a/host-tools/run-test.sh b/host-tools/run-test.sh new file mode 100644 index 00000000..da0fa16c --- /dev/null +++ b/host-tools/run-test.sh @@ -0,0 +1,172 @@ +#!/bin/sh + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause-Clear + +# Resolve the real path of this script +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Disable wrapper-level capture; each test will capture in its own folder +export RUN_STDOUT_ENABLE=0 +unset RUN_STDOUT_TAG RUN_STDOUT_FILE + +# Safely source init_env from the same directory as this script +# init_env will set TOOLS, ROOT_DIR, __RUNNER_SUITES_DIR, etc. +if [ -f "$SCRIPT_DIR/init_env" ]; then + # shellcheck source=/dev/null + . "$SCRIPT_DIR/init_env" +else + echo "[ERROR] init_env not found at $SCRIPT_DIR/init_env" + exit 1 +fi + +# Verify that init_env set up the environment correctly +if [ -z "$TOOLS" ] || [ ! -f "$TOOLS/functestlib.sh" ]; then + echo "[ERROR] functestlib.sh not found at $TOOLS/functestlib.sh" + echo "[ERROR] init_env may not have set up the environment correctly" + exit 1 +fi + +# Export key vars so they are visible to child scripts like ./run.sh +export ROOT_DIR +export TOOLS +export __RUNNER_SUITES_DIR +export __RUNNER_UTILS_BIN_DIR + +# Set host-tools specific suites directory +HOST_TOOLS_DIR="$SCRIPT_DIR" +export HOST_TOOLS_DIR + +# Store results +RESULTS_PASS="" +RESULTS_FAIL="" +RESULTS_SKIP="" + +execute_test_case() { + test_path=$1 + shift + + test_name=$(basename "$test_path") + + if [ -d "$test_path" ]; then + run_script="$test_path/run.sh" + if [ -f "$run_script" ]; then + log "Executing test case: $test_name" + ( + cd "$test_path" || exit 2 + # Enable per-test capture in the test folder with a clear tag + RUN_STDOUT_ENABLE=1 RUN_STDOUT_TAG="$test_name" sh "./run.sh" "$@" + ) + res_file="$test_path/$test_name.res" + if [ -f "$res_file" ]; then + if grep -q "SKIP" "$res_file"; then + log_skip "$test_name skipped" + if [ -z "$RESULTS_SKIP" ]; then + RESULTS_SKIP="$test_name" + else + RESULTS_SKIP=$(printf "%s\n%s" "$RESULTS_SKIP" "$test_name") + fi + elif grep -q "PASS" "$res_file"; then + log_pass "$test_name passed" + if [ -z "$RESULTS_PASS" ]; then + RESULTS_PASS="$test_name" + else + RESULTS_PASS=$(printf "%s\n%s" "$RESULTS_PASS" "$test_name") + fi + elif grep -q "FAIL" "$res_file"; then + log_fail "$test_name failed" + if [ -z "$RESULTS_FAIL" ]; then + RESULTS_FAIL="$test_name" + else + RESULTS_FAIL=$(printf "%s\n%s" "$RESULTS_FAIL" "$test_name") + fi + else + log_fail "$test_name: unknown result in .res file" + RESULTS_FAIL=$(printf "%s\n%s" "$RESULTS_FAIL" "$test_name (unknown result)") + fi + else + log_fail "$test_name: .res file not found" + RESULTS_FAIL=$(printf "%s\n%s" "$RESULTS_FAIL" "$test_name (.res not found)") + fi + else + log_error "No run.sh found in $test_path" + RESULTS_FAIL=$(printf "%s\n%s" "$RESULTS_FAIL" "$test_name (missing run.sh)") + fi + else + log_error "Test case directory not found: $test_path" + RESULTS_FAIL=$(printf "%s\n%s" "$RESULTS_FAIL" "$test_name (directory not found)") + fi +} + +run_specific_test_by_name() { + test_name=$1 + shift + test_path=$(find_test_case_by_name "$test_name") + if [ -z "$test_path" ]; then + log_error "Test case with name $test_name not found." + RESULTS_FAIL=$(printf "%s\n%s" "$RESULTS_FAIL" "$test_name (not found)") + else + execute_test_case "$test_path" "$@" + fi +} + +run_all_tests() { + # Search for tests in host-tools directory (not Runner/suites) + find "${HOST_TOOLS_DIR}" -maxdepth 2 -type d -name '[A-Za-z]*' | while IFS= read -r test_dir; do + # Skip the host-tools directory itself + if [ "$test_dir" = "$HOST_TOOLS_DIR" ]; then + continue + fi + if [ -f "$test_dir/run.sh" ]; then + execute_test_case "$test_dir" + fi + done +} + +print_summary() { + echo + log_info "========== Test Summary ==========" + echo "PASSED:" + [ -n "$RESULTS_PASS" ] && printf "%s\n" "$RESULTS_PASS" || echo " None" + echo + echo "FAILED:" + [ -n "$RESULTS_FAIL" ] && printf "%s\n" "$RESULTS_FAIL" || echo " None" + echo + echo "SKIPPED:" + [ -n "$RESULTS_SKIP" ] && printf "%s\n" "$RESULTS_SKIP" || echo " None" + log_info "==================================" +} + +print_usage() { + cat >&2 < [arg1 arg2 ...] + +Notes: + - Extra args are forwarded only when a single is specified. + - 'all' runs every test and does not accept additional args. + - Each test captures stdout/stderr next to its .res file as: + _stdout_.log +EOF +} + +if [ "$#" -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + print_usage + if [ "$#" -eq 0 ]; then + log_error "No arguments provided" + exit 1 + else + exit 0 + fi +fi + +if [ "$1" = "all" ]; then + run_all_tests +else + test_case_name="$1" + shift + run_specific_test_by_name "$test_case_name" "$@" +fi + +print_summary From 8a6ac9bdd70e81055f91479a9e682f9d2cc3cf31 Mon Sep 17 00:00:00 2001 From: Nitin Nakka Date: Tue, 10 Feb 2026 14:00:12 +0530 Subject: [PATCH 06/10] Add executable permissions to host-tools scripts Signed-off-by: Nitin Nakka --- host-tools/init_env | 0 host-tools/run-test.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 host-tools/init_env mode change 100644 => 100755 host-tools/run-test.sh diff --git a/host-tools/init_env b/host-tools/init_env old mode 100644 new mode 100755 diff --git a/host-tools/run-test.sh b/host-tools/run-test.sh old mode 100644 new mode 100755 From a3a05fe5d6a70b4143481d364d3f29769dacd6e5 Mon Sep 17 00:00:00 2001 From: Nitin Nakka Date: Wed, 11 Feb 2026 18:28:23 +0530 Subject: [PATCH 07/10] Update suspend/resume scripts, init env, and add utils similar like runner to run independently. Signed-off-by: Nitin Nakka --- host-tools/SuspendResume/run.sh | 5 - host-tools/init_env | 55 +- host-tools/utils/functestlib.sh | 4506 +++++++++++++++++++++++++++++++ 3 files changed, 4533 insertions(+), 33 deletions(-) create mode 100644 host-tools/utils/functestlib.sh diff --git a/host-tools/SuspendResume/run.sh b/host-tools/SuspendResume/run.sh index 3dd01ef2..1805a39c 100755 --- a/host-tools/SuspendResume/run.sh +++ b/host-tools/SuspendResume/run.sh @@ -25,13 +25,8 @@ if [ -z "$__INIT_ENV_LOADED" ]; then # shellcheck disable=SC1090 . "$INIT_ENV" fi -# Always source functestlib.sh, using $TOOLS exported by init_env -# shellcheck disable=SC1090,SC1091 -. "$TOOLS/functestlib.sh" TESTNAME="SuspendResume" -test_path=$(find_test_case_by_name "$TESTNAME") -cd "$test_path" || exit 1 # shellcheck disable=SC2034 res_file="./$TESTNAME.res" diff --git a/host-tools/init_env b/host-tools/init_env index 5f52cdb8..f053543d 100755 --- a/host-tools/init_env +++ b/host-tools/init_env @@ -7,45 +7,45 @@ [ -n "$__INIT_ENV_LOADED" ] && return __INIT_ENV_LOADED=1 -# (Optional) Remove/comment log line below to keep CI logs clean: -# echo "[INFO] init_env loaded." - -# --- Robust root detection --- +# --- Robust root detection for host-tools --- if [ -z "$ROOT_DIR" ]; then - # Fast path: current working directory is root - if [ -d "./utils" ] && [ -d "./suites" ]; then - ROOT_DIR="$(pwd)" - # Check if we're in host-tools and Runner exists as sibling - elif [ -d "../Runner/utils" ] && [ -d "../Runner/suites" ]; then - ROOT_DIR="$(cd ../Runner && pwd)" + # When sourced, we need to find the directory containing this init_env file + # Try multiple methods to get the script location + if [ -n "${BASH_SOURCE:-}" ]; then + # Bash + _script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + elif [ -n "${ZSH_VERSION:-}" ]; then + # Zsh + _script_dir="$(cd "$(dirname "${(%):-%x}")" && pwd)" + else + # POSIX fallback: assume init_env is in a known location relative to caller + # The caller (run.sh) sets INIT_ENV variable before sourcing + if [ -n "${INIT_ENV:-}" ]; then + _script_dir="$(cd "$(dirname "$INIT_ENV")" && pwd)" + else + _script_dir="$(cd "$(dirname "$0")" && pwd)" + fi + fi + + # For host-tools, ROOT_DIR is the host-tools directory itself + if [ -d "$_script_dir/utils" ] && [ -f "$_script_dir/utils/functestlib.sh" ]; then + ROOT_DIR="$_script_dir" else - # Fallback: walk up from this script's location - _script_dir="$(cd "$(dirname "$0")" && pwd)" - _search="$_script_dir" - while [ "$_search" != "/" ]; do - if [ -d "$_search/utils" ] && [ -d "$_search/suites" ]; then - ROOT_DIR="$_search" - break - fi - # Also check for Runner subdirectory - if [ -d "$_search/Runner/utils" ] && [ -d "$_search/Runner/suites" ]; then - ROOT_DIR="$_search/Runner" - break - fi - _search=$(dirname "$_search") - done + echo "[ERROR] Could not detect host-tools root (missing utils/ or functestlib.sh)" >&2 + echo "[DEBUG] Tried _script_dir=$_script_dir" >&2 + exit 1 fi fi # --- Validate and export key environment paths --- if [ -z "${ROOT_DIR:-}" ] || [ ! -d "$ROOT_DIR/utils" ] || [ ! -f "$ROOT_DIR/utils/functestlib.sh" ]; then - echo "[ERROR] Could not detect testkit root (missing utils/ or functestlib.sh)" >&2 + echo "[ERROR] Could not detect host-tools root (missing utils/ or functestlib.sh)" >&2 exit 1 fi export ROOT_DIR export TOOLS="$ROOT_DIR/utils" -export __RUNNER_SUITES_DIR="$ROOT_DIR/suites" +export __RUNNER_SUITES_DIR="$ROOT_DIR" export __RUNNER_UTILS_BIN_DIR="$ROOT_DIR/common" # --- Ensure TOOLS is usable in all shells --- @@ -58,7 +58,6 @@ case ":$PATH:" in esac # --- Source functestlib.sh to make functions available --- -# Note: functestlib.sh must be sourced for run-test.sh to work properly if [ -f "$TOOLS/functestlib.sh" ]; then # shellcheck disable=SC1090,SC1091 . "$TOOLS/functestlib.sh" diff --git a/host-tools/utils/functestlib.sh b/host-tools/utils/functestlib.sh new file mode 100644 index 00000000..7ff9ddca --- /dev/null +++ b/host-tools/utils/functestlib.sh @@ -0,0 +1,4506 @@ +#!/bin/sh +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause-Clear + +# --- Logging helpers --- +log() { + level=$1 + shift + echo "[$level] $(date '+%Y-%m-%d %H:%M:%S') - $*" +} +log_info() { log "INFO" "$@"; } +log_pass() { log "PASS" "$@"; } +log_fail() { log "FAIL" "$@"; } +log_error() { log "ERROR" "$@"; } +log_skip() { log "SKIP" "$@"; } +log_warn() { log "WARN" "$@"; } + +# --- Kernel Log Collection --- +get_kernel_log() { + if command -v journalctl >/dev/null 2>&1; then + journalctl -k -b + elif command -v dmesg >/dev/null 2>&1; then + dmesg + elif [ -f /var/log/kern.log ]; then + cat /var/log/kern.log + else + log_warn "No kernel log source found" + return 1 + fi +} + +# Locate a kernel module (.ko) file by name +# Tries to find it under current kernel version first, then all module trees +find_kernel_module() { + module_name="$1" + kver=$(uname -r) + + # Attempt to find module under the currently running kernel + module_path=$(find "/lib/modules/$kver" -name "${module_name}.ko" 2>/dev/null | head -n 1) + + # If not found, search all available module directories + if [ -z "$module_path" ]; then + log_warn "Module not found under /lib/modules/$kver, falling back to full search in /lib/modules/" + module_path=$(find /lib/modules/ -name "${module_name}.ko" 2>/dev/null | head -n 1) + + # Warn if found outside current kernel version + if [ -n "$module_path" ]; then + found_version=$(echo "$module_path" | cut -d'/' -f4) + if [ "$found_version" != "$kver" ]; then + log_warn "Found ${module_name}.ko under $found_version, not under current kernel ($kver)" + fi + fi + fi + echo "$module_path" +} + +# Check if a kernel module is currently loaded +is_module_loaded() { + module_name="$1" + /sbin/lsmod | awk '{print $1}' | grep -q "^${module_name}$" +} + +# load_kernel_module [params...] +# 1) If already loaded, no-op +# 2) Try insmod [params] +# 3) If that fails, try modprobe [params] +load_kernel_module() { + module_path="$1"; shift + params="$*" + module_name=$(basename "$module_path" .ko) + + if is_module_loaded "$module_name"; then + log_info "Module $module_name is already loaded" + return 0 + fi + + if [ ! -f "$module_path" ]; then + log_error "Module file not found: $module_path" + # still try modprobe if it exists in modules directory + else + log_info "Loading module via insmod: $module_path $params" + if /sbin/insmod "$module_path" "$params" 2>insmod_err.log; then + log_info "Module $module_name loaded successfully via insmod" + return 0 + else + log_warn "insmod failed: $(cat insmod_err.log)" + fi + fi + + # fallback to modprobe + log_info "Falling back to modprobe $module_name $params" + if /sbin/modprobe "$module_name" "$params" 2>modprobe_err.log; then + log_info "Module $module_name loaded successfully via modprobe" + return 0 + else + log_error "modprobe failed: $(cat modprobe_err.log)" + return 1 + fi +} + +# Remove a kernel module by name with optional forced removal +unload_kernel_module() { + module_name="$1" + force="$2" + + if ! is_module_loaded "$module_name"; then + log_info "Module $module_name is not loaded, skipping unload" + return 0 + fi + + log_info "Attempting to remove module: $module_name" + if /sbin/rmmod "$module_name" 2>rmmod_err.log; then + log_info "Module $module_name removed via rmmod" + return 0 + fi + + log_warn "rmmod failed: $(cat rmmod_err.log)" + log_info "Trying modprobe -r as fallback" + if /sbin/modprobe -r "$module_name" 2>modprobe_err.log; then + log_info "Module $module_name removed via modprobe" + return 0 + fi + + log_warn "modprobe -r failed: $(cat modprobe_err.log)" + + if [ "$force" = "true" ]; then + log_warn "Trying forced rmmod: $module_name" + if /sbin/rmmod -f "$module_name" 2>>rmmod_err.log; then + log_info "Module $module_name force removed" + return 0 + else + log_error "Forced rmmod failed: $(cat rmmod_err.log)" + return 1 + fi + fi + + log_error "Unable to unload module: $module_name" + return 1 +} + +# --- Dependency check --- +check_dependencies() { + # Support both: + # check_dependencies date awk sed + # check_dependencies "$deps" where deps="date awk sed" + if [ "$#" -eq 1 ]; then + # Split the single string into args + # shellcheck disable=SC2086 + set -- $1 + fi + + missing=0 + missing_cmds="" + + for cmd in "$@"; do + [ -n "$cmd" ] || continue + if ! command -v "$cmd" >/dev/null 2>&1; then + log_warn "Required command '$cmd' not found in PATH." + missing=1 + missing_cmds="$missing_cmds $cmd" + fi + done + + if [ "$missing" -ne 0 ]; then + testname="${TESTNAME:-UnknownTest}" + log_skip "$testname SKIP missing dependencies$missing_cmds" + if [ -n "${TESTNAME:-}" ]; then + echo "$TESTNAME SKIP" > "./$TESTNAME.res" 2>/dev/null || true + fi + exit 0 + fi + + return 0 +} + +# --- Test case directory lookup --- +find_test_case_by_name() { + test_name=$1 + base_dir="${__RUNNER_SUITES_DIR:-$ROOT_DIR/suites}" + # Only search under the SUITES directory! + testpath=$(find "$base_dir" -type d -iname "$test_name" -print -quit 2>/dev/null) + echo "$testpath" +} + +find_test_case_bin_by_name() { + test_name=$1 + base_dir="${__RUNNER_UTILS_BIN_DIR:-$ROOT_DIR/common}" + find "$base_dir" -type f -iname "$test_name" -print -quit 2>/dev/null +} + +find_test_case_script_by_name() { + test_name=$1 + base_dir="${__RUNNER_UTILS_BIN_DIR:-$ROOT_DIR/common}" + find "$base_dir" -type d -iname "$test_name" -print -quit 2>/dev/null +} + +# Check each given kernel config is set to y/m in /proc/config.gz, logs result, returns 0/1. +check_kernel_config() { + cfgs=$1 + for config_key in $cfgs; do + if command -v zgrep >/dev/null 2>&1; then + if zgrep -qE "^${config_key}=(y|m)" /proc/config.gz 2>/dev/null; then + log_pass "Kernel config $config_key is enabled" + else + log_fail "Kernel config $config_key is missing or not enabled" + return 1 + fi + else + # Fallback if zgrep is unavailable + if gzip -dc /proc/config.gz 2>/dev/null | grep -qE "^${config_key}=(y|m)"; then + log_pass "Kernel config $config_key is enabled" + else + log_fail "Kernel config $config_key is missing or not enabled" + return 1 + fi + fi + done + return 0 +} + +check_dt_nodes() { + node_paths="$1" + log_info "$node_paths" + found=false + for node in $node_paths; do + log_info "$node" + if [ -d "$node" ] || [ -f "$node" ]; then + log_pass "Device tree node exists: $node" + found=true + fi + done + + if [ "$found" = true ]; then + return 0 + else + log_fail "Device tree node(s) missing: $node_paths" + return 1 + fi +} + +check_driver_loaded() { + drivers="$1" + for driver in $drivers; do + if [ -z "$driver" ]; then + log_fail "No driver/module name provided to check_driver_loaded" + return 1 + fi + if grep -qw "$driver" /proc/modules || lsmod | awk '{print $1}' | grep -qw "$driver"; then + log_pass "Driver/module '$driver' is loaded" + return 0 + else + log_fail "Driver/module '$driver' is not loaded" + return 1 + fi + done +} + +# --- Optional: POSIX-safe repo root detector --- +detect_runner_root() { + path=$1 + while [ "$path" != "/" ]; do + if [ -d "$path/suites" ]; then + echo "$path" + return + fi + path=$(dirname "$path") + done + echo "" +} + +# ---------------------------- +# Additional Utility Functions +# ---------------------------- +# Function is to check for network connectivity status +check_network_status() { + echo "[INFO] Checking network connectivity..." + + # Prefer the egress/source IP chosen by the routing table (most accurate). + ip_addr=$(ip -4 route get 1.1.1.1 2>/dev/null \ + | awk 'NR==1{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}') + + # Fallback: first global IPv4 on any UP interface (works even without a default route). + if [ -z "$ip_addr" ]; then + ip_addr=$(ip -o -4 addr show scope global up 2>/dev/null \ + | awk 'NR==1{split($4,a,"/"); print a[1]}') + fi + + if [ -n "$ip_addr" ]; then + echo "[PASS] Network is active. IP address: $ip_addr" + + # Quick reachability probe (single ICMP). BusyBox-compatible flags. + if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then + echo "[PASS] Internet is reachable." + return 0 + else + echo "[WARN] Network active but no internet access." + return 2 + fi + else + echo "[FAIL] No active network interface found." + return 1 + fi +} + +# --- Make sure system time is sane (TLS needs a sane clock) --- +ensure_reasonable_clock() { + now="$(date +%s 2>/dev/null || echo 0)" + cutoff="$(date -d '2020-01-01 UTC' +%s 2>/dev/null || echo 1577836800)" + [ -z "$cutoff" ] && cutoff=1577836800 + + [ "$now" -ge "$cutoff" ] 2>/dev/null && return 0 + + log_warn "System clock looks invalid (epoch=$now). Trying local time sources (no network)..." + + # Optional diagnostics file (caller may set this, e.g. CLOCK_DIAG_FILE="$OUT_DIR/clock_diag.txt") + diag_file="${CLOCK_DIAG_FILE:-}" + + # ---- Diagnostics (only when invalid clock) ---- + if [ -n "$diag_file" ]; then + { + echo "==== CLOCK DIAGNOSTICS ====" + echo "timestamp=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || true)" + echo "epoch_now=$now cutoff=$cutoff" + echo + } >>"$diag_file" 2>/dev/null || true + fi + + # Log minimal summaries to stdout; write full outputs to diag file. + # date summary + date_out="$(date 2>&1 || true)" + if [ -n "$diag_file" ]; then + { + echo "---- date ----" + echo "$date_out" + echo + } >>"$diag_file" 2>/dev/null || true + fi + log_info "date: $(printf '%s' "$date_out" | head -n 1)" + + # timedatectl summaries + full dump + if command -v timedatectl >/dev/null 2>&1; then + td_status="$(timedatectl status 2>&1 || true)" + if [ -n "$diag_file" ]; then + { + echo "---- timedatectl status ----" + echo "$td_status" + echo + } >>"$diag_file" 2>/dev/null || true + fi + + # minimal, stable-ish summaries + td_local="$(printf '%s\n' "$td_status" | sed -n 's/^[[:space:]]*Local time:[[:space:]]*//p' | head -n 1)" + td_sync="$(printf '%s\n' "$td_status" | sed -n 's/^[[:space:]]*System clock synchronized:[[:space:]]*//p' | head -n 1)" + td_ntp="$(printf '%s\n' "$td_status" | sed -n 's/^[[:space:]]*NTP service:[[:space:]]*//p' | head -n 1)" + td_rtc="$(printf '%s\n' "$td_status" | sed -n 's/^[[:space:]]*RTC time:[[:space:]]*//p' | head -n 1)" + + [ -n "$td_local" ] && log_info "timedatectl: Local time: $td_local" + [ -n "$td_rtc" ] && log_info "timedatectl: RTC time: $td_rtc" + [ -n "$td_sync" ] && log_info "timedatectl: System clock synchronized: $td_sync" + [ -n "$td_ntp" ] && log_info "timedatectl: NTP service: $td_ntp" + + td_show="$(timedatectl show-timesync --all 2>&1 || true)" + if [ -n "$diag_file" ]; then + { + echo "---- timedatectl show-timesync --all ----" + echo "$td_show" + echo + } >>"$diag_file" 2>/dev/null || true + fi + + td_server="$(printf '%s\n' "$td_show" | sed -n 's/^ServerName=//p' | head -n 1)" + td_addr="$(printf '%s\n' "$td_show" | sed -n 's/^ServerAddress=//p' | head -n 1)" + td_sysntp="$(printf '%s\n' "$td_show" | sed -n 's/^SystemNTPServers=//p' | head -n 1)" + td_fallback="$(printf '%s\n' "$td_show" | sed -n 's/^FallbackNTPServers=//p' | head -n 1)" + + if [ -n "$td_server" ] || [ -n "$td_addr" ]; then + log_info "timesync: server=${td_server:-NA} addr=${td_addr:-NA}" + fi + if [ -n "$td_sysntp" ]; then + # Keep it short + short_sysntp="$(printf '%s' "$td_sysntp" | awk '{print $1" "$2" "$3" "$4}')" + log_info "timesync: SystemNTPServers: ${short_sysntp:-NA}" + elif [ -n "$td_fallback" ]; then + short_fb="$(printf '%s' "$td_fallback" | awk '{print $1" "$2" "$3" "$4}')" + log_info "timesync: FallbackNTPServers: ${short_fb:-NA}" + fi + + td_ts="$(timedatectl timesync-status 2>&1 || true)" + if [ -n "$diag_file" ]; then + { + echo "---- timedatectl timesync-status ----" + echo "$td_ts" + echo + } >>"$diag_file" 2>/dev/null || true + fi + + td_pkt="$(printf '%s\n' "$td_ts" | sed -n 's/^[[:space:]]*Packet count:[[:space:]]*//p' | head -n 1)" + td_srvline="$(printf '%s\n' "$td_ts" | sed -n 's/^[[:space:]]*Server:[[:space:]]*//p' | head -n 1)" + [ -n "$td_srvline" ] && log_info "timesync-status: Server: $td_srvline" + [ -n "$td_pkt" ] && log_info "timesync-status: Packet count: $td_pkt" + else + log_info "timedatectl: not available" + if [ -n "$diag_file" ]; then + echo "timedatectl: not available" >>"$diag_file" 2>/dev/null || true + fi + fi + + # systemctl status summaries + full dump + if command -v systemctl >/dev/null 2>&1; then + sdts="$(systemctl status systemd-timesyncd --no-pager --full 2>&1 || true)" + if [ -n "$diag_file" ]; then + { + echo "---- systemctl status systemd-timesyncd ----" + echo "$sdts" + echo + } >>"$diag_file" 2>/dev/null || true + fi + + sd_active="$(printf '%s\n' "$sdts" | sed -n 's/^[[:space:]]*Active:[[:space:]]*//p' | head -n 1)" + sd_pid="$(printf '%s\n' "$sdts" | sed -n 's/^[[:space:]]*Main PID:[[:space:]]*//p' | head -n 1)" + [ -n "$sd_active" ] && log_info "systemd-timesyncd: Active: $sd_active" + [ -n "$sd_pid" ] && log_info "systemd-timesyncd: Main PID: $sd_pid" + else + log_info "systemctl: not available" + if [ -n "$diag_file" ]; then + echo "systemctl: not available" >>"$diag_file" 2>/dev/null || true + fi + fi + + # 1) Try RTC (if it is sane) + if command -v hwclock >/dev/null 2>&1 && [ -e /dev/rtc0 ]; then + hwclock -s 2>/dev/null || true + now="$(date +%s 2>/dev/null || echo 0)" + if [ "$now" -ge "$cutoff" ] 2>/dev/null; then + log_pass "Clock restored from RTC." + return 0 + fi + + if [ -r /sys/class/rtc/rtc0/since_epoch ]; then + rtc_epoch="$(tr -cd 0-9 < /sys/class/rtc/rtc0/since_epoch 2>/dev/null)" + if [ -n "$rtc_epoch" ]; then + log_info "rtc0: since_epoch=$rtc_epoch" + if [ -n "$diag_file" ]; then + echo "rtc0: since_epoch=$rtc_epoch" >>"$diag_file" 2>/dev/null || true + fi + fi + if [ -n "$rtc_epoch" ] && [ "$rtc_epoch" -ge "$cutoff" ] 2>/dev/null; then + if date -d "@$rtc_epoch" >/dev/null 2>&1; then + date -s "@$rtc_epoch" >/dev/null 2>&1 || true + fi + now="$(date +%s 2>/dev/null || echo 0)" + if [ "$now" -ge "$cutoff" ] 2>/dev/null; then + log_pass "Clock restored from rtc0 since_epoch." + return 0 + fi + fi + fi + else + log_info "RTC: hwclock or /dev/rtc0 not available" + if [ -n "$diag_file" ]; then + echo "RTC: hwclock or /dev/rtc0 not available" >>"$diag_file" 2>/dev/null || true + fi + fi + + # 2) Try kernel build timestamp from /proc/version or uname -v + kb="$(uname -v 2>/dev/null || cat /proc/version 2>/dev/null || true)" + if [ -n "$diag_file" ]; then + { + echo "---- kernel version string ----" + echo "$kb" + echo + } >>"$diag_file" 2>/dev/null || true + fi + + kb_date="$(printf '%s\n' "$kb" | sed -n \ + 's/.*\([A-Z][a-z][a-z] [A-Z][a-z][a-z] [ 0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9] [A-Z][A-Za-z0-9+:-]* [0-9][0-9][0-9][0-9]\).*/\1/p' \ + | head -n 1)" + + if [ -n "$kb_date" ]; then + log_info "kernel-build-time: parsed='$kb_date'" + kb_epoch="$(date -d "$kb_date" +%s 2>/dev/null || echo 0)" + if [ "$kb_epoch" -ge "$cutoff" ] 2>/dev/null; then + date -s "$kb_date" >/dev/null 2>&1 || true + now="$(date +%s 2>/dev/null || echo 0)" + if [ "$now" -ge "$cutoff" ] 2>/dev/null; then + log_pass "Clock seeded from kernel build time: $kb_date" + return 0 + fi + fi + else + log_info "kernel-build-time: could not parse from uname -v / /proc/version" + fi + + log_warn "Clock still invalid; continuing (timestamps may be epoch)." + return 1 +} + +# If the tar file already exists,then function exit. Otherwise function to check the network connectivity and it will download tar from internet. +extract_tar_from_url() { + url="$1" + outdir="${LOG_DIR:-.}" + mkdir -p "$outdir" 2>/dev/null || true + + case "$url" in + /*) + tarfile="$url" + ;; + file://*) + tarfile="${url#file://}" + ;; + *) + tarfile="$outdir/$(basename "$url")" + ;; + esac + markfile="${tarfile}.extracted" + skip_sentinel="${outdir}/.asset_fetch_skipped" + + # If a previous run already marked "assets unavailable", honor it and SKIP. + if [ -f "$skip_sentinel" ]; then + log_info "Previous run marked assets unavailable on this system (${skip_sentinel}); skipping download." + return 2 + fi + + tar_already_extracted() { + tf="$1" + od="$2" + if [ -f "${tf}.extracted" ]; then + return 0 + fi + tmp_list="${od}/.tar_ls.$$" + if tar -tf "$tf" 2>/dev/null | head -n 20 > "$tmp_list"; then + total=0 + present=0 + while IFS= read -r ent; do + [ -z "$ent" ] && continue + total=$((total + 1)) + ent="${ent%/}" + if [ -e "$od/$ent" ] || [ -e "$od/$(basename "$ent")" ]; then + present=$((present + 1)) + fi + done < "$tmp_list" + rm -f "$tmp_list" 2>/dev/null || true + if [ "$present" -ge 3 ]; then + return 0 + fi + if [ "$total" -gt 0 ] && [ $((present * 100 / total)) -ge 50 ]; then + return 0 + fi + fi + return 1 + } + + if command -v check_tar_file >/dev/null 2>&1; then + check_tar_file "$url" + status=$? + else + if [ -f "$tarfile" ]; then + if tar_already_extracted "$tarfile" "$outdir"; then + status=0 + else + status=2 + fi + else + status=1 + fi + fi + + ensure_reasonable_clock || { + log_warn "Proceeding in limited-network mode." + limited_net=1 + } + + is_busybox_wget() { + if command -v wget >/dev/null 2>&1; then + if wget --help 2>&1 | head -n 1 | grep -qi busybox; then + return 0 + fi + fi + return 1 + } + + tls_capable_fetcher_available() { + scheme_https=0 + case "$url" in + https://*) + scheme_https=1 + ;; + esac + if [ "$scheme_https" -eq 0 ]; then + return 0 + fi + if command -v curl >/dev/null 2>&1; then + if curl -V 2>/dev/null | grep -qiE 'ssl|tls'; then + return 0 + fi + fi + if command -v aria2c >/dev/null 2>&1; then + return 0 + fi + if command -v wget >/dev/null 2>&1; then + if ! is_busybox_wget; then + return 0 + fi + if command -v openssl >/dev/null 2>&1; then + return 0 + fi + fi + return 1 + } + + try_download() { + src="$1" + dst="$2" + part="${dst}.part.$$" + ca="" + + for cand in \ + /etc/ssl/certs/ca-certificates.crt \ + /etc/ssl/cert.pem \ + /system/etc/security/cacerts/ca-certificates.crt + do + if [ -r "$cand" ]; then + ca="$cand" + break + fi + done + + if command -v curl >/dev/null 2>&1; then + if [ -n "$ca" ]; then + curl -4 -L --fail --retry 3 --retry-delay 2 --connect-timeout 10 \ + -o "$part" --cacert "$ca" "$src" + else + curl -4 -L --fail --retry 3 --retry-delay 2 --connect-timeout 10 \ + -o "$part" "$src" + fi + rc=$? + if [ $rc -eq 0 ]; then + mv -f "$part" "$dst" 2>/dev/null || true + return 0 + fi + rm -f "$part" 2>/dev/null || true + case "$rc" in + 60|35|22) + return 60 + ;; + esac + fi + + if command -v aria2c >/dev/null 2>&1; then + aria2c -x4 -s4 -m3 --connect-timeout=10 \ + -o "$(basename "$part")" --dir="$(dirname "$part")" "$src" + rc=$? + if [ $rc -eq 0 ]; then + mv -f "$part" "$dst" 2>/dev/null || true + return 0 + fi + rm -f "$part" 2>/dev/null || true + fi + + if command -v wget >/dev/null 2>&1; then + if is_busybox_wget; then + wget -O "$part" -T 15 "$src" + rc=$? + if [ $rc -ne 0 ]; then + log_warn "BusyBox wget failed (rc=$rc); final attempt with --no-check-certificate." + wget -O "$part" -T 15 --no-check-certificate "$src" + rc=$? + fi + if [ $rc -eq 0 ]; then + mv -f "$part" "$dst" 2>/dev/null || true + return 0 + fi + rm -f "$part" 2>/dev/null || true + return 60 + else + if [ -n "$ca" ]; then + wget -4 --timeout=15 --tries=3 --ca-certificate="$ca" -O "$part" "$src" + rc=$? + else + wget -4 --timeout=15 --tries=3 -O "$part" "$src" + rc=$? + fi + if [ $rc -ne 0 ]; then + log_warn "wget failed (rc=$rc); final attempt with --no-check-certificate." + wget -4 --timeout=15 --tries=1 --no-check-certificate -O "$part" "$src" + rc=$? + fi + if [ $rc -eq 0 ]; then + mv -f "$part" "$dst" 2>/dev/null || true + return 0 + fi + rm -f "$part" 2>/dev/null || true + if [ $rc -eq 5 ]; then + return 60 + fi + return $rc + fi + fi + + return 127 + } + + if [ "$status" -eq 0 ]; then + log_info "Already extracted. Skipping download." + return 0 + fi + + if [ "$status" -eq 2 ]; then + log_info "File exists and is valid, but not yet extracted. Proceeding to extract." + else + case "$url" in + /*|file://*) + if [ ! -f "$tarfile" ]; then + log_fail "Local tar file not found: $tarfile" + return 1 + fi + ;; + *) + if [ ! -f "$tarfile" ] || [ ! -s "$tarfile" ]; then + prestage_dirs="" + if [ -n "${ASSET_DIR:-}" ]; then prestage_dirs="$prestage_dirs $ASSET_DIR"; fi + if [ -n "${VIDEO_ASSET_DIR:-}" ]; then prestage_dirs="$prestage_dirs $VIDEO_ASSET_DIR"; fi + if [ -n "${AUDIO_ASSET_DIR:-}" ]; then prestage_dirs="$prestage_dirs $AUDIO_ASSET_DIR"; fi + prestage_dirs="$prestage_dirs . $outdir ${ROOT_DIR:-} ${ROOT_DIR:-}/cache /var/Runner /var/Runner/cache" + + for d in $prestage_dirs; do + if [ -d "$d" ] && [ -f "$d/$(basename "$tarfile")" ]; then + log_info "Using pre-staged tarball: $d/$(basename "$tarfile")" + cp -f "$d/$(basename "$tarfile")" "$tarfile" 2>/dev/null || true + break + fi + done + + if [ ! -s "$tarfile" ]; then + for top in /mnt /media; do + if [ -d "$top" ]; then + for d in "$top"/*; do + if [ -d "$d" ] && [ -f "$d/$(basename "$tarfile")" ]; then + log_info "Using pre-staged tarball: $d/$(basename "$tarfile")" + cp -f "$d/$(basename "$tarfile")" "$tarfile" 2>/dev/null || true + break 2 + fi + done + fi + done + fi + fi + + if [ ! -s "$tarfile" ]; then + if [ -n "$limited_net" ]; then + log_warn "Limited network, cannot fetch media bundle. Marking SKIP for callers." + : > "$skip_sentinel" 2>/dev/null || true + return 2 + fi + + if ! tls_capable_fetcher_available; then + log_warn "No TLS-capable downloader available on this minimal build, cannot fetch: $url" + log_warn "Pre-stage $(basename "$url") locally or use a file:// URL." + : > "$skip_sentinel" 2>/dev/null || true + return 2 + fi + + log_info "Downloading $url -> $tarfile" + if ! try_download "$url" "$tarfile"; then + rc=$? + if [ $rc -eq 60 ]; then + log_warn "TLS/handshake problem while downloading (cert/clock/firewall or minimal wget). Marking SKIP." + : > "$skip_sentinel" 2>/dev/null || true + return 2 + fi + log_fail "Failed to download $(basename "$url")" + return 1 + fi + fi + ;; + esac + fi + + log_info "Extracting $(basename "$tarfile")..." + if tar -xvf "$tarfile"; then + : > "$markfile" 2>/dev/null || true + # Clear the minimal/offline sentinel only if it exists (SC2015-safe) + if [ -f "$skip_sentinel" ]; then + rm -f "$skip_sentinel" 2>/dev/null || true + fi + + first_entry="$(tar -tf "$tarfile" 2>/dev/null | head -n 1 | sed 's#/$##')" + if [ -n "$first_entry" ]; then + if [ -e "$first_entry" ] || [ -e "$outdir/$first_entry" ]; then + log_pass "Files extracted successfully ($(basename "$first_entry") present)." + return 0 + fi + fi + log_warn "Extraction finished but couldn't verify entries. Assuming success." + return 0 + fi + + log_fail "Failed to extract $(basename "$tarfile")" + return 1 +} + + +# Function to check if a tar file exists +check_tar_file() { + url="$1" + outdir="${LOG_DIR:-.}" + mkdir -p "$outdir" 2>/dev/null || true + + case "$url" in + /*) tarfile="$url" ;; + file://*) tarfile="${url#file://}" ;; + *) tarfile="$outdir/$(basename "$url")" ;; + esac + markfile="${tarfile}.extracted" + + # 1) Existence & basic validity + if [ ! -f "$tarfile" ]; then + log_info "File $(basename "$tarfile") does not exist in $outdir." + return 1 + fi + if [ ! -s "$tarfile" ]; then + log_warn "File $(basename "$tarfile") exists but is empty." + return 1 + fi + if ! tar -tf "$tarfile" >/dev/null 2>&1; then + log_warn "File $(basename "$tarfile") is not a valid tar archive." + return 1 + fi + + # 2) Already extracted? (marker first) + if [ -f "$markfile" ]; then + log_pass "$(basename "$tarfile") has already been extracted (marker present)." + return 0 + fi + + # 3) Heuristic: check multiple entries from the tar exist on disk + tmp_list="${outdir}/.tar_ls.$$" + if tar -tf "$tarfile" 2>/dev/null | head -n 20 >"$tmp_list"; then + total=0; present=0 + while IFS= read -r ent; do + [ -z "$ent" ] && continue + total=$((total + 1)) + ent="${ent%/}" + # check exact relative path and also basename (covers archives with a top-level dir) + if [ -e "$outdir/$ent" ] || [ -e "$outdir/$(basename "$ent")" ]; then + present=$((present + 1)) + fi + done < "$tmp_list" + rm -f "$tmp_list" 2>/dev/null || true + + # If we find a reasonable portion of entries, assume it's extracted + if [ "$present" -ge 3 ] || { [ "$total" -gt 0 ] && [ $((present * 100 / total)) -ge 50 ]; }; then + log_pass "$(basename "$tarfile") already extracted ($present/$total entries found)." + return 0 + fi + fi + + # 4) Exists and valid, but not yet extracted + log_info "$(basename "$tarfile") exists and is valid, but not yet extracted." + return 2 +} + +# Return space-separated PIDs for 'weston' (BusyBox friendly). +weston_pids() { + pids="" + if command -v pgrep >/dev/null 2>&1; then + pids="$(pgrep -x weston 2>/dev/null || true)" + fi + if [ -z "$pids" ]; then + pids="$(ps -eo pid,comm 2>/dev/null | awk '$2=="weston"{print $1}')" + fi + echo "$pids" +} + +# Is Weston running? +weston_is_running() { + [ -n "$(weston_pids)" ] +} + +# Stop all Weston processes +weston_stop() { + if weston_is_running; then + log_info "Stopping Weston..." + pkill -x weston + for i in $(seq 1 10); do + log_info "Waiting for Weston to stop with $i attempt " + if ! weston_is_running; then + log_info "Weston stopped successfully" + return 0 + fi + sleep 1 + done + log_error "Failed to stop Weston after waiting." + return 1 + else + log_info "Weston is not running." + fi + return 0 +} + +# Start weston with correct env if not running +weston_start() { + if weston_is_running; then + log_info "Weston already running." + return 0 + fi + + if command -v systemctl >/dev/null 2>&1; then + log_info "Attempting to start via systemd: weston.service" + systemctl start weston.service >/dev/null 2>&1 || true + sleep 1 + if weston_is_running; then + log_info "Weston started via systemd (weston.service)." + return 0 + fi + + log_info "Attempting to start via systemd: weston@.service" + systemctl start weston@.service >/dev/null 2>&1 || true + sleep 1 + if weston_is_running; then + log_info "Weston started via systemd (weston@.service)." + return 0 + fi + + log_warn "systemd start did not bring Weston up; will try direct spawn." + fi + + # Minimal-friendly direct spawn (no headless module guesses here). + ensure_xdg_runtime_dir + + if ! command -v weston >/dev/null 2>&1; then + log_fail "weston binary not found in PATH." + return 1 + fi + + log_info "Attempting to spawn Weston (no backend override). Log: /tmp/weston.self.log" + ( nohup weston --log=/tmp/weston.self.log >/dev/null 2>&1 & ) || true + + tries=0 + while [ $tries -lt 5 ]; do + if weston_is_running; then + log_info "Weston is now running (PID(s): $(weston_pids))." + return 0 + fi + if [ -n "$(find_wayland_sockets | head -n1)" ]; then + log_info "A Wayland socket appeared after spawn." + return 0 + fi + sleep 1 + tries=$((tries+1)) + done + + if [ -f /tmp/weston.self.log ]; then + log_warn "Weston spawn failed; last log lines:" + tail -n 20 /tmp/weston.self.log 2>/dev/null | sed 's/^/[weston.log] /' || true + else + log_warn "Weston spawn failed; no log file present." + fi + return 1 +} + +overlay_start_weston_drm() { + EGL_JSON="/usr/share/glvnd/egl_vendor.d/EGL_adreno.json" + + if [ -f "$EGL_JSON" ]; then + export __EGL_VENDOR_LIBRARY_FILENAMES="$EGL_JSON" + log_info "Overlay EGL: using vendor JSON: $EGL_JSON" + fi + + RUNTIME_DIR="/dev/socket/weston" + if ! mkdir -p "$RUNTIME_DIR"; then + log_warn "Failed to create runtime dir $RUNTIME_DIR; falling back to /run/user/0" + RUNTIME_DIR="/run/user/0" + mkdir -p "$RUNTIME_DIR" || true + fi + chmod 700 "$RUNTIME_DIR" 2>/dev/null || true + + XDG_RUNTIME_DIR="$RUNTIME_DIR" + export XDG_RUNTIME_DIR + + # Do NOT force a specific WAYLAND_DISPLAY; let Weston decide. + unset WAYLAND_DISPLAY + + log_dir=${1:-$PWD} + WESTON_LOG="$log_dir/weston.log" + log_info "Overlay Weston start: XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR (WAYLAND_DISPLAY=)" + log_info "Weston log: $WESTON_LOG" + + # Start Weston in the background; we intentionally do not track the PID + # here to avoid killing it while clients are still using the socket. + weston --continue-without-input --idle-time=0 --log="$WESTON_LOG" \ + >/dev/null 2>&1 & + + # Best-effort check: see if ANY wayland-* socket appears, but do not kill Weston. + i=0 + sock_found="" + while [ "$i" -lt 10 ]; do + for candidate in "$XDG_RUNTIME_DIR"/wayland-*; do + [ -S "$candidate" ] || continue + sock_found="$candidate" + break + done + [ -n "$sock_found" ] && break + sleep 1 + i=$((i + 1)) + done + + if [ -n "$sock_found" ]; then + log_info "Overlay Weston created Wayland socket at $sock_found" + # We still let the caller discover/adopt the env via + # discover_wayland_socket_anywhere + adopt_wayland_env_from_socket. + return 0 + fi + + log_warn "Overlay Weston did not create a Wayland socket under $XDG_RUNTIME_DIR (see $WESTON_LOG)" + return 1 +} + +# Choose a socket (or try to start), adopt env, and echo chosen path. +wayland_choose_or_start() { + wayland_debug_snapshot "pre-choose" + sock="$(wayland_pick_socket || true)" + if [ -z "$sock" ]; then + log_info "No Wayland socket found; attempting to start Weston…" + weston_start || log_warn "weston_start() did not succeed." + # Re-scan a few times + n=0 + while [ $n -lt 5 ] && [ -z "$sock" ]; do + sock="$(wayland_pick_socket || true)" + [ -n "$sock" ] && break + sleep 1 + n=$((n+1)) + done + fi + if [ -n "$sock" ]; then + adopt_wayland_env_from_socket "$sock" + wayland_debug_snapshot "post-choose" + echo "$sock" + return 0 + fi + wayland_debug_snapshot "no-socket" + return 1 +} +# Ensure we have a writable XDG_RUNTIME_DIR for the current user. +# Prefers /run/user/, falls back to /tmp/xdg-runtime-. +ensure_xdg_runtime_dir() { + uid="$(id -u 2>/dev/null || echo 0)" + cand="/run/user/$uid" + + if [ ! -d "$cand" ]; then + mkdir -p "$cand" 2>/dev/null || cand="/tmp/xdg-runtime-$uid" + fi + + mkdir -p "$cand" 2>/dev/null || true + chmod 700 "$cand" 2>/dev/null || true + export XDG_RUNTIME_DIR="$cand" + + log_info "XDG_RUNTIME_DIR ensured: $XDG_RUNTIME_DIR" +} + +# Choose newest socket (by mtime); logs candidates for debugging. +wayland_pick_socket() { + best="" + best_mtime=0 + + log_info "Wayland sockets found (candidate list):" + for s in $(find_wayland_sockets | sort -u); do + mt="$(stat -c %Y "$s" 2>/dev/null || echo 0)" + log_info " - $s (mtime=$mt)" + if [ "$mt" -gt "$best_mtime" ]; then + best="$s" + best_mtime="$mt" + fi + done + + if [ -n "$best" ]; then + log_info "Picked Wayland socket (newest): $best" + echo "$best" + return 0 + fi + return 1 +} + +# ---- Wayland/Weston helpers ----------------------- +# Ensure a private XDG runtime directory exists and is usable (0700). +weston_start() { + # Already up? + if weston_is_running; then + log_info "Weston already running." + return 0 + fi + + # 1) Try systemd user/system units if present + if command -v systemctl >/dev/null 2>&1; then + for unit in weston.service weston@.service; do + log_info "Attempting to start via systemd: $unit" + systemctl start "$unit" >/dev/null 2>&1 || true + sleep 1 + if weston_is_running; then + log_info "Weston started via $unit." + return 0 + fi + done + log_warn "systemd start did not bring Weston up; will try direct spawn." + fi + + # Helper: attempt spawn for a given uid (empty => current user) + # Tries multiple backend names (to cover distro/plugin differences) + # Returns 0 if a weston process + socket appears, else non-zero. + spawn_weston_try() { + target_uid="$1" # "" or numeric uid + backends="${WESTON_BACKENDS:-headless headless-backend.so}" + + # Prepare runtime dir + if [ -n "$target_uid" ]; then + run_dir="/run/user/$target_uid" + mkdir -p "$run_dir" 2>/dev/null || true + chown "$target_uid:$target_uid" "$run_dir" 2>/dev/null || true + else + ensure_xdg_runtime_dir + run_dir="$XDG_RUNTIME_DIR" + fi + chmod 700 "$run_dir" 2>/dev/null || true + + # Where to log + log_file="/tmp/weston.${target_uid:-self}.log" + rm -f "$log_file" 2>/dev/null || true + + for be in $backends; do + log_info "Spawning weston (uid=${target_uid:-$(id -u)}) with backend='$be' …" + if ! command -v weston >/dev/null 2>&1; then + log_fail "weston binary not found in PATH." + return 1 + fi + + # Build the command: avoid optional modules that may not exist on minimal builds + cmd="XDG_RUNTIME_DIR='$run_dir' weston --backend='$be' --log='$log_file'" + + if [ -n "$target_uid" ]; then + # Run as that uid if we can + if command -v su >/dev/null 2>&1; then + su -s /bin/sh -c "$cmd >/dev/null 2>&1 &" "#$target_uid" || true + elif command -v runuser >/dev/null 2>&1; then + runuser -u "#$target_uid" -- sh -c "$cmd >/dev/null 2>&1 &" || true + else + log_warn "No su/runuser available to switch uid=$target_uid; skipping this mode." + continue + fi + else + # Current user + ( nohup sh -c "$cmd" >/dev/null 2>&1 & ) || true + fi + + # Wait up to ~5s for process + a socket to appear + tries=0 + while [ $tries -lt 5 ]; do + if weston_is_running; then + # See if a fresh socket is visible + sock="$(wayland_pick_socket)" + if [ -n "$sock" ]; then + log_info "Weston up (backend=$be). Socket: $sock" + return 0 + fi + fi + sleep 1 + tries=$((tries+1)) + done + + # Show weston log tail to aid debugging + if [ -r "$log_file" ]; then + log_warn "Weston did not come up with backend '$be'. Last log lines:" + tail -n 20 "$log_file" | sed 's/^/[weston.log] /' + else + log_warn "Weston did not come up with backend '$be' and no log file present ($log_file)." + fi + done + + return 1 + } + + # 2) Try as current user + if spawn_weston_try ""; then + return 0 + fi + + # 3) Try as 'weston' user (common on embedded images) + weston_uid="" + if command -v getent >/dev/null 2>&1; then + weston_uid="$(getent passwd weston 2>/dev/null | awk -F: '{print $3}')" + fi + [ -z "$weston_uid" ] && weston_uid="$(id -u weston 2>/dev/null || true)" + + if [ -n "$weston_uid" ]; then + log_info "Attempting to spawn Weston as uid=$weston_uid (user 'weston')." + if spawn_weston_try "$weston_uid"; then + return 0 + fi + else + log_info "No 'weston' user found; skipping user-switch spawn." + fi + + log_warn "All weston spawn attempts failed." + return 1 +} + +# Return first Wayland socket under a base dir (prints path or fails). +find_wayland_socket_in() { + base="$1" + [ -d "$base" ] || return 1 + for s in "$base"/wayland-*; do + [ -S "$s" ] || continue + printf '%s\n' "$s" + return 0 + done + return 1 +} + +# Best-effort discovery of a usable Wayland socket anywhere. +discover_wayland_socket_anywhere() { + # Prefer an already-configured, valid env + if [ -n "$XDG_RUNTIME_DIR" ] && [ -n "$WAYLAND_DISPLAY" ] && + [ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]; then + printf '%s\n' "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" + return 0 + fi + + uid="$(id -u 2>/dev/null || echo 0)" + bases="" + + # If caller set XDG_RUNTIME_DIR, keep it as highest priority + if [ -n "$XDG_RUNTIME_DIR" ]; then + bases="$bases $XDG_RUNTIME_DIR" + fi + + # Common locations on Linux/Android + bases="$bases /dev/socket/weston /run/user/$uid /tmp/wayland-$uid /dev/shm" + + for b in $bases; do + [ -d "$b" ] || continue + if s="$(find_wayland_socket_in "$b" 2>/dev/null)"; then + [ -n "$s" ] || continue + printf '%s\n' "$s" + return 0 + fi + done + + # Fallback: scan all /run/user/* (handles Weston running as a different UID, + # e.g. weston user with UID 1000 while tests run as root). + for d in /run/user/*; do + [ -d "$d" ] || continue + if s="$(find_wayland_socket_in "$d" 2>/dev/null)"; then + [ -n "$s" ] || continue + printf '%s\n' "$s" + return 0 + fi + done + + return 1 +} + +# Adopt env from a Wayland socket path like /run/user/0/wayland-0 +# Sets XDG_RUNTIME_DIR and WAYLAND_DISPLAY. Returns 0 on success. +adopt_wayland_env_from_socket() { + s="$1" + if [ -z "$s" ] || [ ! -S "$s" ]; then + log_warn "adopt_wayland_env_from_socket: invalid socket: ${s:-}" + return 1 + fi + + dir="$(dirname "$s")" + name="$(basename "$s")" + + if [ -z "$dir" ] || [ -z "$name" ]; then + log_warn "adopt_wayland_env_from_socket: could not derive env from '$s'" + return 1 + fi + + XDG_RUNTIME_DIR="$dir" + WAYLAND_DISPLAY="$name" + export XDG_RUNTIME_DIR WAYLAND_DISPLAY + + # Best-effort perms fix for minimal systems (ignore errors) + chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null || true + + log_info "Adopting Wayland environment from socket: $s" + log_info "Adopted Wayland env: XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR WAYLAND_DISPLAY=$WAYLAND_DISPLAY" + log_info "Reproduce with:" + log_info " export XDG_RUNTIME_DIR='$XDG_RUNTIME_DIR'" + log_info " export WAYLAND_DISPLAY='$WAYLAND_DISPLAY'" +} + +# Try to connect to Wayland. Returns 0 on OK. +wayland_can_connect() { + if command -v weston-info >/dev/null 2>&1; then + weston-info >/dev/null 2>&1 + return $? + fi + # fallback: quick client probe + ( env -i XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" WAYLAND_DISPLAY="$WAYLAND_DISPLAY" true ) >/dev/null 2>&1 + return $? +} + +# Ensure a Weston socket exists; if not, stop+start Weston and adopt helper socket. +weston_pick_env_or_start() { + ctx="${1:-weston_pick_env_or_start}" + sock="" + + # Honor WESTON_LOG_DIR for any Weston logs that helpers might write + log_dir="${WESTON_LOG_DIR:-/tmp}" + log_info "$ctx: Weston logs (if any) will be under: $log_dir" + + # 0) If env already points to a valid socket, keep it. + if [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -n "${WAYLAND_DISPLAY:-}" ] \ + && [ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]; then + log_info "$ctx: Using existing Wayland env: $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" + return 0 + fi + + # 1) Try to discover any existing Wayland socket first. + if command -v discover_wayland_socket_anywhere >/dev/null 2>&1; then + sock="$(discover_wayland_socket_anywhere 2>/dev/null | head -n 1)" + elif command -v find_wayland_socket_in >/dev/null 2>&1; then + uid="$(id -u 2>/dev/null || echo 0)" + bases="" + [ -n "${XDG_RUNTIME_DIR:-}" ] && bases="$bases $XDG_RUNTIME_DIR" + bases="$bases /run/user/$uid /dev/socket/weston /tmp/wayland-$uid" + for b in $bases; do + [ -z "$b" ] && continue + if s="$(find_wayland_socket_in "$b" 2>/dev/null || true)"; then + if [ -n "$s" ]; then + sock="$s" + break + fi + fi + done + fi + + if [ -n "$sock" ]; then + if adopt_wayland_env_from_socket "$sock"; then + log_info "$ctx: Selected existing Wayland socket: $sock" + return 0 + fi + log_warn "$ctx: Failed to adopt env from existing socket: $sock" + return 1 + fi + + # 2) No socket found → restart Weston and wait for a new one. + if weston_is_running; then + log_info "$ctx: Weston is running but no socket found; stopping..." + weston_stop + i=0 + while weston_is_running && [ "$i" -lt 5 ]; do + i=$((i + 1)) + sleep 1 + done + fi + + uid="$(id -u 2>/dev/null || echo 0)" + + # Ensure XDG_RUNTIME_DIR before starting Weston (manual mkdir, no external helpers). + if [ -z "${XDG_RUNTIME_DIR:-}" ]; then + if mkdir -p "/run/user/$uid" 2>/dev/null; then + chmod 700 "/run/user/$uid" 2>/dev/null || true + XDG_RUNTIME_DIR="/run/user/$uid" + elif mkdir -p "/dev/socket/weston" 2>/dev/null; then + chmod 700 "/dev/socket/weston" 2>/dev/null || true + XDG_RUNTIME_DIR="/dev/socket/weston" + fi + export XDG_RUNTIME_DIR + log_info "$ctx: XDG_RUNTIME_DIR set to '$XDG_RUNTIME_DIR' before starting Weston" + else + mkdir -p "$XDG_RUNTIME_DIR" 2>/dev/null || true + chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null || true + log_info "$ctx: Using existing XDG_RUNTIME_DIR='$XDG_RUNTIME_DIR' before starting Weston" + fi + + # Never pre-set WAYLAND_DISPLAY; let Weston choose. + unset WAYLAND_DISPLAY + + log_info "$ctx: Starting Weston..." + weston_start + + # 3) Wait up to ~10 seconds for any Wayland socket to appear. + i=0 + sock="" + while [ "$i" -lt 10 ]; do + if command -v discover_wayland_socket_anywhere >/dev/null 2>&1; then + sock="$(discover_wayland_socket_anywhere 2>/dev/null | head -n 1)" + elif [ -n "${XDG_RUNTIME_DIR:-}" ] && command -v find_wayland_socket_in >/devnull 2>&1; then + sock="$(find_wayland_socket_in "$XDG_RUNTIME_DIR" 2>/dev/null || true)" + fi + if [ -n "$sock" ]; then + break + fi + sleep 1 + i=$((i + 1)) + done + + if [ -z "$sock" ]; then + log_fail "$ctx: Could not find Wayland socket after starting Weston." + return 1 + fi + + if ! adopt_wayland_env_from_socket "$sock"; then + log_fail "$ctx: Failed to adopt env from socket: $sock" + return 1 + fi + + log_info "$ctx: Weston started; socket: $sock" + return 0 +} + +# Find candidate Wayland sockets in common locations. +# Prints absolute socket paths, one per line, most-preferred first. +find_wayland_sockets() { + uid="$(id -u 2>/dev/null || echo 0)" + + if [ -n "${XDG_RUNTIME_DIR:-}" ] && [ -n "${WAYLAND_DISPLAY:-}" ] && + [ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]; then + echo "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" + fi + + # Current uid + for f in "/run/user/$uid/wayland-0" "/run/user/$uid/wayland-1" "/run/user/$uid/wayland-2"; do + [ -S "$f" ] && echo "$f" + done + for f in /run/user/"$uid"/wayland-*; do + [ -S "$f" ] && echo "$f" + done 2>/dev/null + + # Any other user under /run/user (covers weston as uid 100, 1000, etc.) + for d in /run/user/*; do + [ -d "$d" ] || continue + [ "$d" = "/run/user/$uid" ] && continue # skip current uid, already handled above + for f in "$d"/wayland-*; do + [ -S "$f" ] && echo "$f" + done + done 2>/dev/null + + for f in /dev/socket/weston/wayland-*; do + [ -S "$f" ] && echo "$f" + done 2>/dev/null + + for f in /tmp/wayland-*; do + [ -S "$f" ] && echo "$f" + done 2>/dev/null +} + +# Ensure XDG_RUNTIME_DIR has owner=current-user and mode 0700. +# Returns 0 if OK (or fixed), non-zero if still not compliant. +ensure_wayland_runtime_dir_perms() { + dir="$1" + [ -n "$dir" ] && [ -d "$dir" ] || return 1 + + cur_uid="$(id -u 2>/dev/null || echo 0)" + cur_gid="$(id -g 2>/dev/null || echo 0)" + + # Best-effort fixups first (don’t error if chown/chmod fail) + chown "$cur_uid:$cur_gid" "$dir" 2>/dev/null || true + chmod 0700 "$dir" 2>/dev/null || true + + # Verify using stat (GNU first, then BSD). If stat is unavailable, + # we can’t verify—assume OK to avoid SC2012 (ls) usage. + if command -v stat >/dev/null 2>&1; then + # Mode: GNU: %a ; BSD: %Lp + mode="$(stat -c '%a' "$dir" 2>/dev/null || stat -f '%Lp' "$dir" 2>/dev/null || echo '')" + # Owner uid: GNU: %u ; BSD: %u + uid="$(stat -c '%u' "$dir" 2>/dev/null || stat -f '%u' "$dir" 2>/dev/null || echo '')" + + [ "$mode" = "700" ] && [ "$uid" = "$cur_uid" ] && return 0 + return 1 + fi + + # No stat available: directory exists and we attempted to fix perms/owner. + # Treat as success so clients can try; avoids SC2012 warnings. + return 0 +} + +# Quick Wayland handshake check. +# Prefers `wayland-info` with a short timeout; otherwise validates socket presence. +# Also enforces/fixes XDG_RUNTIME_DIR permissions so clients won’t reject it. +wayland_connection_ok() { + sock="" + + # Sanity-check the socket path first. + if [ -z "$XDG_RUNTIME_DIR" ] || [ -z "$WAYLAND_DISPLAY" ]; then + log_warn "wayland_connection_ok: XDG_RUNTIME_DIR or WAYLAND_DISPLAY not set" + else + sock="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" + if [ ! -S "$sock" ]; then + log_warn "wayland_connection_ok: no Wayland socket at $sock" + return 1 + fi + log_info "wayland_connection_ok: using socket $sock" + fi + + if command -v wayland-info >/dev/null 2>&1; then + log_info "Probing Wayland with: wayland-info" + wayland-info >/dev/null 2>&1 + rc=$? + [ "$rc" -eq 0 ] && return 0 + # Accept common “killed by timeout/signal” cases as OK (best-effort probe) + [ "$rc" -eq 143 ] && return 0 + [ "$rc" -eq 124 ] && return 0 + return 1 + fi + + if command -v weston-info >/dev/null 2>&1; then + log_info "Probing Wayland with: weston-info" + weston-info >/dev/null 2>&1 + rc=$? + [ "$rc" -eq 0 ] && return 0 + [ "$rc" -eq 143 ] && return 0 + [ "$rc" -eq 124 ] && return 0 + return 1 + fi + + if command -v weston-simple-egl >/dev/null 2>&1; then + log_info "Probing Wayland by briefly starting weston-simple-egl" + ( + weston-simple-egl >/dev/null 2>&1 & + echo "$!" >"/tmp/.wsegl.$$" + ) + pid="$(cat "/tmp/.wsegl.$$" 2>/dev/null || echo '')" + rm -f "/tmp/.wsegl.$$" 2>/dev/null || true + + i=0 + while [ "$i" -lt 2 ]; do + sleep 1 + i=$((i + 1)) + done + + if [ -n "$pid" ]; then + kill "$pid" 2>/dev/null || true + fi + # If it started at all, consider the connection OK (best effort). + return 0 + fi + + # Last resort: trust socket existence alone. + if [ -n "$sock" ] && [ -S "$sock" ]; then + log_info "No probe tools present; accepting socket existence as OK." + return 0 + fi + + return 1 +} +# Very verbose snapshot for debugging (processes, sockets, env, perms). +wayland_debug_snapshot() { + label="$1" + [ -n "$label" ] || label="snapshot" + log_info "----- Wayland/Weston debug snapshot: $label -----" + + # Processes + wpids="$(weston_pids)" + if [ -n "$wpids" ]; then + log_info "weston PIDs: $wpids" + for p in $wpids; do + if command -v ps >/dev/null 2>&1; then + ps -o pid,user,group,cmd -p "$p" 2>/dev/null | sed 's/^/[ps] /' || true + fi + if [ -r "/proc/$p/cmdline" ]; then + tr '\0' ' ' <"/proc/$p/cmdline" 2>/dev/null | sed 's/^/[cmdline] /' || true + fi + done + else + log_info "weston PIDs: (none)" + fi + + # Sockets (meta) — use stat instead of ls (SC2012) + for s in $(find_wayland_sockets | sort -u); do + log_info "socket: $s" + stat -c '[stat] %n -> owner=%U:%G mode=%A size=%s mtime=%y' "$s" 2>/dev/null || true + done + + # Current env + log_info "Env now: XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-} WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-}" + if [ -n "${XDG_RUNTIME_DIR:-}" ]; then + stat -c '[stat] %n -> owner=%U:%G mode=%A size=%s mtime=%y' "$XDG_RUNTIME_DIR" 2>/dev/null || true + fi + + log_info "Suggested export (current env):" + log_info " export XDG_RUNTIME_DIR='${XDG_RUNTIME_DIR:-}'" + log_info " export WAYLAND_DISPLAY='${WAYLAND_DISPLAY:-}'" + + log_info "----- End snapshot: $label -----" +} + +# Print concise metadata for a path (portable). +# Prefers stat(1) (GNU or BSD); falls back to ls(1) only if needed. +# Usage: print_path_meta "/some/path" +print_path_meta() { + p=$1 + if [ -z "$p" ]; then + return 1 + fi + # GNU stat + if stat -c '%A %U %G %a %n' "$p" >/dev/null 2>&1; then + stat -c '%A %U %G %a %n' "$p" + return 0 + fi + # BSD/Mac stat + if stat -f '%Sp %Su %Sg %OLp %N' "$p" >/dev/null 2>&1; then + stat -f '%Sp %Su %Sg %OLp %N' "$p" + return 0 + fi + # shellcheck disable=SC2012 + ls -ld -- "$p" 2>/dev/null +} + +############################################################################### +# DRM / Display helpers (portable, minimal-build friendly) +############################################################################### +# Best-effort: return current mode for a DRM connector using debugfs state. +# Input : full connector name like "card0-HDMI-A-1" +# Output: "1920x1080@60.00" (or "1920x1080@60") or "-" if unknown/unavailable +display_connector_cur_mode() { + full="$1" + [ -n "$full" ] || { echo "-"; return; } + + card="${full%%-*}" # card0 + con="${full#*-}" # HDMI-A-1 + idx="${card#card}" # 0 + + state="/sys/kernel/debug/dri/${idx}/state" + + # debugfs may not be mounted; best-effort mount (ignore failures) + if [ ! -r "$state" ] && [ -r /proc/mounts ] && command -v mount >/dev/null 2>&1; then + if ! grep -q " /sys/kernel/debug " /proc/mounts 2>/dev/null; then + mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null || true + fi + fi + + [ -r "$state" ] || { echo "-"; return; } + + awk -v CON="$con" ' + BEGIN { crtc=""; in_conn=0; in_crtc=0; mode=""; vr=""; active="" } + + # Connector block -> capture bound CRTC id + $0 ~ /^connector/ { + in_conn=0 + if ($0 ~ CON) in_conn=1 + } + in_conn && $0 ~ /crtc/ { + if (match($0, /[0-9]+/, m)) crtc=m[0] + } + in_conn && $0 ~ /^$/ { in_conn=0 } + + # CRTC block -> capture active + mode + refresh + crtc != "" && $0 ~ /^crtc/ { + in_crtc=0 + if ($0 ~ ("crtc " crtc)) in_crtc=1 + } + in_crtc && $0 ~ /active/ { + if ($0 ~ /(yes|true|1)/) active="1" + } + in_crtc && $0 ~ /mode/ { + if (match($0, /[0-9]+x[0-9]+/, m)) mode=m[0] + } + in_crtc && ($0 ~ /vrefresh/ || $0 ~ /refresh/) { + if (match($0, /[0-9]+(\.[0-9]+)?/, m)) vr=m[0] + } + in_crtc && $0 ~ /^$/ { in_crtc=0 } + + END { + if (crtc == "" || mode == "" || active != "1") { print "-"; exit } + if (vr != "") print mode "@" vr + else print mode + }' "$state" 2>/dev/null || echo "-" +} + +# Echo lines: "\t\t\t\t" +# Example: "card0-HDMI-A-1 connected HDMI-A 9 1920x1080" +display_list_connectors() { + found=0 + for d in /sys/class/drm/*-*; do + [ -e "$d" ] || continue + [ -f "$d/status" ] || continue + name="$(basename "$d")" + status="$(tr -d '\r\n' <"$d/status" 2>/dev/null)" + + # enabled (best-effort; may not exist on some kernels) + enabled="unknown" + if [ -f "$d/enabled" ]; then + enabled="$(tr -d '\r\n' <"$d/enabled" 2>/dev/null)" + [ -z "$enabled" ] && enabled="unknown" + fi + + # Derive connector type from name: cardX--N + typ="$(printf '%s' "$name" \ + | sed -n 's/^card[0-9]\+-\([A-Za-z0-9+]\+\(-[A-Za-z0-9+]\+\)*\)-[0-9]\+/\1/p')" + [ -z "$typ" ] && typ="unknown" + + # Modes + modes_file="$d/modes" + if [ -f "$modes_file" ]; then + mc="$(wc -l <"$modes_file" 2>/dev/null | tr -d '[:space:]')" + [ -z "$mc" ] && mc=0 + fm="$(head -n 1 "$modes_file" 2>/dev/null | tr -d '\r\n')" + [ -z "$fm" ] && fm="" + else + mc=0 + fm="" + fi + + # Current mode (best-effort via debugfs helper) + cur="$(display_connector_cur_mode "$name")" + [ -z "$cur" ] && cur="-" + + # NOTE: 7 columns: name status enabled typ mc fm cur + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$name" "$status" "$enabled" "$typ" "$mc" "$fm" "$cur" + found=1 + done + [ "$found" -eq 1 ] || return 1 + return 0 +} +# Return 0 if any connector is connected; else 1 +display_any_attached() { + for d in /sys/class/drm/*-*; do + [ -f "$d/status" ] || continue + st="$(tr -d '\r\n' <"$d/status" 2>/dev/null)" + if [ "$st" = "connected" ]; then + return 0 + fi + done + return 1 +} + +# Print one compact human line summarizing connected outputs +display_connected_summary() { + have=0 + line="" + # shellcheck disable=SC2039 + while IFS="$(printf '\t')" read -r name status typ mc fm; do + [ "$status" = "connected" ] || continue + have=1 + if [ -n "$fm" ]; then + seg="${name}(${typ},${fm})" + else + seg="${name}(${typ})" + fi + if [ -z "$line" ]; then line="$seg"; else line="$line, $seg"; fi + done </dev/null || true) +EOF + if [ "$have" -eq 1 ]; then + echo "$line" + return 0 + fi + echo "none" + return 1 +} + +# Best-effort "primary" guess: first connected with a mode; else first connected; echoes name +display_primary_guess() { + best="" + # Prefer one with modes + # shellcheck disable=SC2039 + while IFS="$(printf '\t')" read -r name status typ mc fm; do + [ "$status" = "connected" ] || continue + if [ -n "$fm" ]; then echo "$name"; return 0; fi + [ -z "$best" ] && best="$name" + done </dev/null || true) +EOF + [ -n "$best" ] && { echo "$best"; return 0; } + return 1 +} + +# Optional enrichment via weston-info (if available) +# Prints lines: "weston: model= make= phys=mm" +display_weston_outputs() { + if ! command -v weston-info >/dev/null 2>&1; then + return 0 + fi + # Very light parse; tolerate different locales + weston-info 2>/dev/null \ + | awk ' + $1=="output" && $2~/^[0-9]+:$/ {out=$2; sub(":","",out)} + /make:/ {make=$2} + /model:/ {model=$2} + /physical size:/ {w=$3; h=$5; sub("mm","",h)} + /scale:/ { + if (out!="") { + printf("weston: %s make=%s model=%s phys=%sx%sm\n", out, make, model, w, h); + out=""; make=""; model=""; w=""; h=""; + } + } + ' + return 0 +} + +# One-stop debug snapshot +display_debug_snapshot() { + ctx="$1" + [ -z "$ctx" ] && ctx="display-snapshot" + log_info "----- Display snapshot: $ctx -----" + + have=0 + while IFS="$(printf '\t')" read -r name status enabled typ mc fm cur; do + [ -n "$name" ] || continue + have=1 + [ -z "$fm" ] && fm="" + [ -z "$cur" ] && cur="-" + log_info "DRM: ${name} status=${status} enabled=${enabled} type=${typ} modes=${mc} first=${fm} cur=${cur}" + done </dev/null || true) +EOF + [ "$have" -eq 1 ] || log_warn "No DRM connectors in /sys/class/drm." + + log_info "----- End display snapshot: $ctx -----" +} + +# Returns true (0) if interface is administratively and physically up +is_interface_up() { + iface="$1" + if [ -f "/sys/class/net/$iface/operstate" ]; then + [ "$(cat "/sys/class/net/$iface/operstate")" = "up" ] + elif command -v ip >/dev/null 2>&1; then + ip link show "$iface" 2>/dev/null | grep -qw "state UP" + elif command -v ifconfig >/dev/null 2>&1; then + ifconfig "$iface" 2>/dev/null | grep -qw "UP" + else + return 1 + fi +} + +# Returns true (0) if physical link/carrier is detected (cable plugged in) +is_link_up() { + iface="$1" + [ -f "/sys/class/net/$iface/carrier" ] && [ "$(cat "/sys/class/net/$iface/carrier")" = "1" ] +} + +# Returns true (0) if interface is Ethernet type (type 1 in sysfs) +is_ethernet_interface() { + iface="$1" + [ -f "/sys/class/net/$iface/type" ] && [ "$(cat "/sys/class/net/$iface/type")" = "1" ] +} + +# Get all Ethernet interfaces (excluding common virtual types) +get_ethernet_interfaces() { + for path in /sys/class/net/*; do + iface=$(basename "$path") + case "$iface" in + lo|docker*|br-*|veth*|virbr*|tap*|tun*|wl*) continue ;; + esac + if is_ethernet_interface "$iface"; then + echo "$iface" + fi + done +} + +# Bring up interface with retries (down before up). +bringup_interface() { + iface="$1"; retries="${2:-3}"; sleep_sec="${3:-2}"; i=0 + while [ $i -lt "$retries" ]; do + if command -v ip >/dev/null 2>&1; then + ip link set "$iface" down + sleep 1 + ip link set "$iface" up + sleep "$sleep_sec" + ip link show "$iface" | grep -q "state UP" && return 0 + elif command -v ifconfig >/dev/null 2>&1; then + ifconfig "$iface" down + sleep 1 + ifconfig "$iface" up + sleep "$sleep_sec" + ifconfig "$iface" | grep -q "UP" && return 0 + fi + i=$((i + 1)) + done + return 1 +} + +# Wait for a valid IPv4 address on the given interface, up to a timeout (default 30s) +wait_for_ip_address() { + iface="$1" + timeout="${2:-30}" + elapsed=0 + while [ "$elapsed" -lt "$timeout" ]; do + ip_addr=$(get_ip_address "$iface") + if [ -n "$ip_addr" ]; then + if echo "$ip_addr" | grep -q '^169\.254'; then + echo "$ip_addr" + return 2 + fi + echo "$ip_addr" + return 0 + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + return 1 +} + +# Get the IPv4 address for a given interface. +get_ip_address() { + iface="$1" + if command -v ip >/dev/null 2>&1; then + ip -4 -o addr show "$iface" | awk '{print $4}' | cut -d/ -f1 | head -n1 + elif command -v ifconfig >/dev/null 2>&1; then + ifconfig "$iface" 2>/dev/null | awk '/inet / {print $2}' | head -n1 + fi +} + +# Run a command with a timeout (in seconds) +run_with_timeout() { + timeout="$1"; shift + ( "$@" ) & + pid=$! + ( sleep "$timeout"; kill "$pid" 2>/dev/null ) & + watcher=$! + wait $pid 2>/dev/null + status=$? + kill $watcher 2>/dev/null + return $status +} + +# Only apply a timeout if TIMEOUT is set; prefer `timeout`; avoid functestlib here +runWithTimeoutIfSet() { + # Normalize TIMEOUT: treat empty or non-numeric as 0 + t="${TIMEOUT:-}" + case "$t" in + ''|*[!0-9]*) + t=0 + ;; + esac + + if [ "$t" -gt 0 ] && command -v run_with_timeout >/dev/null 2>&1; then + # Correct signature: run_with_timeout [args...] + run_with_timeout "$t" "$@" + else + # No timeout -> run command directly + "$@" + fi +} + +# DHCP client logic (dhclient and udhcpc with timeouts) +run_dhcp_client() { + iface="$1" + timeout="${2:-10}" + ip_addr="" + log_info "Attempting DHCP on $iface (timeout ${timeout}s)..." + if command -v dhclient >/dev/null 2>&1; then + log_info "Trying dhclient for $iface" + run_with_timeout "$timeout" dhclient "$iface" + ip_addr=$(wait_for_ip_address "$iface" 5) + if [ -n "$ip_addr" ]; then + echo "$ip_addr" + return 0 + fi + fi + if command -v udhcpc >/dev/null 2>&1; then + log_info "Trying udhcpc for $iface" + run_with_timeout "$timeout" udhcpc -i "$iface" -T 3 -t 3 + ip_addr=$(wait_for_ip_address "$iface" 5) + if [ -n "$ip_addr" ]; then + echo "$ip_addr" + return 0 + fi + fi + log_warn "DHCP failed for $iface" + return 1 +} + +# Safely run DHCP client without disrupting existing config +try_dhcp_client_safe() { + iface=$1 + timeout=${2:-10} + + [ -n "$iface" ] || return 1 + + # Debug breadcrumb: track which path we took + dhcp_path="unknown" + dhcp_note="" + + # Ensure interface is up (best-effort) + ip link set "$iface" up >/dev/null 2>&1 || true + + # If link is down/no cable, don't try DHCP + if command -v is_link_up >/dev/null 2>&1; then + if ! is_link_up "$iface"; then + log_warn "$iface link is down; skipping DHCP" + return 1 + fi + elif command -v ethIsLinkUp >/dev/null 2>&1; then + if ! ethIsLinkUp "$iface"; then + log_warn "$iface link is down; skipping DHCP" + return 1 + fi + fi + + current_ip=$(get_ip_address "$iface") + if [ -n "$current_ip" ] && ! echo "$current_ip" | grep -q '^169\.254'; then + dhcp_path="skip_existing_ip" + log_info "$iface DHCP path: ${dhcp_path} (ip=${current_ip})" + log_info "$iface already has valid IP: $current_ip. Skipping DHCP." + return 0 + fi + + # If NetworkManager/systemd-networkd is active, avoid fighting it: + # just wait for IP to appear. + if command -v systemctl >/dev/null 2>&1; then + if systemctl is-active --quiet NetworkManager 2>/dev/null \ + || systemctl is-active --quiet systemd-networkd 2>/dev/null; then + dhcp_path="systemd_nm_wait" + log_info "$iface DHCP path: ${dhcp_path} (timeout=${timeout}s)" + log_info "Network manager detected; waiting up to ${timeout}s for IP on $iface..." + i=0 + while [ "$i" -lt "$timeout" ]; do + current_ip=$(get_ip_address "$iface") + if [ -n "$current_ip" ] && ! echo "$current_ip" | grep -q '^169\.254'; then + log_info "$iface obtained IP via network manager: $current_ip" + log_info "$iface DHCP path: ${dhcp_path} success (ip=${current_ip})" + return 0 + fi + i=$((i + 1)) + sleep 1 + done + log_warn "$iface did not obtain a valid IP within ${timeout}s" + log_warn "$iface DHCP path: ${dhcp_path} failed (no valid IP within ${timeout}s)" + return 1 + fi + fi + + # Minimal/kernel-only builds might not have udhcpc. Try other clients first if needed. + if ! command -v udhcpc >/dev/null 2>&1; then + log_warn "udhcpc not found (kernel/minimal build). Trying fallback DHCP clients..." + dhcp_path="fallback_clients" + log_info "$iface DHCP path: ${dhcp_path} (trying dhclient/dhcpcd)" + + if command -v dhclient >/dev/null 2>&1; then + dhcp_note="dhclient" + log_info "$iface DHCP path: ${dhcp_path}/${dhcp_note} (timeout=${timeout}s)" + # Best-effort: try bounded dhclient + if command -v run_with_timeout >/dev/null 2>&1; then + run_with_timeout "${timeout}s" dhclient -1 -v "$iface" >/dev/null 2>&1 || true + else + dhclient -1 -v "$iface" >/dev/null 2>&1 || true + fi + + current_ip=$(get_ip_address "$iface") + if [ -n "$current_ip" ] && ! echo "$current_ip" | grep -q '^169\.254'; then + log_info "$iface DHCP path: ${dhcp_path}/${dhcp_note} success (ip=${current_ip})" + return 0 + fi + fi + + if command -v dhcpcd >/dev/null 2>&1; then + dhcp_note="dhcpcd" + log_info "$iface DHCP path: ${dhcp_path}/${dhcp_note} (timeout=${timeout}s)" + # dhcpcd: try a bounded attempt and then stop + if command -v run_with_timeout >/dev/null 2>&1; then + run_with_timeout "${timeout}s" dhcpcd -4 -t "$timeout" "$iface" >/dev/null 2>&1 || true + else + dhcpcd -4 -t "$timeout" "$iface" >/dev/null 2>&1 || true + fi + # Avoid leaving daemon running if it spawned + dhcpcd -k "$iface" >/dev/null 2>&1 || true + + current_ip=$(get_ip_address "$iface") + if [ -n "$current_ip" ] && ! echo "$current_ip" | grep -q '^169\.254'; then + log_info "$iface DHCP path: ${dhcp_path}/${dhcp_note} success (ip=${current_ip})" + return 0 + fi + fi + + log_warn "$iface DHCP path: ${dhcp_path} failed (no client succeeded)" + return 1 + fi + + # Safe udhcpc script: + # - Avoid aggressive flushes + # - Remove only link-local (169.254/16) if present + # - Apply lease IP/route + safe_dhcp_script=$(mktemp /tmp/udhcpc-safe-"$iface".XXXXXX 2>/dev/null || echo "/tmp/udhcpc-safe-$iface-$$.sh") + cat <<'EOF' > "$safe_dhcp_script" +#!/bin/sh +# udhcpc env: interface ip subnet mask router dns ... +mask_to_prefix() { + m="$1" + case "$m" in + *.*.*.*) + echo "$m" | awk -F. ' + function bits8(n, b) { + b=0 + while (n>0) { b += (n%2); n=int(n/2) } + return b + } + { + p=0 + for (i=1; i<=4; i++) { + n=$i+0 + # count bits set in each octet (valid for contiguous netmasks) + if (n==255) p+=8 + else if (n==254) p+=7 + else if (n==252) p+=6 + else if (n==248) p+=5 + else if (n==240) p+=4 + else if (n==224) p+=3 + else if (n==192) p+=2 + else if (n==128) p+=1 + else if (n==0) p+=0 + else { p="" ; break } + } + if (p!="") print p + }' + ;; + *) + case "$m" in + ''|*[!0-9]*) echo "" ;; + *) echo "$m" ;; + esac + ;; + esac +} + +case "$1" in + bound|renew) + # Drop link-local if present + ip -4 addr show dev "$interface" 2>/dev/null | awk '/inet 169\.254\./{print $2}' \ + | while IFS= read -r cidr; do + [ -n "$cidr" ] || continue + ip addr del "$cidr" dev "$interface" >/dev/null 2>&1 || true + done + + # Add/replace address (best-effort) + if [ -n "$ip" ]; then + pfx="" + if [ -n "$mask" ]; then + pfx=$(mask_to_prefix "$mask" 2>/dev/null) + fi + if [ -n "$pfx" ]; then + ip addr replace "$ip/$pfx" dev "$interface" >/dev/null 2>&1 || true + else + ip addr add "$ip" dev "$interface" >/dev/null 2>&1 || true + fi + fi + + # Default route + if [ -n "$router" ]; then + ip route replace default via "$router" dev "$interface" >/dev/null 2>&1 || true + fi + exit 0 + ;; + deconfig) + # Do nothing (avoid flushing) + exit 0 + ;; + *) + exit 0 + ;; +esac +EOF + chmod +x "$safe_dhcp_script" + + dhcp_path="udhcpc_safe_script" + log_info "$iface DHCP path: ${dhcp_path} (timeout=${timeout}s)" + log_info "Attempting DHCP on $iface (udhcpc) for up to ${timeout}s..." + if command -v run_with_timeout >/dev/null 2>&1; then + run_with_timeout "${timeout}s" udhcpc -i "$iface" -n -q -s "$safe_dhcp_script" >/dev/null 2>&1 || true + else + # Best-effort bounded retries if run_with_timeout isn't available + udhcpc -i "$iface" -n -q -t 3 -T 3 -s "$safe_dhcp_script" >/dev/null 2>&1 || true + fi + + rm -f "$safe_dhcp_script" >/dev/null 2>&1 || true + + # Verify outcome + current_ip=$(get_ip_address "$iface") + if [ -n "$current_ip" ] && ! echo "$current_ip" | grep -q '^169\.254'; then + log_info "$iface obtained IP after DHCP: $current_ip" + log_info "$iface DHCP path: ${dhcp_path} success (ip=${current_ip})" + return 0 + fi + + log_warn "$iface still has no valid IP after DHCP attempt" + log_warn "$iface DHCP path: ${dhcp_path} failed (no valid IP after attempt)" + return 1 +} + +ethLinkDetected() { + iface=$1 + [ -n "$iface" ] || return 1 + + command -v ethtool >/dev/null 2>&1 || return 1 + # Prints: yes/no (or nothing on parse failure) + ethtool "$iface" 2>/dev/null \ + | awk -F': ' '/^[[:space:]]*Link detected:/ {print $2; exit 0}' +} + +ethIsLinkUp() { + iface=$1 + [ -n "$iface" ] || return 1 + + # 1) If carrier says 1, we are good (fast path) + if [ -r "/sys/class/net/$iface/carrier" ]; then + [ "$(cat "/sys/class/net/$iface/carrier" 2>/dev/null)" = "1" ] && return 0 + # If carrier is 0, do NOT return yet — fall through to other hints. + fi + + # 2) If helper exists and says up, accept it (but don't fail early) + if command -v is_link_up >/dev/null 2>&1; then + is_link_up "$iface" && return 0 + fi + + # 3) operstate can sometimes reflect link sooner than carrier in some stacks + if [ -r "/sys/class/net/$iface/operstate" ]; then + st=$(cat "/sys/class/net/$iface/operstate" 2>/dev/null || true) + [ "$st" = "up" ] && return 0 + fi + + # 4) ip link LOWER_UP (physical) is a good signal if ip exists + if command -v ip >/dev/null 2>&1; then + ip link show "$iface" 2>/dev/null | grep -qw "LOWER_UP" && return 0 + fi + + # 5) Last resort: ethtool parse + if command -v ethtool >/dev/null 2>&1; then + ld=$(ethtool "$iface" 2>/dev/null | awk -F': ' '/^[[:space:]]*Link detected:/ {print $2; exit 0}' || true) + [ "$ld" = "yes" ] && return 0 + fi + + return 1 +} + +ethWaitLinkUp() { + iface=$1 + timeout_s=$2 + i=0 + + [ -n "$iface" ] || return 1 + [ -n "$timeout_s" ] || timeout_s=5 + + while [ "$i" -lt "$timeout_s" ]; do + if ethIsLinkUp "$iface"; then + return 0 + fi + i=$((i + 1)) + sleep 1 + done + + return 1 +} + +ethGetLinkSpeedMbps() { + iface=$1 + sp="" + + [ -n "$iface" ] || return 1 + + # Only meaningful when link is up + if ! ethIsLinkUp "$iface"; then + return 1 + fi + + # Prefer sysfs (but can be -1 even when link is up) + if [ -r "/sys/class/net/$iface/speed" ]; then + sp=$(cat "/sys/class/net/$iface/speed" 2>/dev/null || true) + case "$sp" in ""|-1|*[!0-9]*) sp="" ;; esac + if [ -n "$sp" ] && [ "$sp" -gt 0 ] 2>/dev/null; then + printf '%s\n' "$sp" + return 0 + fi + fi + + # Fallback: ethtool parse ("100Mb/s", "2500Mb/s", etc.) + if command -v ethtool >/dev/null 2>&1; then + sp=$(ethtool "$iface" 2>/dev/null \ + | awk -F': ' '/^[[:space:]]*Speed:/ {print $2; exit 0}') + sp=$(printf '%s\n' "$sp" | sed -n 's/^\([0-9][0-9]*\)Mb\/s.*/\1/p') + case "$sp" in ""|*[!0-9]*) sp="" ;; esac + if [ -n "$sp" ]; then + printf '%s\n' "$sp" + return 0 + fi + fi + + return 1 +} + +ethRestartAutoneg() { + iface=$1 + [ -n "$iface" ] || return 1 + command -v ethtool >/dev/null 2>&1 || return 1 + + # Restart autoneg/link training (if supported by driver) + ethtool -r "$iface" >/dev/null 2>&1 || return 1 + return 0 +} + +ethForceSpeedMbps() { + iface=$1 + mbps=$2 + + [ -n "$iface" ] || return 1 + command -v ethtool >/dev/null 2>&1 || return 1 + case "$mbps" in ""|*[!0-9]*) return 1 ;; esac + + ethtool -s "$iface" speed "$mbps" duplex full autoneg off >/dev/null 2>&1 || return 1 + return 0 +} + +ethEnableAutoneg() { + iface=$1 + + [ -n "$iface" ] || return 1 + command -v ethtool >/dev/null 2>&1 || return 1 + + ethtool -s "$iface" autoneg on >/dev/null 2>&1 || return 1 + return 0 +} + +ethSupportsSpeedMbps() { + iface=$1 + mbps=$2 + + [ -n "$iface" ] || return 1 + case "$mbps" in ""|*[!0-9]*) return 1 ;; esac + command -v ethtool >/dev/null 2>&1 || return 1 + + ethtool "$iface" 2>/dev/null | grep -Eq "[[:space:]]${mbps}baseT/Full|[[:space:]]${mbps}baseT1/Full|[[:space:]]${mbps}baseX/Full" +} + +# Returns: on/off/unknown (best-effort) +ethGetAutonegState() { + iface=$1 + [ -n "$iface" ] || return 1 + command -v ethtool >/dev/null 2>&1 || { echo "unknown"; return 0; } + + st=$(ethtool "$iface" 2>/dev/null | awk -F': ' '/^[[:space:]]*Auto-negotiation:/ {print $2; exit 0}' || true) + case "$st" in + on|off) echo "$st" ;; + *) echo "unknown" ;; + esac + return 0 +} + +# Heuristic: decide force order for 100M-only/locked ports +# Echoes a space-separated list like: "1000 100" or "100 1000" +ethPickForceOrder() { + iface=$1 + [ -n "$iface" ] || { echo "1000 100"; return 0; } + command -v ethtool >/dev/null 2>&1 || { echo "1000 100"; return 0; } + + # Pull supported/advertised modes blocks (best-effort) + modes=$(ethtool "$iface" 2>/dev/null | awk ' + BEGIN{cap=0} + /^[[:space:]]*Supported link modes:/ {cap=1; print; next} + /^[[:space:]]*Advertised link modes:/ {cap=1; print; next} + cap==1 && /^[[:space:]]+[0-9]/ {print; next} + cap==1 && /^[^[:space:]]/ {cap=0} + ' || true) + + # Default + order="1000 100" + + # If we see no gig/2.5g/10g capability mentioned at all, prefer trying 100 first. + if [ -n "$modes" ]; then + if ! printf '%s\n' "$modes" | grep -Eq '1000baseT|2500baseT|5000baseT|10000baseT'; then + order="100 1000" + fi + fi + + echo "$order" + return 0 +} + +ethEnsureLinkUpWithFallback() { + iface=$1 + timeout_s=$2 + [ -n "$iface" ] || return 1 + [ -n "$timeout_s" ] || timeout_s=5 + init_autoneg=$(ethGetAutonegState "$iface" 2>/dev/null || echo "unknown") + + # Bring interface up using existing helper (admin-up) + if command -v bringup_interface >/dev/null 2>&1; then + bringup_interface "$iface" 3 2 >/dev/null 2>&1 || true + else + ip link set "$iface" up >/dev/null 2>&1 || true + sleep 1 + fi + + # Let PHY settle a bit after admin-up + sleep 1 + + # 1) Wait for normal autoneg link + if ethWaitLinkUp "$iface" "$timeout_s"; then + return 0 + fi + + # If no ethtool, we cannot do fallback forcing + command -v ethtool >/dev/null 2>&1 || return 1 + + # 2) Try restarting autoneg once + if ethRestartAutoneg "$iface"; then + # Give retrain a moment before polling + sleep 1 + if ethWaitLinkUp "$iface" "$timeout_s"; then + return 0 + fi + fi + + # 3) Force speeds (smart order: may try 100 first for 100M-only setups) + force_order=$(ethPickForceOrder "$iface" 2>/dev/null || echo "1000 100") + for sp in $force_order; do + if ethForceSpeedMbps "$iface" "$sp"; then + # After forcing speed, some PHYs need a short settle window + sleep 1 + + # Best-effort retrain + ethtool -r "$iface" >/dev/null 2>&1 || true + sleep 1 + + if ethWaitLinkUp "$iface" "$timeout_s"; then + # IMPORTANT: Do NOT restore autoneg here. + # If we had to force speed (autoneg off), keeping it avoids regression + # on 100M-only / locked ports (your exact case). + return 0 + fi + fi + done + + # Bring-up failed: + # If autoneg was originally on, restore it (best-effort) so we don't leave user in forced mode. + if [ "$init_autoneg" = "on" ]; then + ethEnableAutoneg "$iface" >/dev/null 2>&1 || true + fi + return 1 +} +############################################################################### +# get_remoteproc_by_firmware [outfile] [all] +# - If outfile is given: append *all* matches as "|||" +# (one per line) and return 0 if at least one match. +# - If no outfile: print the *first* match to stdout and return 0. +# - Returns 1 if nothing matched, 3 if misuse (no fw argument). +############################################################################### +get_remoteproc_by_firmware() { + fw="$1" + out="$2" # optional: filepath to append results + list_all="$3" # set to "all" to continue past first match + + [ -n "$fw" ] || return 3 # misuse if no firmware provided + + found=0 + for p in /sys/class/remoteproc/remoteproc*; do + [ -d "$p" ] || continue + + # read name, firmware, state + name="" + [ -r "$p/name" ] && IFS= read -r name <"$p/name" + firmware="" + [ -r "$p/firmware" ] && IFS= read -r firmware <"$p/firmware" + state="unknown" + [ -r "$p/state" ] && IFS= read -r state <"$p/state" + + case "$name $firmware" in + *"$fw"*) + line="${p}|${state}|${firmware}|${name}" + if [ -n "$out" ]; then + printf '%s\n' "$line" >>"$out" + found=1 + continue + fi + + # print to stdout and possibly stop + printf '%s\n' "$line" + found=1 + [ "$list_all" = "all" ] || return 0 + ;; + esac + done + + # if we appended to a file, success if found>=1 + if [ "$found" -eq 1 ]; then + return 0 + else + return 1 + fi +} + +# ------------------------------------------------------------------------------ +# dt_has_remoteproc_fw +# Return: +# 0 = DT describes this remoteproc firmware +# 1 = DT does not describe it +# 3 = misuse (no argument) +# ------------------------------------------------------------------------------ +dt_has_remoteproc_fw() { + fw="$1" + [ -n "$fw" ] || return 3 + + base="/proc/device-tree" + [ -d "$base" ] || return 1 + + # lower-case match key + fw_lc=$(printf '%s\n' "$fw" | tr '[:upper:]' '[:lower:]') + + # new fast-path (any smp2p-* or remoteproc-* directory) + found=0 + for d in "$base"/smp2p-"$fw"* "$base"/remoteproc-"$fw"*; do + [ -d "$d" ] && found=1 && break + done + [ "$found" -eq 1 ] && return 0 + + # 2) Shallow find (/dev/null | grep -q .; then + return 0 + fi + + # 3) Grep soc@0 and aliases for a first match + if grep -Iq -m1 -F "$fw_lc" "$base/soc@0" "$base/aliases" 2>/dev/null; then + return 0 + fi + + # 4) Fallback: grep entire DT tree + if grep -Iq -m1 -F "$fw_lc" "$base" 2>/dev/null; then + return 0 + fi + + return 1 +} + +# Find the remoteproc path for a given firmware substring (e.g., "adsp", "cdsp", "gdsp"). +# Logic: +# - grep -n over /sys/class/remoteproc/remoteproc*/firmware (one line per remoteproc) +# - Take the first matching line number (1-based) +# - Subtract 1 → remoteproc index → /sys/class/remoteproc/remoteproc${idx} +get_remoteproc_path_by_firmware() { + name=$1 + + [ -n "$name" ] || return 1 + [ -d /sys/class/remoteproc ] || return 1 + + for fw in /sys/class/remoteproc/remoteproc*/firmware; do + # Skip if glob didn't match any file + [ -f "$fw" ] || continue + + # Read first line from firmware file without using cat + if IFS= read -r fwname <"$fw"; then + case "$fwname" in + *"$name"*) + # Map firmware file back to its remoteproc directory + dir=${fw%/firmware} + if [ -d "$dir" ]; then + printf '%s\n' "$dir" + return 0 + fi + ;; + esac + fi + done + + return 1 +} + +# Get remoteproc state +get_remoteproc_state() { + rp="$1" + [ -z "$rp" ] && { printf '\n'; return 1; } + + case "$rp" in + /sys/*) rpath="$rp" ;; + *) rpath="/sys/class/remoteproc/$rp" ;; + esac + + state_file="$rpath/state" + if [ -r "$state_file" ]; then + IFS= read -r state < "$state_file" || state="" + printf '%s\n' "$state" + return 0 + fi + printf '\n' + return 1 +} + +# wait_remoteproc_state +wait_remoteproc_state() { + rp="$1"; want="$2"; to=${3:-10}; poll=${4:-1} + + case "$rp" in + /sys/*) rpath="$rp" ;; + *) rpath="/sys/class/remoteproc/$rp" ;; + esac + + start_ts=$(date +%s) + while :; do + cur=$(get_remoteproc_state "$rpath") + [ "$cur" = "$want" ] && return 0 + + now_ts=$(date +%s) + [ $((now_ts - start_ts)) -ge "$to" ] && { + log_info "Waiting for state='$want' timed out (got='$cur')..." + return 1 + } + sleep "$poll" + done +} + +# Stop remoteproc +stop_remoteproc() { + rproc_path="$1" + + # Resolve to a real sysfs dir if only a name was given + case "$rproc_path" in + /sys/*) path="$rproc_path" ;; + remoteproc*) path="/sys/class/remoteproc/$rproc_path" ;; + *) path="$rproc_path" ;; # last resort, assume caller passed full path + esac + + statef="$path/state" + if [ ! -w "$statef" ]; then + log_warn "stop_remoteproc: state file not found/writable: $statef" + return 1 + fi + + printf 'stop\n' >"$statef" 2>/dev/null || return 1 + wait_remoteproc_state "$path" offline 6 +} + +# Start remoteproc +start_remoteproc() { + rproc_path="$1" + + case "$rproc_path" in + /sys/*) path="$rproc_path" ;; + remoteproc*) path="/sys/class/remoteproc/$rproc_path" ;; + *) path="$rproc_path" ;; + esac + + statef="$path/state" + if [ ! -w "$statef" ]; then + log_warn "start_remoteproc: state file not found/writable: $statef" + return 1 + fi + + printf 'start\n' >"$statef" 2>/dev/null || return 1 + wait_remoteproc_state "$path" running 6 +} +# Validate remoteproc running state with retries and logging +validate_remoteproc_running() { + fw_name="$1" + log_file="${2:-/dev/null}" + max_wait_secs="${3:-10}" + delay_per_try_secs="${4:-1}" + + rproc_path=$(get_remoteproc_path_by_firmware "$fw_name") + if [ -z "$rproc_path" ]; then + echo "[ERROR] Remoteproc for '$fw_name' not found" >> "$log_file" + { + echo "---- Last 20 remoteproc dmesg logs ----" + dmesg | grep -i "remoteproc" | tail -n 20 + echo "----------------------------------------" + } >> "$log_file" + return 1 + fi + + total_waited=0 + while [ "$total_waited" -lt "$max_wait_secs" ]; do + state=$(get_remoteproc_state "$rproc_path") + if [ "$state" = "running" ]; then + return 0 + fi + sleep "$delay_per_try_secs" + total_waited=$((total_waited + delay_per_try_secs)) + done + + echo "[ERROR] $fw_name remoteproc did not reach 'running' state within ${max_wait_secs}s (last state: $state)" >> "$log_file" + { + echo "---- Last 20 remoteproc dmesg logs ----" + dmesg | grep -i "remoteproc" | tail -n 20 + echo "----------------------------------------" + } >> "$log_file" + return 1 +} + +# acquire_test_lock +acquire_test_lock() { + lockfile="/var/lock/$1.lock" + exec 9>"$lockfile" + if ! flock -n 9; then + log_warn "Could not acquire lock on $lockfile → SKIP" + echo "$1 SKIP" > "./$1.res" + exit 0 + fi + log_info "Acquired lock on $lockfile" +} + +# release_test_lock +release_test_lock() { + flock -u 9 + log_info "Released lock" +} + +# summary_report +# Appends a machine‐readable summary line to the test log +summary_report() { + test="$1" + mode="$2" + stop_t="$3" + start_t="$4" + rp="$5" + log_info "Summary for ${test}: mode=${mode} stop_time_s=${stop_t} start_time_s=${start_t} rpmsg=${rp}" +} + +# dump_rproc_logs