Compare commits
4
Commits
520ce45a50
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d34a6e5901 | ||
|
|
4ba1d1317f | ||
|
|
67c4ae45a1 | ||
|
|
1c95257369 |
@@ -23,3 +23,4 @@
|
|||||||
- [GnuPG](software/006-opengpg.qd)
|
- [GnuPG](software/006-opengpg.qd)
|
||||||
- [KDE Connect](software/007-kdeconnect.qd)
|
- [KDE Connect](software/007-kdeconnect.qd)
|
||||||
- [Docker](software/008-docker.qd)
|
- [Docker](software/008-docker.qd)
|
||||||
|
- [Teams for Linux](software/009-teams-for-linux.qd)
|
||||||
|
|||||||
+143
-11
@@ -47,22 +47,44 @@ running in the background is enough. Before the workaround the controller spent
|
|||||||
uptime runtime-suspended, so the race window was being hit constantly.
|
uptime runtime-suspended, so the race window was being hit constantly.
|
||||||
|
|
||||||
Pin the audio controller out of runtime power management so the SoundWire links stay powered and the
|
Pin the audio controller out of runtime power management so the SoundWire links stay powered and the
|
||||||
window never opens.
|
window never opens. This alone is not sufficient — see "Docking re-arms it" below for the second
|
||||||
|
trigger this misses.
|
||||||
|
|
||||||
`/etc/udev/rules.d/99-sof-audio-no-runtime-pm.rules`:
|
.box {Do not do this with udev} type:{warning}
|
||||||
|
A udev rule writing `ATTR{power/control}="on"` on `add|bind` looks like the obvious lever and
|
||||||
|
matches correctly — `udevadm test` confirms it fires for both actions — but it does not hold.
|
||||||
|
`snd_sof_pci` calls `pm_runtime_allow()` from `sof_pci_probe_complete()`, which runs *after* the
|
||||||
|
uevents udev hooks, so the driver reverts `power/control` to `auto` on every boot.
|
||||||
|
|
||||||
|
This cost five further panics between 2026-08-11 and 2026-08-20 while the rule sat in
|
||||||
|
`/etc/udev/rules.d/` looking like active protection.
|
||||||
|
|
||||||
|
Gate the driver instead. Bit 0 of `sof_pci_debug` (`SOF_PCI_DISABLE_PM_RUNTIME`) makes
|
||||||
|
`sof_pci_probe_complete()` return before it reaches the runtime PM setup, so there is no ordering to
|
||||||
|
lose:
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
ACTION=="add|bind", SUBSYSTEM=="pci", ATTR{vendor}=="0x8086", ATTR{device}=="0xa828", ATTR{power/control}="on"
|
sof_pci_probe_complete:
|
||||||
|
testb $0x1, sof_pci_debug ; skip the block entirely when bit 0 is set
|
||||||
|
je ...
|
||||||
|
pm_runtime_set_autosuspend_delay(dev, 0x7d0) ; 2000 ms
|
||||||
|
__pm_runtime_use_autosuspend(dev, 1)
|
||||||
|
pm_runtime_allow(dev) ; this is what reverts power/control to "auto"
|
||||||
```
|
```
|
||||||
|
|
||||||
Matching on the PCI ID `8086:a828` rather than the slot `0000:00:1f.3` keeps the rule valid if the
|
`/etc/modprobe.d/99-sof-no-runtime-pm.conf`:
|
||||||
device is ever renumbered. `add|bind` covers both device registration and driver bind.
|
|
||||||
|
|
||||||
Apply it without rebooting:
|
```txt
|
||||||
|
options snd_sof_pci sof_pci_debug=1
|
||||||
|
```
|
||||||
|
|
||||||
|
`snd_sof_pci` is not in the initramfs, so this needs no `mkinitcpio` run and no re-signing of the
|
||||||
|
unified kernel image. It takes effect on the next boot.
|
||||||
|
|
||||||
|
Close the window immediately on a running system, without waiting for a reboot:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo udevadm control --reload
|
echo on | sudo tee /sys/bus/pci/devices/0000:00:1f.3/power/control
|
||||||
sudo udevadm trigger --action=add /sys/bus/pci/devices/0000:00:1f.3
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify. `control` must read `on` and `runtime_status` must read `active`:
|
Verify. `control` must read `on` and `runtime_status` must read `active`:
|
||||||
@@ -79,14 +101,110 @@ cat /sys/bus/pci/devices/0000:00:1f.3/power/runtime_suspended_time
|
|||||||
cat /sys/bus/pci/devices/0000:00:1f.3/power/runtime_active_time
|
cat /sys/bus/pci/devices/0000:00:1f.3/power/runtime_active_time
|
||||||
```
|
```
|
||||||
|
|
||||||
Confirm the rule also applies on a fresh boot rather than only surviving the manual trigger:
|
Check the ratio after a fresh boot, not just the manual write. Unmitigated, the controller spends
|
||||||
|
around 90% of uptime suspended and cycles constantly rather than settling — roughly 30 seconds of
|
||||||
|
active time accrues in the first 8 minutes, so the race window is being re-entered continuously:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
udevadm test --action=add /sys/bus/pci/devices/0000:00:1f.3 2>&1 | grep power/control
|
awk '{printf "suspended %d%%\n", 100*$1/($1+$2)}' \
|
||||||
|
<(paste /sys/bus/pci/devices/0000:00:1f.3/power/runtime_{suspended,active}_time)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Docking re-arms it
|
||||||
|
|
||||||
|
Pinning the PCI controller is not the end of it. A panic on 2026-08-21 happened with `sof_pci_debug=1`
|
||||||
|
active and `runtime_suspended_time` flat at `0` for the whole session — the PCI-level fix was
|
||||||
|
verified working right up to the crash. What was different: the Dell U2724DE dock had been unplugged
|
||||||
|
and replugged 72 minutes earlier.
|
||||||
|
|
||||||
|
The dock's DP monitors drive an ASoC card-level jack/ELD reconfiguration on every replug — this is
|
||||||
|
card-wide, not SoundWire-specific, since the internal speakers and the DP outputs share the same
|
||||||
|
`sof-soundwire` ALSA card. That reconfiguration independently resumes whichever SoundWire links the
|
||||||
|
card touches. Confirmed by sampling `runtime_active_time` across a reproduced replug: `link.1` and
|
||||||
|
`link.2` (the two `rt1318` speaker amps) went from suspended-and-static to active and climbing within
|
||||||
|
under a minute, while `link.0` (the unused `rt715` mic) stayed suspended throughout. Boot alone never
|
||||||
|
produces this — every link starts out uniformly suspended and stays that way until something asks for
|
||||||
|
audio.
|
||||||
|
|
||||||
|
That asymmetric state — some links live and taking interrupts, one still parked — is exactly the
|
||||||
|
precondition `sdw_intel_thread`'s walk over `cdns == NULL` needs. The PCI-level pin has no visibility
|
||||||
|
into it, because it only prevents the *controller* from ever entering runtime PM; the link and codec
|
||||||
|
devices underneath are separate PM domains that a jack event can wake regardless.
|
||||||
|
|
||||||
|
Pinning those nine devices individually with a udev `add|bind` rule would repeat the mistake already
|
||||||
|
made once with the PCI controller: it would look correct and might still be silently reverted by a
|
||||||
|
driver-internal call this doc hasn't found. Instead, assert the invariant repeatedly rather than trust
|
||||||
|
a single write to hold:
|
||||||
|
|
||||||
|
`/usr/local/bin/pin-sof-soundwire-pm.sh` walks the PCI device, its three `soundwire_intel.link.*`,
|
||||||
|
their `sdw-master-*` children, and every `/sys/bus/soundwire/devices/sdw:*` codec, writing `on` to
|
||||||
|
`power/control` wherever it isn't already set. It's idempotent and only logs when it actually changes
|
||||||
|
something.
|
||||||
|
|
||||||
|
`/etc/systemd/system/pin-sof-soundwire-pm.timer` runs it every 15 seconds, forever, so any revert this
|
||||||
|
doc hasn't identified self-heals within one tick regardless of the mechanism:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=10s
|
||||||
|
OnUnitActiveSec=15s
|
||||||
|
```
|
||||||
|
|
||||||
|
`/etc/udev/rules.d/99-sof-soundwire-links-pm.rules` reasserts it immediately on the dock's own
|
||||||
|
Thunderbolt hotplug event, closing the gap between replug and the next timer tick, since the risky
|
||||||
|
moment is the resume race itself:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
ACTION=="add", SUBSYSTEM=="thunderbolt", ATTR{vendor}=="0xd4", ATTR{device}=="0xc045", \
|
||||||
|
TAG+="systemd", ENV{SYSTEMD_WANTS}+="pin-sof-soundwire-pm.service"
|
||||||
|
```
|
||||||
|
|
||||||
|
.box {Use `+=`, not `=`, for `SYSTEMD_WANTS`} type:{warning}
|
||||||
|
`/usr/lib/udev/rules.d/90-bolt.rules` already sets `ENV{SYSTEMD_WANTS}+="bolt.service"` on the
|
||||||
|
same `thunderbolt` subsystem. A first draft of this rule used `=` instead of `+=`; being numbered
|
||||||
|
`99-` it loaded after bolt's rule and silently clobbered it, so every dock replug stopped
|
||||||
|
requesting `bolt.service` at all. `udevadm test --action=add /sys/bus/thunderbolt/devices/1-1`
|
||||||
|
catches this — check the merged `SYSTEMD_WANTS` line lists both services, not just one.
|
||||||
|
|
||||||
|
Update the Thunderbolt vendor/device match if the dock is ever replaced:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cat /sys/bus/thunderbolt/devices/*/vendor /sys/bus/thunderbolt/devices/*/device \
|
||||||
|
/sys/bus/thunderbolt/devices/*/device_name
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify all nine devices are pinned:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
for d in /sys/bus/pci/devices/0000:00:1f.3 /sys/bus/pci/devices/0000:00:1f.3/soundwire_intel.link.* \
|
||||||
|
/sys/bus/pci/devices/0000:00:1f.3/soundwire_intel.link.*/sdw-master-* \
|
||||||
|
/sys/bus/soundwire/devices/sdw:*; do
|
||||||
|
printf '%-70s %s\n' "$d" "$(cat "$d/power/control")"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recognising it after the fact
|
||||||
|
|
||||||
|
The panic leaves nothing behind, so identify it by shape rather than by trace. `CONFIG_EFI_VARS_PSTORE_DEFAULT_DISABLE`
|
||||||
|
is set and no pstore backend is registered, so `/sys/fs/pstore` and `/var/lib/systemd/pstore` are
|
||||||
|
both empty after a hang.
|
||||||
|
|
||||||
|
A crashed boot ends mid-heartbeat with no shutdown sequence. Compare the last line of each boot: a
|
||||||
|
clean shutdown ends in `Unmounted /efi` or `Stopping Flush Journal to Persistent Storage`, while a
|
||||||
|
panic ends on whatever was logging periodically — usually `kdeconnectd`, exactly on its 30-second
|
||||||
|
cadence, with the next tick missing:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
for b in $(journalctl --list-boots -q | awk '{print $1}'); do
|
||||||
|
printf "%4s %s\n" "$b" "$(journalctl -b "$b" -o short-iso -q | tail -1)"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
The machine is always idle when it dies, not under load, and the journal is silent on the kernel side
|
||||||
|
for the entire session — `journalctl -b -1 -k -p 4` shows only the usual boot-time noise.
|
||||||
|
|
||||||
.box {This is a mitigation, not a fix} type:{warning}
|
.box {This is a mitigation, not a fix} type:{warning}
|
||||||
The driver defect is still present. The rule only stops it from being triggered.
|
The driver defect is still present. Disabling runtime PM only stops it from being triggered.
|
||||||
|
|
||||||
If the freezes return, bypass SoundWire entirely by forcing the legacy HDA driver in
|
If the freezes return, bypass SoundWire entirely by forcing the legacy HDA driver in
|
||||||
`/etc/modprobe.d/`:
|
`/etc/modprobe.d/`:
|
||||||
@@ -106,6 +224,20 @@ udevadm test --action=add /sys/bus/pci/devices/0000:00:1f.3 2>&1 | grep power/co
|
|||||||
The hardware is fine too: no machine check exceptions, no ECC or EDAC errors, and `fwupdmgr`
|
The hardware is fine too: no machine check exceptions, no ECC or EDAC errors, and `fwupdmgr`
|
||||||
reports all firmware current.
|
reports all firmware current.
|
||||||
|
|
||||||
|
The battery charge-controller fault is a separate problem and not the cause of these hangs. It
|
||||||
|
presents the same way from the outside — the machine dies with no clean shutdown and nothing in
|
||||||
|
the journal — so check it off explicitly. In a SoundWire panic the battery is untouched
|
||||||
|
(`charge_now == charge_full` on the next boot) and `battery-charge-watchdog` logs only its
|
||||||
|
startup line, never a `FAULT`.
|
||||||
|
|
||||||
|
The two did co-occur on 2026-08-21 — the fault fired for 101 minutes that morning, resolving
|
||||||
|
itself 8 seconds before the dock was unplugged, with the panic following 72 minutes later — but
|
||||||
|
the mechanisms are unrelated. Same dock event, two independent bugs.
|
||||||
|
|
||||||
|
A dock replug on the same day also reproduced `pcieport 0000:39:00.0: not ready 1023ms after
|
||||||
|
resume; giving up`. Not yet investigated; noted here since it fires on the same trigger as the
|
||||||
|
SoundWire re-arm above and could be mistaken for a symptom of it.
|
||||||
|
|
||||||
Capturing a fresh trace would need `efi_pstore.pstore_disable=0` on the kernel command line,
|
Capturing a fresh trace would need `efi_pstore.pstore_disable=0` on the kernel command line,
|
||||||
which means creating `/etc/kernel/cmdline`, regenerating the unified kernel image and re-signing
|
which means creating `/etc/kernel/cmdline`, regenerating the unified kernel image and re-signing
|
||||||
it. `CONFIG_EFI_VARS_PSTORE_DEFAULT_DISABLE` is set, so panics are not persisted by default.
|
it. `CONFIG_EFI_VARS_PSTORE_DEFAULT_DISABLE` is set, so panics are not persisted by default.
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
.docname {Teams for Linux}
|
||||||
|
.include {docs}
|
||||||
|
|
||||||
|
Microsoft does not publish a native Teams client for Linux. [Teams for Linux][teams-for-linux] is an
|
||||||
|
unofficial [Electron][electron] wrapper around the Teams web app that provides a standalone desktop
|
||||||
|
client, system tray integration and native notifications.
|
||||||
|
|
||||||
|
Install it with `yay -S teams-for-linux-bin`.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The configuration is stored in `~/.config/teams-for-linux/config.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"notificationMethod": "web",
|
||||||
|
"electronCLIFlags": [
|
||||||
|
["enable-features", "WebRtcPipeWireCamera"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `notificationMethod: web` uses the browser notification API of the embedded web app instead of the
|
||||||
|
Electron notification API.
|
||||||
|
- `WebRtcPipeWireCamera` enables the PipeWire camera backend, which is required for the webcam to
|
||||||
|
work under Wayland.
|
||||||
|
|
||||||
|
## Opening meeting links in the app
|
||||||
|
|
||||||
|
Clicking a Teams meeting link in the [Thunderbird](002-thunderbird.qd) or Outlook web interface opens
|
||||||
|
the meeting in the browser instead of in Teams for Linux. Getting the handover to work requires a
|
||||||
|
userscript, because the desktop integration alone is not sufficient.
|
||||||
|
|
||||||
|
### Verify the protocol handler
|
||||||
|
|
||||||
|
The package registers the `msteams:` URI scheme through its desktop entry, so this part usually works
|
||||||
|
out of the box:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gio mime x-scheme-handler/msteams
|
||||||
|
```
|
||||||
|
|
||||||
|
The output must name `teams-for-linux.desktop` as the default application. If it does not, register it
|
||||||
|
with `xdg-mime default teams-for-linux.desktop x-scheme-handler/msteams`.
|
||||||
|
|
||||||
|
A deep link can be tested directly from a shell. This opens the meeting in Teams for Linux:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xdg-open 'msteams:/meet/<meeting-id>?p=<passcode>'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why the browser never asks
|
||||||
|
|
||||||
|
Meeting invitations do not contain `msteams:` links. They contain plain HTTPS links in the form
|
||||||
|
`https://teams.microsoft.com/meet/<meeting-id>?p=<passcode>`. An HTTPS URL belongs to the browser,
|
||||||
|
so nothing is ever handed to an external application.
|
||||||
|
|
||||||
|
That URL answers with a redirect to a launcher page:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
HTTP/2 302
|
||||||
|
location: /dl/launcher/launcher.html?url=%2F_%23%2Fmeet%2F<meeting-id>%3Fp%3D<passcode>%26anon%3Dtrue&type=meet&directDl=true&msLaunch=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Because the redirect happens on the server, no document is ever created for the `/meet/` URL and a
|
||||||
|
userscript cannot match on it. The launcher page is the first document that actually loads, and its
|
||||||
|
`url` query parameter carries the deep link path, prefixed with `/_#`. Stripping that prefix yields
|
||||||
|
exactly the path the `msteams:` scheme expects.
|
||||||
|
|
||||||
|
### Userscript
|
||||||
|
|
||||||
|
Install [Tampermonkey][tampermonkey] in [Librewolf](001-librewolf.qd) and add the following userscript.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// ==UserScript==
|
||||||
|
// @name Teams links to teams-for-linux
|
||||||
|
// @match https://teams.microsoft.com/dl/launcher/launcher.html*
|
||||||
|
// @match https://teams.microsoft.com/meet/*
|
||||||
|
// @match https://teams.microsoft.com/l/*
|
||||||
|
// @run-at document-start
|
||||||
|
// @grant GM_info
|
||||||
|
// ==/UserScript==
|
||||||
|
(() => {
|
||||||
|
let path;
|
||||||
|
if (location.pathname.startsWith('/dl/launcher/')) {
|
||||||
|
const u = new URLSearchParams(location.search).get('url');
|
||||||
|
if (!u) return;
|
||||||
|
path = u.replace(/^\/_#/, '');
|
||||||
|
} else {
|
||||||
|
path = location.pathname + location.search;
|
||||||
|
}
|
||||||
|
if (!/^\/(meet|l)\//.test(path)) return;
|
||||||
|
|
||||||
|
window.stop();
|
||||||
|
location.href = 'msteams:' + path;
|
||||||
|
setTimeout(() => {
|
||||||
|
window.close();
|
||||||
|
location.replace('about:blank');
|
||||||
|
}, 1500);
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
Three details in this script are not obvious and the script silently stops working without them:
|
||||||
|
|
||||||
|
- `@grant GM_info` forces Tampermonkey to run the script in its sandboxed content script scope. With
|
||||||
|
`@grant none` the script is injected into the page as an inline `<script>` element and is then
|
||||||
|
blocked by the launcher page, which sends a `script-src 'nonce-...'` content security policy.
|
||||||
|
The script still matches, but never executes.
|
||||||
|
- `window.stop()` aborts the launcher page before its own scripts are parsed. Otherwise the page
|
||||||
|
navigates on to `https://teams.microsoft.com/v2/`, which destroys the document and discards the
|
||||||
|
pending timeout, so the tab is never cleaned up. It must be called **before** the `msteams:`
|
||||||
|
assignment, because calling it afterwards cancels the handover itself.
|
||||||
|
- `@run-at document-start` is what makes the above possible in the first place.
|
||||||
|
|
||||||
|
On the first meeting link the browser asks which application should open the link. Select Teams for
|
||||||
|
Linux and enable `Always allow teams.microsoft.com to open links of this type`. The dialog is
|
||||||
|
tab-modal, so if it is still open when the tab closes it disappears together with the tab.
|
||||||
|
|
||||||
|
> Tip: The tab is closed by `window.close()` even though `dom.allow_scripts_to_close_windows` is
|
||||||
|
> `false` by default. Outlook opens meeting links through `window.open()`, and Firefox permits a
|
||||||
|
> script to close windows that were opened by a script. The `location.replace('about:blank')` call
|
||||||
|
> is the fallback for tabs that were opened another way.
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
Two upstream changes can break this and are worth checking first if meeting links stop working:
|
||||||
|
|
||||||
|
- Microsoft changing the launcher URL layout. The `.replace(/^\/_#/, '')` call encodes the current
|
||||||
|
`/_#/meet/...` format of the `url` query parameter.
|
||||||
|
- Teams for Linux tightening its deep link validation. The accepted schemes are defined by these
|
||||||
|
regular expressions in the application:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
^msteams:/(?:meet/|l/(?:app|call|channel|chat|entity|file|meet(?:ing|up-join)|message|task|team)/)
|
||||||
|
^msteams://teams\.(?:microsoft\.com|live\.com|cloud\.microsoft)/(?:meet/|l/(?:app|call|channel|chat|entity|file|meet(?:ing|up-join)|message|task|team)/)
|
||||||
|
```
|
||||||
|
|
||||||
|
[teams-for-linux]: https://github.com/IsmaelMartinez/teams-for-linux
|
||||||
|
[electron]: https://www.electronjs.org
|
||||||
|
[tampermonkey]: https://www.tampermonkey.net
|
||||||
Reference in New Issue
Block a user