diff --git a/Runner/suites/Connectivity/Ethernet/Ethernet.yaml b/Runner/suites/Connectivity/Ethernet/Ethernet.yaml index 85868ca4..6fef9e51 100644 --- a/Runner/suites/Connectivity/Ethernet/Ethernet.yaml +++ b/Runner/suites/Connectivity/Ethernet/Ethernet.yaml @@ -11,7 +11,7 @@ metadata: - functional params: - INTERFACE: "eth0" + INTERFACE: "end0" run: steps: diff --git a/Runner/suites/Connectivity/Ethernet/run.sh b/Runner/suites/Connectivity/Ethernet/run.sh index 1fe749fa..258e49ec 100755 --- a/Runner/suites/Connectivity/Ethernet/run.sh +++ b/Runner/suites/Connectivity/Ethernet/run.sh @@ -1,8 +1,7 @@ #!/bin/sh - -#Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -#SPDX-License-Identifier: BSD-3-Clause-Clear - +# 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="" @@ -20,72 +19,244 @@ if [ -z "$INIT_ENV" ]; then exit 1 fi -if [ -z "$__INIT_ENV_LOADED" ]; then +if [ -z "${__INIT_ENV_LOADED:-}" ]; then # shellcheck disable=SC1090 . "$INIT_ENV" + __INIT_ENV_LOADED=1 fi + # shellcheck disable=SC1090,SC1091 . "$TOOLS/functestlib.sh" TESTNAME="Ethernet" test_path=$(find_test_case_by_name "$TESTNAME") cd "$test_path" || exit 1 + res_file="./$TESTNAME.res" summary_file="./$TESTNAME.summary" rm -f "$res_file" "$summary_file" - +: >"$summary_file" + log_info "--------------------------------------------------------------------------" log_info "-------------------Starting $TESTNAME Testcase----------------------------" +# Config knobs (safe defaults) +LINK_TIMEOUT_S="${LINK_TIMEOUT_S:-5}" +IP_TIMEOUT_S="${IP_TIMEOUT_S:-10}" +PING_TARGET="${PING_TARGET:-8.8.8.8}" +PING_COUNT="${PING_COUNT:-4}" +PING_WAIT_S="${PING_WAIT_S:-2}" +PING_RETRIES="${PING_RETRIES:-3}" + +log_info "Config: LINK_TIMEOUT_S=${LINK_TIMEOUT_S} IP_TIMEOUT_S=${IP_TIMEOUT_S} PING_TARGET=${PING_TARGET} PING_RETRIES=${PING_RETRIES} PING_COUNT=${PING_COUNT} PING_WAIT_S=${PING_WAIT_S}" + +# Gate on kernel config BEFORE dependency checks (as requested) +if ! check_kernel_config "CONFIG_QCA808X_PHY"; then + log_warn "$TESTNAME : CONFIG_QCA808X_PHY not enabled; skipping Ethernet test" + echo "$TESTNAME SKIP" >"$res_file" + exit 0 +fi + # Check for dependencies check_dependencies ip ping -# User-specified interface (argument) or all detected -user_iface="$1" -if [ -n "$user_iface" ]; then - ETH_IFACES="$user_iface" - log_info "User specified interface: $user_iface" -else - ETH_IFACES="$(get_ethernet_interfaces)" - log_info "Auto-detected Ethernet interfaces: $ETH_IFACES" +# ethtool is required for robust fallback on 100M-locked ports +if ! command -v ethtool >/dev/null 2>&1; then + log_warn "ethtool not found; cannot apply robust link-speed fallback; skipping" + echo "$TESTNAME SKIP" >"$res_file" + exit 0 fi +ETH_IFACES="$(get_ethernet_interfaces)" +log_info "Auto-detected Ethernet interfaces: $ETH_IFACES" + if [ -z "$ETH_IFACES" ]; then log_warn "No Ethernet interfaces detected." - echo "No Ethernet interfaces detected." >> "$summary_file" - echo "$TESTNAME SKIP" > "$res_file" + echo "No Ethernet interfaces detected." >>"$summary_file" + echo "$TESTNAME SKIP" >"$res_file" exit 0 fi +# Detect if a network manager is active once (avoid fighting it per interface) +nm_active=0 +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 + nm_active=1 + fi +fi + +if [ "$nm_active" -eq 1 ]; then + log_info "Network manager detected (NetworkManager/systemd-networkd active): will wait for IP, will NOT run udhcpc." +else + log_info "No network manager detected: will use try_dhcp_client_safe if IP is missing." +fi + +is_valid_ipv4() { + _ip="$1" + [ -n "$_ip" ] || return 1 + echo "$_ip" | grep -q '^169\.254' && return 1 + return 0 +} + +iface_link_up() { + _iface="$1" + [ -n "$_iface" ] || return 1 + + # Prefer functestlib helpers if present + if command -v ethIsLinkUp >/dev/null 2>&1; then + ethIsLinkUp "$_iface" + return $? + fi + if command -v is_link_up >/dev/null 2>&1; then + is_link_up "$_iface" + return $? + fi + + # Fallback to carrier + [ -r "/sys/class/net/$_iface/carrier" ] || return 1 + [ "$(cat "/sys/class/net/$_iface/carrier" 2>/dev/null)" = "1" ] +} + +run_ping_check() { + _iface="$1" + + i=1 + while [ "$i" -le "$PING_RETRIES" ]; do + log_info "Ping attempt $i/$PING_RETRIES: ping -I $_iface -c $PING_COUNT -W $PING_WAIT_S $PING_TARGET" + ping_out="$(ping -I "$_iface" -c "$PING_COUNT" -W "$PING_WAIT_S" "$PING_TARGET" 2>&1)" + ping_rc=$? + + printf '%s\n' "$ping_out" | while IFS= read -r l; do + [ -n "$l" ] && log_info "[ping:$_iface] $l" + done + + if [ "$ping_rc" -eq 0 ]; then + return 0 + fi + + log_warn "Ping failed for $_iface (rc=$ping_rc) (attempt $i/$PING_RETRIES)" + i=$((i + 1)) + sleep 2 + done + + return 1 +} + any_passed=0 any_tested=0 for iface in $ETH_IFACES; do log_info "---- Testing interface: $iface ----" - if ! is_interface_up "$iface"; then - log_warn "$iface is DOWN, skipping" - echo "$iface: SKIP (down/no cable)" >> "$summary_file" + # FAST PATH (no ethtool dependency here): + # If link is already up AND a valid (non-link-local) IPv4 exists, skip bring-up/DHCP. + ip_addr="$(get_ip_address "$iface" 2>/dev/null || true)" + if is_valid_ipv4 "$ip_addr" && iface_link_up "$iface"; then + log_info "$iface fast-path: link is up and valid IP already present ($ip_addr). Skipping link bring-up/IP acquisition." + + log_pass "$iface is UP" + log_info "$iface got IP: $ip_addr" + any_tested=1 + + if run_ping_check "$iface"; then + log_pass "Ethernet connectivity verified via ping" + echo "$iface: PASS (IP: $ip_addr, ping OK)" >>"$summary_file" + any_passed=1 + else + log_fail "Ping test failed for $iface" + echo "$iface: FAIL (IP: $ip_addr, ping failed)" >>"$summary_file" + fi + continue fi - ip_addr=$(get_ip_address "$iface") - if [ -z "$ip_addr" ]; then - if try_dhcp_client_safe "$iface" 10; then - ip_addr=$(get_ip_address "$iface") - log_info "$iface obtained IP after DHCP: $ip_addr" - fi + # Debug snapshot (compact) + carrier="?" + if [ -r "/sys/class/net/$iface/carrier" ]; then + carrier=$(cat "/sys/class/net/$iface/carrier" 2>/dev/null || echo "?") fi + log_info "$iface: initial carrier=$carrier" + ip link show "$iface" 2>/dev/null | while IFS= read -r l; do [ -n "$l" ] && log_info "[ip-link] $l"; done + ethtool "$iface" 2>/dev/null | awk -F': ' '/^[[:space:]]*(Speed:|Duplex:|Auto-negotiation:|Link detected:)/ {print}' \ + | while IFS= read -r l; do [ -n "$l" ] && log_info "[ethtool] $l"; done - if [ -z "$ip_addr" ]; then - log_warn "$iface did not obtain an IP address after DHCP attempt, skipping" - echo "$iface: SKIP (no IP, DHCP failed)" >> "$summary_file" + log_info "Bringing link up with fallback for $iface (timeout=${LINK_TIMEOUT_S}s)..." + if ethEnsureLinkUpWithFallback "$iface" "$LINK_TIMEOUT_S"; then + sp="-" + if sp=$(ethGetLinkSpeedMbps "$iface" 2>/dev/null); then + log_info "$iface link is UP (speed=${sp}Mb/s)" + else + log_info "$iface link is UP (speed=unknown)" + fi + ethtool "$iface" 2>/dev/null | awk -F': ' '/^[[:space:]]*(Speed:|Duplex:|Auto-negotiation:|Link detected:)/ {print}' \ + | while IFS= read -r l; do [ -n "$l" ] && log_info "[ethtool] $l"; done + else + # Decide SKIP/FAIL post-failure + carrier="?" + if [ -r "/sys/class/net/$iface/carrier" ]; then + carrier=$(cat "/sys/class/net/$iface/carrier" 2>/dev/null || echo "?") + fi + + log_info "Link bring-up failed for $iface. Diagnostics:" + ip link show "$iface" 2>/dev/null | while IFS= read -r l; do [ -n "$l" ] && log_info "[ip-link] $l"; done + ethtool "$iface" 2>/dev/null | sed -n '1,80p' | while IFS= read -r l; do [ -n "$l" ] && log_info "[ethtool] $l"; done + + if [ "$carrier" = "0" ]; then + log_warn "$iface: no link detected (carrier=0); treating as no-cable and skipping" + echo "$iface: SKIP (no cable/link; carrier=0)" >>"$summary_file" + elif [ "$carrier" = "1" ]; then + log_fail "$iface: carrier=1 but link did not come up after fallback; failing" + echo "$iface: FAIL (link bring-up failed; carrier=1)" >>"$summary_file" + any_tested=1 + else + link_detected="unknown" + link_detected=$(ethtool "$iface" 2>/dev/null | awk -F': ' '/^[[:space:]]*Link detected:/ {print $2; exit 0}' || true) + [ -n "$link_detected" ] || link_detected="unknown" + + if [ "$link_detected" = "no" ]; then + log_warn "$iface: Link detected: no; treating as no-cable and skipping" + echo "$iface: SKIP (no cable/link; ethtool Link detected: no)" >>"$summary_file" + else + log_fail "$iface: link did not come up after fallback (carrier unknown, Link detected=${link_detected}); failing" + echo "$iface: FAIL (link bring-up failed; carrier unknown, Link detected=${link_detected})" >>"$summary_file" + any_tested=1 + fi + fi continue fi - if echo "$ip_addr" | grep -q '^169\.254'; then - log_warn "$iface got only link-local IP ($ip_addr), skipping" - echo "$iface: SKIP (link-local only: $ip_addr)" >> "$summary_file" + # IP acquisition only if needed + ip_addr="$(get_ip_address "$iface" 2>/dev/null || true)" + if [ -n "$ip_addr" ]; then + log_info "$iface pre-existing IP: $ip_addr" + else + log_info "$iface has no IPv4 address yet." + fi + + if ! is_valid_ipv4 "$ip_addr"; then + if [ "$nm_active" -eq 1 ]; then + log_info "Waiting up to ${IP_TIMEOUT_S}s for IP on $iface (managed by network manager)..." + ip_addr="$(wait_for_ip_address "$iface" "$IP_TIMEOUT_S" 2>/dev/null || true)" + else + log_info "Attempting safe DHCP on $iface (timeout=${IP_TIMEOUT_S}s)..." + if try_dhcp_client_safe "$iface" "$IP_TIMEOUT_S"; then + ip_addr="$(get_ip_address "$iface" 2>/dev/null || true)" + fi + fi + fi + + ip -4 addr show "$iface" 2>/dev/null | while IFS= read -r l; do [ -n "$l" ] && log_info "[ip-addr] $l"; done + ip route show dev "$iface" 2>/dev/null | sed -n '1,40p' | while IFS= read -r l; do [ -n "$l" ] && log_info "[ip-route:$iface] $l"; done + + if ! is_valid_ipv4 "$ip_addr"; then + if [ -z "$ip_addr" ]; then + log_warn "$iface did not obtain an IP address, skipping" + echo "$iface: SKIP (no IP)" >>"$summary_file" + else + log_warn "$iface got only link-local IP ($ip_addr), skipping" + echo "$iface: SKIP (link-local only: $ip_addr)" >>"$summary_file" + fi continue fi @@ -93,24 +264,14 @@ for iface in $ETH_IFACES; do log_info "$iface got IP: $ip_addr" any_tested=1 - retries=3 - pass=0 - for i in $(seq 1 $retries); do - if ping -I "$iface" -c 4 -W 2 8.8.8.8 >/dev/null 2>&1; then - log_pass "Ethernet connectivity verified via ping" - echo "$iface: PASS (IP: $ip_addr, ping OK)" >> "$summary_file" - pass=1 - any_passed=1 - break - else - log_warn "Ping failed for $iface (attempt $i/$retries)" - sleep 2 - fi - done - if [ "$pass" -eq 0 ]; then + if run_ping_check "$iface"; then + log_pass "Ethernet connectivity verified via ping" + echo "$iface: PASS (IP: $ip_addr, ping OK)" >>"$summary_file" + any_passed=1 + else log_fail "Ping test failed for $iface" - echo "$iface: FAIL (IP: $ip_addr, ping failed)" >> "$summary_file" + echo "$iface: FAIL (IP: $ip_addr, ping failed)" >>"$summary_file" fi done @@ -122,14 +283,16 @@ else fi if [ "$any_passed" -gt 0 ]; then - echo "$TESTNAME PASS" > "$res_file" + echo "$TESTNAME PASS" >"$res_file" exit 0 -elif [ "$any_tested" -gt 0 ]; then - echo "$TESTNAME FAIL" > "$res_file" - exit 1 -else - log_warn "No interfaces were tested (all were skipped)." - echo "No suitable Ethernet interfaces found. All were down, link-local, or failed to get IP." >> "$summary_file" - echo "$TESTNAME SKIP" > "$res_file" +fi + +if [ "$any_tested" -gt 0 ]; then + echo "$TESTNAME FAIL" >"$res_file" exit 0 fi + +log_warn "No interfaces were tested (all were skipped)." +echo "No suitable Ethernet interfaces found. All were no-link, link-local, or no-IP." >>"$summary_file" +echo "$TESTNAME SKIP" >"$res_file" +exit 0 diff --git a/Runner/utils/functestlib.sh b/Runner/utils/functestlib.sh index f1af00d6..7795a229 100755 --- a/Runner/utils/functestlib.sh +++ b/Runner/utils/functestlib.sh @@ -1775,37 +1775,469 @@ run_dhcp_client() { # Safely run DHCP client without disrupting existing config try_dhcp_client_safe() { - iface="$1" - timeout="${2:-10}" - + 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, skipping DHCP attempt" + 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 - - # Use a no-op script to avoid flushing IPs - safe_dhcp_script="/tmp/dhcp-noop-$$.sh" + + # 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 -exit 0 +# 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="" - log_info "Attempting DHCP on $iface safely..." - (udhcpc -i "$iface" -n -q -s "$safe_dhcp_script" >/dev/null 2>&1) & - dhcp_pid=$! - sleep "$timeout" - kill "$dhcp_pid" 2>/dev/null - rm -f "$safe_dhcp_script" + [ -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 "|||"