This repository was archived by the owner on Nov 8, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathbluetooth-resume-fix
More file actions
195 lines (182 loc) · 5.7 KB
/
Copy pathbluetooth-resume-fix
File metadata and controls
195 lines (182 loc) · 5.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#!/bin/bash
# SPDX-License-Identifier: MIT
# Adapted from omacom/omarchy-mac-pkgs aa747a8c; see ../power/UPSTREAM.md.
set -euo pipefail
export LC_ALL=C
support_status=0
/usr/libexec/mseries/bluetooth-supported || support_status=$?
if (( support_status == 1 )); then
exit 0
elif (( support_status != 0 )); then
echo "Cannot determine Bluetooth recovery support" >&2
exit "$support_status"
fi
driver=${MSERIES_BLUETOOTH_DRIVER_DIR:-/sys/bus/pci/drivers/hci_bcm4377}
controllers=${MSERIES_BLUETOOTH_CLASS_DIR:-/sys/class/bluetooth}
stats=${MSERIES_SUSPEND_STATS_DIR:-/sys/power/suspend_stats}
wait_before=20
wait_after=15
wait_journal=10
started=$(date '+%Y-%m-%d %H:%M:%S')
radio_enabled() {
local state name blocked
state=$(rfkill --noheadings --output DEVICE,SOFT list bluetooth 2>/dev/null) || return 2
while read -r name blocked; do
# shellcheck disable=SC2053 # HCI identifiers from sysfs contain no glob syntax.
if [[ $name == $controller ]]; then
[[ $blocked == "unblocked" ]]
return $?
fi
done <<<"$state"
return 1
}
bound_controller() {
local path
for path in "$driver"/*:*:*.*; do
if [[ -e $path ]]; then
basename "$path"
return 0
fi
done
return 1
}
if device=$(bound_controller); then
:
else
echo "No controller bound to hci_bcm4377, nothing to rebind"
exit 0
fi
controller_for_device() {
local path parent
for path in "$controllers"/hci*; do
parent=$(readlink -f "$path/device") || continue
# shellcheck disable=SC2053 # A PCI function identifier contains no glob syntax.
if [[ ${parent##*/} == $device ]]; then
basename "$path"
return 0
fi
done
return 1
}
if controller=$(controller_for_device); then
signature="$controller: command 0x[0-9a-f]+ tx timeout"
else
echo "Cannot identify the Bluetooth controller bound to $device" >&2
exit 1
fi
radio_status=0
radio_enabled || radio_status=$?
if (( radio_status == 1 )); then
echo "Bluetooth is disabled or absent, nothing to do"
exit 0
elif (( radio_status != 0 )); then
echo "Cannot read Bluetooth radio state" >&2
exit "$radio_status"
fi
# The kernel counts suspend attempts this boot, and logs one entry line for each.
suspend_attempts() {
local success fail
success=$(<"$stats/success") && fail=$(<"$stats/fail") || return 1
echo $((success + fail))
}
# Watch from this cycle's suspend entry: device-resume timeouts precede this
# service. Wait until journald holds the entry the kernel counted, or its latest
# entry is an earlier cycle's, with that cycle's timeouts.
anchor() {
local kernel seen waited=0
attempts=$(suspend_attempts) || attempts=
while true; do
kernel=$(journalctl -kqb --no-pager -o cat 2>/dev/null) || return 2
seen=$(grep -c 'PM: suspend entry' <<<"$kernel" || true)
if [[ -z $attempts ]] || (( seen >= attempts )); then
break
fi
if (( waited >= wait_journal )); then
echo "The journal has no entry for this suspend after ${wait_journal}s; watching from now instead"
cursor=
started=$(date '+%Y-%m-%d %H:%M:%S')
return 0
fi
waited=$((waited + 1))
sleep 1
done
cursor=$(journalctl -kqb -n 1 -g 'PM: suspend entry' --show-cursor 2>/dev/null |
sed -n 's/^-- cursor: //p') || cursor=
if [[ -z $cursor ]]; then
echo "No suspend marker in the journal; watching from now instead"
fi
}
timed_out() {
local journal
if [[ -n $cursor ]]; then
journal=$(journalctl -kqb --after-cursor "$cursor" --no-pager 2>/dev/null) || return 2
else
journal=$(journalctl -kqb --since "$started" --no-pager 2>/dev/null) || return 2
fi
[[ $journal =~ $signature ]]
}
observe_status=0
anchor || observe_status=$?
if (( observe_status != 0 )); then
echo "Cannot read the kernel journal for Bluetooth recovery" >&2
exit "$observe_status"
fi
elapsed=0
while (( elapsed < wait_before )); do
# A suspend during the watch starts a new cycle; this run follows it, since
# systemd won't start the unit again while it is still running.
if [[ -n $attempts ]] && now=$(suspend_attempts) && (( now > attempts )); then
echo "Suspended again while watching; following the latest resume"
anchor || observe_status=$?
if (( observe_status != 0 )); then
echo "Cannot read the kernel journal for Bluetooth recovery" >&2
exit "$observe_status"
fi
elapsed=0
fi
timed_out || observe_status=$?
if (( observe_status == 0 )); then
break
elif (( observe_status != 1 )); then
echo "Cannot read the kernel journal for Bluetooth recovery" >&2
exit "$observe_status"
fi
observe_status=0
elapsed=$((elapsed + 1))
sleep 1
done
if (( elapsed >= wait_before )); then
echo "No HCI command timeout from $controller in the ${wait_before}s after resume; leaving it alone"
exit 0
fi
# A user can turn the radio off while the service is watching the journal.
radio_status=0
radio_enabled || radio_status=$?
if (( radio_status == 1 )); then
echo "Bluetooth was disabled while watching, nothing to do"
exit 0
elif (( radio_status != 0 )); then
echo "Cannot read Bluetooth radio state before the rebind" >&2
exit "$radio_status"
fi
echo "HCI command timeout from $controller ${elapsed}s after resume ($device); rebinding hci_bcm4377"
if ! { printf '%s\n' "$device" >"$driver/unbind"; } 2>/dev/null; then
echo "Failed to unbind $device; a reboot is needed" >&2
exit 1
fi
sleep 1
if ! { printf '%s\n' "$device" >"$driver/bind"; } 2>/dev/null; then
echo "Failed to bind $device; a reboot is needed" >&2
exit 1
fi
elapsed=0
while (( elapsed < wait_after )); do
if controller_for_device >/dev/null; then
echo "Controller back ${elapsed}s after the rebind"
exit 0
fi
elapsed=$((elapsed + 1))
sleep 1
done
echo "Controller still missing ${wait_after}s after the rebind" >&2
exit 1