mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 15:39:42 +00:00
fix: improve CI stability and Bluetooth audio detection
This commit is contained in:
265
bin/clean.sh
265
bin/clean.sh
@@ -182,32 +182,42 @@ normalize_paths_for_cleanup() {
|
|||||||
local normalized="${path%/}"
|
local normalized="${path%/}"
|
||||||
[[ -z "$normalized" ]] && normalized="$path"
|
[[ -z "$normalized" ]] && normalized="$path"
|
||||||
local found=false
|
local found=false
|
||||||
for existing in "${unique_paths[@]}"; do
|
if [[ ${#unique_paths[@]} -gt 0 ]]; then
|
||||||
if [[ "$existing" == "$normalized" ]]; then
|
for existing in "${unique_paths[@]}"; do
|
||||||
found=true
|
if [[ "$existing" == "$normalized" ]]; then
|
||||||
break
|
found=true
|
||||||
fi
|
break
|
||||||
done
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
[[ "$found" == "true" ]] || unique_paths+=("$normalized")
|
[[ "$found" == "true" ]] || unique_paths+=("$normalized")
|
||||||
done
|
done
|
||||||
|
|
||||||
local sorted_paths
|
local sorted_paths
|
||||||
sorted_paths=$(printf '%s\n' "${unique_paths[@]}" | awk '{print length "|" $0}' | LC_ALL=C sort -n | cut -d'|' -f2-)
|
if [[ ${#unique_paths[@]} -gt 0 ]]; then
|
||||||
|
sorted_paths=$(printf '%s\n' "${unique_paths[@]}" | awk '{print length "|" $0}' | LC_ALL=C sort -n | cut -d'|' -f2-)
|
||||||
|
else
|
||||||
|
sorted_paths=""
|
||||||
|
fi
|
||||||
|
|
||||||
local -a result_paths=()
|
local -a result_paths=()
|
||||||
while IFS= read -r path; do
|
while IFS= read -r path; do
|
||||||
[[ -z "$path" ]] && continue
|
[[ -z "$path" ]] && continue
|
||||||
local is_child=false
|
local is_child=false
|
||||||
for kept in "${result_paths[@]}"; do
|
if [[ ${#result_paths[@]} -gt 0 ]]; then
|
||||||
if [[ "$path" == "$kept" || "$path" == "$kept"/* ]]; then
|
for kept in "${result_paths[@]}"; do
|
||||||
is_child=true
|
if [[ "$path" == "$kept" || "$path" == "$kept"/* ]]; then
|
||||||
break
|
is_child=true
|
||||||
fi
|
break
|
||||||
done
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
[[ "$is_child" == "true" ]] || result_paths+=("$path")
|
[[ "$is_child" == "true" ]] || result_paths+=("$path")
|
||||||
done <<< "$sorted_paths"
|
done <<< "$sorted_paths"
|
||||||
|
|
||||||
printf '%s\n' "${result_paths[@]}"
|
if [[ ${#result_paths[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${result_paths[@]}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
get_cleanup_path_size_kb() {
|
get_cleanup_path_size_kb() {
|
||||||
@@ -307,7 +317,12 @@ safe_clean() {
|
|||||||
while IFS= read -r path; do
|
while IFS= read -r path; do
|
||||||
[[ -n "$path" ]] && normalized_paths+=("$path")
|
[[ -n "$path" ]] && normalized_paths+=("$path")
|
||||||
done < <(normalize_paths_for_cleanup "${existing_paths[@]}")
|
done < <(normalize_paths_for_cleanup "${existing_paths[@]}")
|
||||||
existing_paths=("${normalized_paths[@]}")
|
|
||||||
|
if [[ ${#normalized_paths[@]} -gt 0 ]]; then
|
||||||
|
existing_paths=("${normalized_paths[@]}")
|
||||||
|
else
|
||||||
|
existing_paths=()
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Only show spinner if we have enough items to justify it (>10 items)
|
# Only show spinner if we have enough items to justify it (>10 items)
|
||||||
@@ -330,61 +345,111 @@ safe_clean() {
|
|||||||
local last_progress_update=$(date +%s)
|
local last_progress_update=$(date +%s)
|
||||||
local total_paths=${#existing_paths[@]}
|
local total_paths=${#existing_paths[@]}
|
||||||
|
|
||||||
for path in "${existing_paths[@]}"; do
|
if [[ ${#existing_paths[@]} -gt 0 ]]; then
|
||||||
(
|
for path in "${existing_paths[@]}"; do
|
||||||
local size
|
(
|
||||||
size=$(get_cleanup_path_size_kb "$path")
|
local size
|
||||||
# Ensure size is numeric (additional safety layer)
|
size=$(get_cleanup_path_size_kb "$path")
|
||||||
[[ ! "$size" =~ ^[0-9]+$ ]] && size=0
|
# Ensure size is numeric (additional safety layer)
|
||||||
# Use index + PID for unique filename
|
[[ ! "$size" =~ ^[0-9]+$ ]] && size=0
|
||||||
local tmp_file="$temp_dir/result_${idx}.$$"
|
# Use index + PID for unique filename
|
||||||
# Optimization: Skip expensive file counting. Size is the key metric.
|
local tmp_file="$temp_dir/result_${idx}.$$"
|
||||||
# Just indicate presence if size > 0
|
# Optimization: Skip expensive file counting. Size is the key metric.
|
||||||
if [[ "$size" -gt 0 ]]; then
|
# Just indicate presence if size > 0
|
||||||
echo "$size 1" > "$tmp_file"
|
if [[ "$size" -gt 0 ]]; then
|
||||||
else
|
echo "$size 1" > "$tmp_file"
|
||||||
echo "0 0" > "$tmp_file"
|
else
|
||||||
fi
|
echo "0 0" > "$tmp_file"
|
||||||
mv "$tmp_file" "$temp_dir/result_${idx}" 2> /dev/null || true
|
fi
|
||||||
) &
|
mv "$tmp_file" "$temp_dir/result_${idx}" 2> /dev/null || true
|
||||||
pids+=($!)
|
) &
|
||||||
((idx++))
|
pids+=($!)
|
||||||
|
((idx++))
|
||||||
|
|
||||||
if ((${#pids[@]} >= MOLE_MAX_PARALLEL_JOBS)); then
|
if ((${#pids[@]} >= MOLE_MAX_PARALLEL_JOBS)); then
|
||||||
wait "${pids[0]}" 2> /dev/null || true
|
wait "${pids[0]}" 2> /dev/null || true
|
||||||
pids=("${pids[@]:1}")
|
pids=("${pids[@]:1}")
|
||||||
|
((completed++))
|
||||||
|
|
||||||
|
# Update progress using helper function
|
||||||
|
if [[ "$show_spinner" == "true" && -t 1 ]]; then
|
||||||
|
update_progress_if_needed "$completed" "$total_paths" last_progress_update 2 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#pids[@]} -gt 0 ]]; then
|
||||||
|
for pid in "${pids[@]}"; do
|
||||||
|
wait "$pid" 2> /dev/null || true
|
||||||
((completed++))
|
((completed++))
|
||||||
|
|
||||||
# Update progress using helper function
|
# Update progress using helper function
|
||||||
if [[ "$show_spinner" == "true" && -t 1 ]]; then
|
if [[ "$show_spinner" == "true" && -t 1 ]]; then
|
||||||
update_progress_if_needed "$completed" "$total_paths" last_progress_update 2 || true
|
update_progress_if_needed "$completed" "$total_paths" last_progress_update 2 || true
|
||||||
fi
|
fi
|
||||||
fi
|
done
|
||||||
done
|
fi
|
||||||
|
|
||||||
for pid in "${pids[@]}"; do
|
|
||||||
wait "$pid" 2> /dev/null || true
|
|
||||||
((completed++))
|
|
||||||
|
|
||||||
# Update progress using helper function
|
|
||||||
if [[ "$show_spinner" == "true" && -t 1 ]]; then
|
|
||||||
update_progress_if_needed "$completed" "$total_paths" last_progress_update 2 || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Read results using same index
|
# Read results using same index
|
||||||
idx=0
|
idx=0
|
||||||
for path in "${existing_paths[@]}"; do
|
if [[ ${#existing_paths[@]} -gt 0 ]]; then
|
||||||
local result_file="$temp_dir/result_${idx}"
|
for path in "${existing_paths[@]}"; do
|
||||||
if [[ -f "$result_file" ]]; then
|
local result_file="$temp_dir/result_${idx}"
|
||||||
read -r size count < "$result_file" 2> /dev/null || true
|
if [[ -f "$result_file" ]]; then
|
||||||
# Even if size is 0 or du failed, we should try to remove the file if it was found
|
read -r size count < "$result_file" 2> /dev/null || true
|
||||||
# count > 0 means the file existed at scan time (or we forced it to 1)
|
# Even if size is 0 or du failed, we should try to remove the file if it was found
|
||||||
|
# count > 0 means the file existed at scan time (or we forced it to 1)
|
||||||
|
|
||||||
# Correction: The subshell now writes "size 1" if size>0, or "0 0" if size=0
|
# Correction: The subshell now writes "size 1" if size>0, or "0 0" if size=0
|
||||||
# But we want to delete even if size is 0.
|
# But we want to delete even if size is 0.
|
||||||
# Let's check if the path still exists to be safe, or trust the input list.
|
# Let's check if the path still exists to be safe, or trust the input list.
|
||||||
# Actually, safe_remove checks existence.
|
# Actually, safe_remove checks existence.
|
||||||
|
|
||||||
|
local removed=0
|
||||||
|
if [[ "$DRY_RUN" != "true" ]]; then
|
||||||
|
# Handle symbolic links separately (only remove the link, not the target)
|
||||||
|
if [[ -L "$path" ]]; then
|
||||||
|
rm "$path" 2> /dev/null && removed=1
|
||||||
|
else
|
||||||
|
if safe_remove "$path" true; then
|
||||||
|
removed=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
removed=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $removed -eq 1 ]]; then
|
||||||
|
if [[ "$size" -gt 0 ]]; then
|
||||||
|
((total_size_kb += size))
|
||||||
|
fi
|
||||||
|
((total_count += 1))
|
||||||
|
removed_any=1
|
||||||
|
else
|
||||||
|
# Only increment failure count if we actually tried and failed
|
||||||
|
# Check existence to avoid false failure report for already gone files
|
||||||
|
if [[ -e "$path" && "$DRY_RUN" != "true" ]]; then
|
||||||
|
((removal_failed_count++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
((idx++))
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Temp dir will be auto-cleaned by cleanup_temp_files
|
||||||
|
else
|
||||||
|
local idx=0
|
||||||
|
if [[ ${#existing_paths[@]} -gt 0 ]]; then
|
||||||
|
for path in "${existing_paths[@]}"; do
|
||||||
|
local size_kb
|
||||||
|
size_kb=$(get_cleanup_path_size_kb "$path")
|
||||||
|
# Ensure size_kb is numeric (additional safety layer)
|
||||||
|
[[ ! "$size_kb" =~ ^[0-9]+$ ]] && size_kb=0
|
||||||
|
|
||||||
|
# Optimization: Skip expensive file counting, but DO NOT skip deletion if size is 0
|
||||||
|
# Previously: if [[ "$size_kb" -gt 0 ]]; then ...
|
||||||
|
|
||||||
local removed=0
|
local removed=0
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
if [[ "$DRY_RUN" != "true" ]]; then
|
||||||
@@ -401,62 +466,20 @@ safe_clean() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $removed -eq 1 ]]; then
|
if [[ $removed -eq 1 ]]; then
|
||||||
if [[ "$size" -gt 0 ]]; then
|
if [[ "$size_kb" -gt 0 ]]; then
|
||||||
((total_size_kb += size))
|
((total_size_kb += size_kb))
|
||||||
fi
|
fi
|
||||||
((total_count += 1))
|
((total_count += 1))
|
||||||
removed_any=1
|
removed_any=1
|
||||||
else
|
else
|
||||||
# Only increment failure count if we actually tried and failed
|
# Only increment failure count if we actually tried and failed
|
||||||
# Check existence to avoid false failure report for already gone files
|
|
||||||
if [[ -e "$path" && "$DRY_RUN" != "true" ]]; then
|
if [[ -e "$path" && "$DRY_RUN" != "true" ]]; then
|
||||||
((removal_failed_count++))
|
((removal_failed_count++))
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
((idx++))
|
||||||
((idx++))
|
done
|
||||||
done
|
fi
|
||||||
|
|
||||||
# Temp dir will be auto-cleaned by cleanup_temp_files
|
|
||||||
else
|
|
||||||
local idx=0
|
|
||||||
for path in "${existing_paths[@]}"; do
|
|
||||||
local size_kb
|
|
||||||
size_kb=$(get_cleanup_path_size_kb "$path")
|
|
||||||
# Ensure size_kb is numeric (additional safety layer)
|
|
||||||
[[ ! "$size_kb" =~ ^[0-9]+$ ]] && size_kb=0
|
|
||||||
|
|
||||||
# Optimization: Skip expensive file counting, but DO NOT skip deletion if size is 0
|
|
||||||
# Previously: if [[ "$size_kb" -gt 0 ]]; then ...
|
|
||||||
|
|
||||||
local removed=0
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
|
||||||
# Handle symbolic links separately (only remove the link, not the target)
|
|
||||||
if [[ -L "$path" ]]; then
|
|
||||||
rm "$path" 2> /dev/null && removed=1
|
|
||||||
else
|
|
||||||
if safe_remove "$path" true; then
|
|
||||||
removed=1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
removed=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $removed -eq 1 ]]; then
|
|
||||||
if [[ "$size_kb" -gt 0 ]]; then
|
|
||||||
((total_size_kb += size_kb))
|
|
||||||
fi
|
|
||||||
((total_count += 1))
|
|
||||||
removed_any=1
|
|
||||||
else
|
|
||||||
# Only increment failure count if we actually tried and failed
|
|
||||||
if [[ -e "$path" && "$DRY_RUN" != "true" ]]; then
|
|
||||||
((removal_failed_count++))
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
((idx++))
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$show_spinner" == "true" ]]; then
|
if [[ "$show_spinner" == "true" ]]; then
|
||||||
@@ -488,23 +511,25 @@ safe_clean() {
|
|||||||
local paths_temp=$(create_temp_file)
|
local paths_temp=$(create_temp_file)
|
||||||
|
|
||||||
idx=0
|
idx=0
|
||||||
for path in "${existing_paths[@]}"; do
|
if [[ ${#existing_paths[@]} -gt 0 ]]; then
|
||||||
local size=0
|
for path in "${existing_paths[@]}"; do
|
||||||
|
local size=0
|
||||||
|
|
||||||
if [[ -n "${temp_dir:-}" && -f "$temp_dir/result_${idx}" ]]; then
|
if [[ -n "${temp_dir:-}" && -f "$temp_dir/result_${idx}" ]]; then
|
||||||
read -r size count < "$temp_dir/result_${idx}" 2> /dev/null || true
|
read -r size count < "$temp_dir/result_${idx}" 2> /dev/null || true
|
||||||
else
|
else
|
||||||
size=$(get_cleanup_path_size_kb "$path" 2> /dev/null || echo "0")
|
size=$(get_cleanup_path_size_kb "$path" 2> /dev/null || echo "0")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ "$size" == "0" || -z "$size" ]] && {
|
[[ "$size" == "0" || -z "$size" ]] && {
|
||||||
|
((idx++))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "$(dirname "$path")|$size|$path" >> "$paths_temp"
|
||||||
((idx++))
|
((idx++))
|
||||||
continue
|
done
|
||||||
}
|
fi
|
||||||
|
|
||||||
echo "$(dirname "$path")|$size|$path" >> "$paths_temp"
|
|
||||||
((idx++))
|
|
||||||
done
|
|
||||||
|
|
||||||
# Group and export paths
|
# Group and export paths
|
||||||
if [[ -f "$paths_temp" && -s "$paths_temp" ]]; then
|
if [[ -f "$paths_temp" && -s "$paths_temp" ]]; then
|
||||||
|
|||||||
@@ -506,7 +506,9 @@ opt_disk_permissions_repair() {
|
|||||||
|
|
||||||
# Bluetooth module reset
|
# Bluetooth module reset
|
||||||
# Resets Bluetooth daemon to fix connectivity issues
|
# Resets Bluetooth daemon to fix connectivity issues
|
||||||
# Only runs if no Bluetooth audio is playing
|
# Intelligently detects Bluetooth audio usage:
|
||||||
|
# 1. Checks if default audio output is Bluetooth (precise)
|
||||||
|
# 2. Falls back to Bluetooth + media app detection (compatibility)
|
||||||
opt_bluetooth_reset() {
|
opt_bluetooth_reset() {
|
||||||
if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then
|
if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then
|
||||||
if has_bluetooth_hid_connected; then
|
if has_bluetooth_hid_connected; then
|
||||||
@@ -517,16 +519,31 @@ opt_bluetooth_reset() {
|
|||||||
# Check if any audio is playing through Bluetooth
|
# Check if any audio is playing through Bluetooth
|
||||||
local bt_audio_active=false
|
local bt_audio_active=false
|
||||||
|
|
||||||
# Check system audio output
|
# Method 1: Check if default audio output is Bluetooth (precise)
|
||||||
if system_profiler SPBluetoothDataType 2> /dev/null | grep -q "Connected: Yes"; then
|
local audio_info
|
||||||
# Check if any audio/video apps are running that might be using Bluetooth
|
audio_info=$(system_profiler SPAudioDataType 2> /dev/null || echo "")
|
||||||
local -a media_apps=("Music" "Spotify" "VLC" "QuickTime Player" "TV" "Podcasts")
|
|
||||||
for app in "${media_apps[@]}"; do
|
# Extract default output device information
|
||||||
if pgrep -x "$app" > /dev/null 2>&1; then
|
local default_output
|
||||||
bt_audio_active=true
|
default_output=$(echo "$audio_info" | awk '/Default Output Device: Yes/,/^$/' 2> /dev/null || echo "")
|
||||||
break
|
|
||||||
fi
|
# Check if transport type is Bluetooth
|
||||||
done
|
if echo "$default_output" | grep -qi "Transport:.*Bluetooth"; then
|
||||||
|
bt_audio_active=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Method 2: Fallback - Bluetooth connected + media apps running (compatibility)
|
||||||
|
if [[ "$bt_audio_active" == "false" ]]; then
|
||||||
|
if system_profiler SPBluetoothDataType 2> /dev/null | grep -q "Connected: Yes"; then
|
||||||
|
# Extended media apps list for broader coverage
|
||||||
|
local -a media_apps=("Music" "Spotify" "VLC" "QuickTime Player" "TV" "Podcasts" "Safari" "Google Chrome" "Chrome" "Firefox" "Arc" "IINA" "mpv")
|
||||||
|
for app in "${media_apps[@]}"; do
|
||||||
|
if pgrep -x "$app" > /dev/null 2>&1; then
|
||||||
|
bt_audio_active=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$bt_audio_active" == "true" ]]; then
|
if [[ "$bt_audio_active" == "true" ]]; then
|
||||||
|
|||||||
@@ -956,6 +956,57 @@ EOF
|
|||||||
[[ "$output" == *"Bluetooth already optimal"* ]]
|
[[ "$output" == *"Bluetooth already optimal"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "opt_bluetooth_reset skips when Bluetooth audio output is active" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
|
||||||
|
|
||||||
|
# Mock system_profiler to return Bluetooth audio as default output (Method 1)
|
||||||
|
system_profiler() {
|
||||||
|
if [[ "$1" == "SPAudioDataType" ]]; then
|
||||||
|
cat << 'AUDIO_OUT'
|
||||||
|
Audio:
|
||||||
|
Devices:
|
||||||
|
AirPods Pro:
|
||||||
|
Default Output Device: Yes
|
||||||
|
Manufacturer: Apple Inc.
|
||||||
|
Output Channels: 2
|
||||||
|
Transport: Bluetooth
|
||||||
|
Output Source: AirPods Pro
|
||||||
|
AUDIO_OUT
|
||||||
|
return 0
|
||||||
|
elif [[ "$1" == "SPBluetoothDataType" ]]; then
|
||||||
|
echo "Bluetooth:"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
export -f system_profiler
|
||||||
|
|
||||||
|
# Mock awk to process audio output
|
||||||
|
awk() {
|
||||||
|
if [[ "${*}" == *"Default Output Device"* ]]; then
|
||||||
|
cat << 'AWK_OUT'
|
||||||
|
Default Output Device: Yes
|
||||||
|
Manufacturer: Apple Inc.
|
||||||
|
Output Channels: 2
|
||||||
|
Transport: Bluetooth
|
||||||
|
Output Source: AirPods Pro
|
||||||
|
AWK_OUT
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
command awk "$@"
|
||||||
|
}
|
||||||
|
export -f awk
|
||||||
|
|
||||||
|
opt_bluetooth_reset
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Bluetooth already optimal"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
@test "opt_bluetooth_reset restarts when safe" {
|
@test "opt_bluetooth_reset restarts when safe" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|||||||
Reference in New Issue
Block a user