Compare commits

...
6 Commits
Author SHA1 Message Date
Frank Zechert d34a6e5901 update bug fix 2026-08-21 12:25:43 +02:00
Frank Zechert 4ba1d1317f added teams-for-linux 2026-08-20 15:36:53 +02:00
Frank Zechert 67c4ae45a1 hardware bugs in xps13 2026-08-20 15:36:38 +02:00
Frank Zechert 1c95257369 add teams-for-linux 2026-08-19 10:10:26 +02:00
Frank Zechert 520ce45a50 update documentation 2026-08-15 17:55:33 +02:00
Frank Zechert 39a16e8650 update software documentation 2026-08-09 19:14:18 +02:00
8 changed files with 604 additions and 0 deletions
+6
View File
@@ -9,6 +9,9 @@
- [Firmware Updates](hardware/001-firmware-updates.qd)
- [Thunderbolt](hardware/002-thunderbolt.qd)
- [Power Profiles](hardware/003-power-profiles.qd)
- [Keychron Keyboard](hardware/004-keychron.qd)
- [XPS 13 9350 (2024)](hardware/005-xps13-9350.qd)
###! Software Configuration
@@ -18,3 +21,6 @@
- [Signal](software/004-signal.qd)
- [Telegram](software/005-telegram.qd)
- [GnuPG](software/006-opengpg.qd)
- [KDE Connect](software/007-kdeconnect.qd)
- [Docker](software/008-docker.qd)
- [Teams for Linux](software/009-teams-for-linux.qd)
+23
View File
@@ -0,0 +1,23 @@
.docname {Keychron Keyboard}
.include {docs}
# Keychron Keyboard
To enable access to the keyboard for the QMK launcher (or similar programs), set the following udev rule in
`/etc/udev/rules.d/99-keychron.conf`
```
# The Keyboard Itself
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="3434", ATTRS{idProduct}=="0aa1", MODE="0666", TAG+="uaccess", TAG+="udev-acl"
# The 2.4 Ghz Wireless Dongle
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="3434", ATTRS{idProduct}=="d030", MODE="0666", TAG+="uaccess", TAG+="udev-acl"
```
Values for `idVendor` and `idProduct` can be found with `lsusb | grep -i Keychron`.
Reload the udev rules `udevadm control --reload-rules`
## Firmware Updates
To enable firmware updates, install `yay -S dfu-util` to provide the required toolbox.
+414
View File
@@ -0,0 +1,414 @@
.docname {XPS 13 9350 (2024)}
.include {docs}
## Sound
Install the firmware `sof-firmware`
### Random Freezes (SoundWire Kernel Panic)
On kernel `7.1.x` the machine hard-hangs every day or two. The internal panel freezes on its last
frame, the external DisplayPort monitors go black, the LED above the keyboard starts blinking, and
nothing at all is written to the journal. Only a long press on the power button recovers it.
The cause is a NULL pointer dereference in the Intel SoundWire interrupt handler.
`sdw_intel_thread` walks the SoundWire link list and calls `sdw_cdns_irq(irq, link->cdns)` while the
links are powered back up after a runtime suspend. One link is still listed with `cdns == NULL`, so
the handler dereferences NULL. The kernel dies in interrupt context, which is why the journal stays
empty — journald never gets a chance to flush.
.collapse {Panic trace}
```txt
Oops: general protection fault, kernel NULL pointer dereference 0x3c0: 0000 [#1] SMP NOPTI
CPU: 3 UID: 0 PID: 807 Comm: irq/146-AudioDS
Hardware name: Dell Inc. XPS 13 9350/0MMW13, BIOS 1.22.0 06/01/2026
RIP: 0010:sdw_cdns_irq+0x9/0x290 [soundwire_cadence]
RSI: 0000000000000000
Call Trace:
<TASK>
sdw_intel_thread+0x3b/0x70 [soundwire_intel]
hda_dsp_interrupt_thread+0x99/0x380 [snd_sof_intel_hda_generic]
irq_thread_fn+0x25/0x60
irq_thread+0x1cb/0x340
kthread+0xe4/0x120
ret_from_fork+0x2a7/0x330
</TASK>
kernel tried to execute NX-protected page - exploit attempt? (uid: 0)
BUG: unable to handle page fault for address: ffff88f18fab5340
Oops: Oops: 0011 [#2] SMP NOPTI
```
The faulting instruction is `cmpb $0x0, 0x3c0(%rsi)` with `RSI = 0`. The second fault is what makes
the hang unrecoverable rather than just killing the interrupt thread.
Anything that repeatedly wakes the audio controller makes this far more likely — a Spotify stream
running in the background is enough. Before the workaround the controller spent roughly 89% of its
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
window never opens. This alone is not sufficient — see "Docking re-arms it" below for the second
trigger this misses.
.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
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"
```
`/etc/modprobe.d/99-sof-no-runtime-pm.conf`:
```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
echo on | sudo tee /sys/bus/pci/devices/0000:00:1f.3/power/control
```
Verify. `control` must read `on` and `runtime_status` must read `active`:
```sh
cat /sys/bus/pci/devices/0000:00:1f.3/power/{control,runtime_status}
```
From this point `runtime_suspended_time` must stop increasing, with all further time accruing to
`runtime_active_time`. If it keeps climbing, the pin is not actually holding:
```sh
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
```
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
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}
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
`/etc/modprobe.d/`:
```txt
options snd-intel-dspcfg dsp_driver=1
```
This costs the internal speakers and the internal microphone array, since both sit behind
SoundWire (`rt1318` amplifiers and an `rt715` microphone). USB and HDMI/DisplayPort audio keep
working.
.box {Ruled out} type:{note}
`power-profiles-daemon` is not the cause, despite the timing looking suspicious — most of the
panics predate its installation. See [Power Profiles](003-power-profiles.qd).
The hardware is fine too: no machine check exceptions, no ECC or EDAC errors, and `fwupdmgr`
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,
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.
## Camera
The camera, while available on free software and without drivers,
produces a very bad, raw image with green tint.
To have a higher quality, we need to install a custom intel
driver.
Install `linux-firmware-intel libcamera-tools dkms linux-headers intel-vision-drivers-dkms-git pipewire-libcamera`.
Make sure that the `intel_cvs` kernel module (and dependency) is loaded:
`/etc/modules-load.d/intel_cvs.conf`:
```txt
gpio_ljca
usb_ljca
intel_cvs
```
After rebooting `cam -l` should show `1: Internal front camera (\_SB_.LINK1)`
after some warnings. `qcam` should start a raw camera stream.
The raw camera stream will have a greenish tint.
> Note: The Intel Vision Driver module must already be loaded when the ov02c10 sensor is probed.
> If you cannot see your camera, run `rmmod ov02c10` && `modprobe ov02c10` as root and try again.
For Firefox, open about:config (See Firefox#Configuration) and set media.webrtc.camera.allow-pipewire to "True".
For Chromium, enable the flag enable-webrtc-pipewire-camera in chrome://flags/ or set it in your configuration file.
### Proprietary Hardware Stack
The proprietary hardware stack requires additional components to be installed and configured,
but it provides substantially better video quality vs using only open source drivers and software.
First ensure that the above steps are complete at least to the point where qcam shows a raw camera stream.
Download `intel-ipu7-dkms-git` and modify the PKBUILD file.
```sh
mkdir ~/.camera_driver
cd ~/.camera_driver
git clone https://aur.archlinux.org/intel-ipu7-dkms-git.git
cd intel-ipu7-dkms-git
nvim PKGBUILD
```
- remove the dependency `intel-ivsc-firmware` from the `depends` section.
- Change `source=("git+${url}")` to instead point to a specific commit and
add a patch file to it
`source=("git+https://github.com/intel/ipu7-drivers.git#commit=77e3a0065697314cc7437a6eefd7e0d36ab06a4b" "https://patch-diff.githubusercontent.com/raw/intel/ipu7-drivers/pull/67.patch")`
- Add another `'SKIP'` to `sha256sums`
- In the `prepare()` section, apply the patch: `patch -Np1 -i ../67.patch`
Here is the pkgfile
```sh
# Maintainer: Yamada Hayao <shun819.mail@gmail.com,>
# Contributer: Karim Vergnes <me@thesola.io>
pkgname=intel-ipu7-dkms-git
_pkgname=ipu7-drivers
pkgver=r77.77e3a00
pkgrel=1
pkgdesc="Intel IPU7 camera drivers (DKMS)"
arch=('any')
url="https://github.com/intel/${_pkgname}"
license=('unknown')
depends=('dkms')
provides=('intel-ivsc-driver-dkms-git' 'intel-ipu7-dkms')
conflicts=('intel-ivsc-driver-dkms-git')
makedepends=('git')
source=("git+https://github.com/intel/ipu7-drivers.git#commit=77e3a0065697314cc7437a6eefd7e0d36ab06a4b" "https://patch-diff.githubusercontent.com/raw/intel/ipu7-drivers/pull/67.patch")
sha256sums=('SKIP' 'SKIP')
pkgver() {
cd $_pkgname
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
prepare() {
cd "$srcdir/$_pkgname"
patch -Np1 -i ../67.patch
sed -i "s/^PACKAGE_VERSION=\".*\"$/PACKAGE_VERSION=\"$pkgver\"/g" ./dkms.conf
}
package() {
cd "$srcdir"
install -dm755 "$pkgdir/usr/src"
cp -rT "$_pkgname" "$pkgdir/usr/src/$_pkgname-$pkgver"
}
```
Run the package build and install
- `makepkg --syncdeps`
- `makepkg --install`
Install `intel-ipu7-camera-hal-git`
Download `icamerasrc-git` and modify the PKBUILD file.
```sh
cd ~/.camera_driver
git clone https://aur.archlinux.org/icamerasrc-git.git
cd icamerasrc-git
nvim PKGBUILD
```
- replace dpendency `ipu6-camera-hal` with `ipu7-camera-hal`
Run the package build and install
- `makepkg --syncdeps`
- `makepkg --install`
### Config Files
Make sure that the kernel modules load in the correct order
Remove the file `/etc/modules-load.d/intel_cvs.conf`.
Create the file `/etc/modprobe.d/90-ipu7-usbio-order.conf` with
```txt
softdep intel_ipu7 pre: usbio gpio_usbio i2c_usbio intel_cvs intel_skl_int3472_discrete
```
Create the file `/usr/lib/udev/rules.d/90-ipu7-psys.rules`
```txt
KERNEL=="ipu7-psys0", MODE="0666", SYMLINK+="ipu-psys0"
```
Reboot the system.
Check that the camera is now working with
```sh
gst-launch-1.0 -v icamerasrc ! video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1 ! videoconvert ! videoflip method=rotate-180 ! waylandsink
```
### V4L2 Relay
Now, we can create a V4L2 loopback device that presents the gstreamer/icamerasrc stream when activated.
Install `v4l2-relayd`.
Create the following file to instantiate the virtual v4l2loopback device on boot:
`/etc/modprobe.d/v4l2loopback.conf`:
```txt
options v4l2loopback card_label="Intel IPU7 Virtual Camera"
```
Create the systemd service file for the relay.
`/etc/v4l2-relayd.d/intel-ipu.conf`
```ini
VIDEOSRC="icamerasrc device-name=ov02c10-uf ! video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1 ! videoconvert ! videoflip method=rotate-180"
FORMAT=NV12
WIDTH=1920
HEIGHT=1080
FRAMERATE=30/1
CARD_LABEL="Intel IPU7 Virtual Camera"
```
Next, enable the `v4l2-relayd@intel-ipu.service`.
After restarting your machine, any supported applications should see your camera as a standard V4L2 device with wide support.
+6
View File
@@ -41,6 +41,12 @@ A system deployed with fz-stack **will not**:
## Todos
- node version in starship anzeigen
- node / nvm / npm version inspect
- Bluetooth
- Workspace to monitor assignment and setup
- Windows VM aufsetzen
[arch-linux]: https://archlinux.org
[arch-linux-wiki]: https://wiki.archlinux.org/title/Main_page
[hyprland]: https://hypr.land
+3
View File
@@ -87,3 +87,6 @@ Download and Install the following extensions
Check <https://github.com/catppuccin/userstyles/tree/main>.
Usage guide: <https://userstyles.catppuccin.com/getting-started/usage/>
## Enable PWAs
Enable PWAs by setting `browser.taskbarTabs.enabled` to `true` in `about:config`
+5
View File
@@ -0,0 +1,5 @@
.docname {KDE Connect}
.include {docs}
KDE connect provides features to connect to Android.
Install `kdeconnect`
+7
View File
@@ -0,0 +1,7 @@
.docname {Docker}
.include {docs}
Install the package `docker`. Make sure that the user is in the group `docker`:
`sudo usermod -aG docker $USER`.
If the group does not yet exist, it can be created with `groupadd -r docker`.
+140
View File
@@ -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