ci / rust (push) Failing after 12s
windows-drivers / probe-and-proto (push) Successful in 48s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Successful in 1m6s
deb / build-publish-client-arm64 (push) Failing after 10s
decky / build-publish (push) Successful in 47s
windows-drivers / driver-build (push) Successful in 1m40s
apple / swift (push) Successful in 3m6s
ci / bench (push) Successful in 7m39s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 1m0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8m2s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m0s
android / android (push) Successful in 12m28s
deb / build-publish (push) Successful in 12m13s
ci / rust-arm64 (push) Successful in 12m31s
arch / build-publish (push) Successful in 12m40s
deb / build-publish-host (push) Successful in 12m17s
windows-host / package (push) Successful in 18m26s
windows-host / winget-source (push) Skipped
apple / screenshots (push) Successful in 23m25s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m34s
docker / build-push-arm64cross (push) Successful in 8s
docker / deploy-docs (push) Successful in 31s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m24s
A LocalService principal could take over a virtual pad's shared input section and
forge HID input into the interactive desktop.
The host duplicates each pad's unnamed DATA section into the driver's WUDFHost, and
through gamepad proto v2 it learned that process from `driver_pid` in the named
bootstrap mailbox. That mailbox has to be LocalService-writable — that is what the
driver's own WUDFHost runs as — and the delivery gate, verify_is_wudfhost, only checks
that the target's IMAGE is %SystemRoot%\System32\WUDFHost.exe. That image is
world-executable. So anything running as LocalService — notably the deliberately
de-privileged plugin runner — could spawn its own WUDFHost (CREATE_SUSPENDED parks it
indefinitely with the right image path), publish that pid, and be handed
SECTION_MAP_READ|WRITE on a live section. For pf-mouse that section drives a real
absolute pointer, so it was desktop control; for the pads it was forged gamepad input
plus a read of the remote user's controller state.
The module docs claimed mailbox tampering "yields at worst a gamepad DoS, never a read
or an injection". That was wrong, and the reasoning behind it — that a LocalService
token is DACL-denied OpenProcess on a UMDF WUDFHost — only covers the REAL host, not
one the attacker spawned itself.
The pid now comes from the device stack (ChannelProof, proto 2 -> 3). The host asks the
devnode it SwDeviceCreate'd who is serving it, looked up by the instance id PnP handed
back, so a planted look-alike devnode is not a candidate and the kernel — not anything
the attacker supplies — does the routing. Only the driver PnP actually bound to that
device can answer. `driver_pid` survives as a liveness hint; a tamperer can still deny a
pad, which squatting the name always allowed, but can no longer choose the recipient.
Two rules keep the state machine honest around it: a delivery stands until its target
process EXITS (judged on a retained SYNCHRONIZE handle, so a recycled pid cannot fake
it, and UMDF's restart-after-driver-crash still re-attaches), and a pad with no
SwDeviceCreate devnode refuses to deliver rather than fall back — unless an operator
sets PUNKTFUNK_PAD_CHANNEL_TRUST_MAILBOX, which says so loudly.
Three transports, because Windows carries different things to different driver shapes,
and the obvious two did not survive contact with hidclass. Measured on .173 (Win11
26200): HidD_GetIndexedString is NOT forwarded to a UMDF HID minidriver at all — it
failed for every index including ones the driver demonstrably serves through the named
wrappers; and a private device interface registers and enumerates but cannot be OPENED
(ERROR_GEN_FAILURE), because hidclass owns IRP_MJ_CREATE on a devnode it is the FDO for.
That is exactly why pf-xusb was never affected: it is not a HID minidriver, so nothing
sits above it. What works:
* pf-xusb — a private IOCTL on its own GUID_DEVINTERFACE_XUSB.
* pf-mouse — the HID serial string. Verified: PFCP:3:0:7296, and 7296 was a genuine
service-spawned WUDFHost.exe. Safe here alone: nothing reads the virtual
mouse's serial, whereas a pad's is SDL/Steam dedup material.
* pf-gamepad — a HID feature report, and it cost NO report-descriptor change. The
captured descriptors already declare far more Feature ids than the driver
ever served: 0x85 is declared on DualSense, DualShock 4 and Edge alike and
used to fail with STATUS_INVALID_PARAMETER, so hidclass lets it through and
nothing can have depended on the old failure. The Deck's one feature report
is unnumbered and Steam drives it command->response, so its proof rides that
existing contract via a private two-byte command. Verified: feature 0x85
returned magic "PFCP", proto 3, pad_index 0, wudf_pid 18456 — and 18456 was
a WUDFHost — with the product string still 'DualSense Wireless Controller'.
Also renamed pf-dualsense -> pf-gamepad. One driver has always served four identities, so
the old name read as if the other three lived elsewhere. ONLY the package identity moved
(crate, INF/CAT/DLL, UMDF service, build script, CI lines, log file, env var). The four
HARDWARE IDS are deliberately unchanged — they bind every devnode the host creates and
every installed system — as are the Global\pfds-boot-<i> mailbox and PAD_MAGIC, which are
wire contract. `driver install --gamepad` now retires the pre-rename store package first,
matched on pf_dualsense.dll because that string appears only in the OLD inf; matching on
the hardware ids would delete what we are about to install. On .173 that separated 14
stale packages from the 1 new one with 0 ambiguous, and the renamed package binds the old
hwid (devgen root\pf_dualsense -> oem143.inf = pf_gamepad.inf).
The repo's own pre-commit/pre-push rustfmt hooks named the old crate, so they caught the
rename before the commit did — they now check pf-gamepad, and pf-mouse alongside it, which
they had been missing relative to the CI line.
Host and drivers MUST ship together: v2<->v3 fails closed in both directions by design,
with the existing "update host + drivers together" diagnostic.
The rename moved files that also carry the security change, so splitting this into two
commits would mean reconstructing an intermediate state that was never gated. It is one
commit on purpose.
Gated on the windows-amd64 runner with cargo clean first (the box's clock lags, so stale
artifacts would read as a vacuous green): clippy -D warnings clean for pf-inject,
pf-capture and pf-driver-proto, drivers workspace build + the CI clippy line clean,
cargo check --release -p punktfunk-host clean, 19 + 58 tests green. Also fixes pf-mouse
still writing its debug log to world-writable C:\Users\Public, which the 2026-07-17
review moved for the other three drivers and missed here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
191 lines
15 KiB
Markdown
191 lines
15 KiB
Markdown
# Windows host packaging — signed Inno Setup installer
|
||
|
||
A one-file, signed `setup.exe` for the punktfunk streaming **host** on Windows, published to Gitea's
|
||
generic package registry (`punktfunk-host-windows`) by `.gitea/workflows/windows-host.yml`.
|
||
|
||
> Full picture (drivers-from-source, toolchain, CI, dev loop): **punktfunk-planning: `windows-build-and-packaging.md`** (internal planning repo). This README is the `packaging/windows/` file index.
|
||
|
||
## Windows 11 22H2+ only (no Windows 10)
|
||
|
||
The installer refuses anything below **Windows 11 22H2 (build 22621)** — `MinVersion=10.0.22621` in
|
||
`punktfunk-host.iss`, with a `[Messages]` override naming the requirement. The floor comes from the
|
||
**pf-vdisplay** driver: it is built against the **IddCx 1.10** class extension (the HDR `*2` DDIs +
|
||
the FP16 adapter cap, linked via the 1.10 `IddCxStub`, no runtime `IddCxGetVersion` downgrade), and
|
||
IddCx 1.10 first shipped in Windows 11 22H2. On older Windows — **all of Windows 10 including LTSC,
|
||
and Windows 11 21H2** — the driver *package* installs fine, but the device then fails to start with
|
||
**Code 10 `STATUS_DEVICE_POWER_FAILURE`** in Device Manager and every session dies with "pf-vdisplay
|
||
driver interface not found". Gating the installer turns that late, confusing failure into an upfront
|
||
message. (Down-level SDR-only support would need a runtime IddCx version check in the driver —
|
||
tracked as a possible future feature, not planned.)
|
||
|
||
## x64 only (no ARM64)
|
||
|
||
Unlike the client (which ships x64 + ARM64 MSIX), the host is **x64-only by design**. It is coupled to
|
||
an NVIDIA GPU (NVENC, via `nvEncodeAPI64.dll` from the driver) and the **pf-vdisplay** virtual-display
|
||
driver — neither exists on Windows ARM64 (no ARM64 NVIDIA driver; the driver builds x64-only). An
|
||
ARM64 host would install but couldn't encode or create a virtual display, so we don't build one.
|
||
Revisit if NVIDIA-ARM Windows PCs ever ship.
|
||
|
||
## Why not MSIX (like the client)
|
||
|
||
The host installs a **`LocalSystem` SCM service** that `CreateProcessAsUserW`'s from Session 0 into the
|
||
interactive session for secure-desktop (UAC / lock screen) capture, adds firewall rules, and depends
|
||
on the **pf-vdisplay** UMDF/IDD virtual-display driver. MSIX's sandbox can install **neither** a SYSTEM
|
||
service of this kind **nor** a driver. So the host ships as a classic elevated installer.
|
||
|
||
The installer is deliberately thin: the real install logic lives in `punktfunk-host` subcommands, not
|
||
in PowerShell — `service install` (SCM registration, firewall rules, the default `host.env`, the
|
||
SYSTEM→interactive-session supervisor; `service.rs`), `driver install [--gamepad]` and `web setup`
|
||
(driver/console provisioning; `windows/install.rs`). The installer lays the exe into
|
||
`C:\Program Files\punktfunk\` and calls those subcommands elevated. Keeping the logic in the compiled
|
||
exe — not a `.ps1` *file* PowerShell reads in the machine codepage — is the fix for the ANSI-codepage
|
||
parse breakage that silently failed installs on non-English boxes.
|
||
|
||
## What the installer does
|
||
|
||
- Installs `punktfunk-host.exe` (+ `host.env.example`, this README) to `{app}` (`C:\Program Files\punktfunk`).
|
||
- **Optional task** *Install the pf-vdisplay virtual display driver* — `punktfunk-host.exe driver install`
|
||
imports the driver's self-signed cert (machine `Root` + `TrustedPublisher`), creates the
|
||
`root\pf_vdisplay` device node (only if absent, via nefconc — never devgen), and stages the driver with
|
||
`pnputil /add-driver /install`.
|
||
Best-effort: a driver failure warns but never aborts the install (the host degrades to a physical
|
||
display without it).
|
||
- Runs `punktfunk-host service install` (idempotent; writes a default `host.env` only if absent, so
|
||
user config survives upgrades) and, by the *Start service now* task, `service start`.
|
||
- **Web management console** (bundled when packed with `-WebDir`/`-BunExe`, which the CI always is):
|
||
lays down the built **self-contained** `.output` server (Nitro `noExternals` — deps bundled +
|
||
tree-shaken, ~75 files, no `node_modules`) + a portable **bun**, prompts for a console login
|
||
password (pre-filled with a secure random default, shown again on the final page; kept on upgrade),
|
||
then `punktfunk-host.exe web setup` writes the ACL'd `%ProgramData%\punktfunk\web-password`, registers the
|
||
**`PunktfunkWeb`** scheduled task (boot, SYSTEM, restart-on-failure → `web-run.cmd` → `bun` on
|
||
`:47992`), opens TCP 47992, and starts it. It proxies the host's loopback mgmt API with the host's
|
||
own `%ProgramData%\punktfunk\mgmt-token`.
|
||
- **GameStream (Moonlight) compatibility is a wizard task** (**unchecked** by default — it pairs over
|
||
plain HTTP, so it is opt-in like the Public-firewall task): the choice is passed to
|
||
`service install --gamestream=on|off`, which writes `PUNKTFUNK_HOST_CMD=serve --gamestream` (or
|
||
`serve`, the secure native-only host) into `host.env`. Unattended, add it with
|
||
`/MERGETASKS=gamestream`. Upgrade-safe: a hand-customized `PUNKTFUNK_HOST_CMD` is never
|
||
overwritten, and on an upgrade the task is inert entirely (the flag is omitted, so `host.env`
|
||
keeps whatever it already says) — change an existing host with
|
||
`punktfunk-host service install --gamestream=on|off` plus a service restart.
|
||
- **Branded, modern wizard**: `WizardStyle=modern dynamic windows11` (Inno ≥ 6.6 — Windows-11-style
|
||
controls following the system light/dark theme; pre-6.6 compilers fall back to plain `modern`), with
|
||
the punktfunk lens mark on the side panel / header tile and a multi-size `punktfunk.ico`
|
||
(`SetupIconFile` + the Apps & features entry). Assets are generated **and committed** by
|
||
`branding/gen-branding.ps1` from the canonical brand geometry (`web/src/components/brand-mark.tsx`);
|
||
re-run it only when the brand changes.
|
||
- **Upgrade:** stops a running `PunktfunkHost` service and waits for `STOPPED` before replacing files
|
||
(otherwise the locked exe / respawning supervisor would block the copy), then re-points the service;
|
||
the existing console password is kept (the wizard page is skipped).
|
||
- **Uninstall** (Add/Remove Programs): runs `service uninstall` (stop + delete service + remove
|
||
firewall rules), removes the `PunktfunkWeb` task + its firewall rule, then `driver uninstall` (+
|
||
`--gamepad`) removes the punktfunk virtual-device drivers — the pf-vdisplay device node(s) and the
|
||
pf-vdisplay / pf-gamepad / pf-xusb driver-store packages (the field report was that they survived
|
||
uninstall). **VB-CABLE is intentionally NOT removed** (a third-party shared component the user may
|
||
use elsewhere — its own uninstaller is `VBCABLE_Setup_x64.exe -u -h`); the `%ProgramData%\punktfunk`
|
||
config (incl. `web-password`) is also left in place.
|
||
|
||
Silent install: `punktfunk-host-setup-<ver>.exe /VERYSILENT` (omit the driver with
|
||
`/MERGETASKS="!installdriver"`; disable Moonlight compat with `/MERGETASKS="!gamestream"`). A silent
|
||
fresh install uses the generated random console password — read it from
|
||
`%ProgramData%\punktfunk\web-password`.
|
||
|
||
## Prerequisites on the target box
|
||
|
||
- A **GPU for hardware encode**: an NVIDIA GPU + driver (NVENC), an AMD GPU (native AMF), or an
|
||
Intel GPU (native QSV via the statically linked VPL dispatcher; the runtime ships in the Intel
|
||
driver) — the CI exe is built `--features nvenc,amf-qsv,qsv`. Software H.264 is the GPU-less
|
||
fallback.
|
||
- **Virtual gamepads need no prerequisite.** The DualSense / DualShock 4 / Xbox 360 (XUSB) UMDF drivers
|
||
are **bundled** in the installer (the *Install the virtual gamepad drivers* task) and
|
||
`pnputil`-installed. **ViGEmBus is no longer used.**
|
||
- **The streaming microphone uses VB-CABLE**, bundled + silently installed by the installer (the *Install
|
||
VB-CABLE virtual audio* task). The host writes the client's mic into VB-CABLE's input; its `CABLE
|
||
Output` capture endpoint surfaces as a host mic. A Windows audio device can only be created by a
|
||
**kernel-mode** driver (no UMDF path exists), so unlike our self-signed UMDF drivers we cannot ship our
|
||
own — VB-CABLE is a vendor-signed cable that loads with no test-signing. It is **donationware** by
|
||
VB-Audio, redistributed under VB-Audio's bundling grant (only the single base cable) — the grant
|
||
requires the end user to see VB-CABLE's origin + donationware status, which the wizard task text and
|
||
`licenses/VB-CABLE-NOTICE.txt` surface. The package binary is **not** in the repo — CI provisions the
|
||
**pinned, SHA-256-verified official package** onto the runner (`scripts/ci/provision-windows-punktfunk-extras.ps1`
|
||
→ `C:\Users\Public\vbcable`) and `windows-host.yml` passes it via `$env:VBCABLE_DIR`, so **published
|
||
installers always bundle it**; locally supply `-VbCableDir` / `$env:VBCABLE_DIR` (the extracted
|
||
official package, containing `VBCABLE_Setup_x64.exe`). Unset → the installer is built without it and
|
||
the host falls back to auto-installing the Steam Streaming pair; set-but-invalid → the pack **fails**
|
||
(a broken provisioning must not silently ship a mic-less installer again). *(Endgame:
|
||
attestation-sign our own MIT virtual-audio driver to drop this dependency.)*
|
||
|
||
## Files here
|
||
|
||
| File | Role |
|
||
|------|------|
|
||
| `punktfunk-host.iss` | Inno Setup script (the installer definition). |
|
||
| `branding/` | Wizard branding: `gen-branding.ps1` renders the brand mark into the committed `wizard-image-*.bmp` / `wizard-small-*.bmp` (100–200% DPI) + `punktfunk.ico`. Re-run only on a brand change. |
|
||
| `pack-host-installer.ps1` | Orchestrator: cert + sign exe, **build + sign the drivers from source**, stage them + FFmpeg + VB-CABLE + the **web console** (`.output` + bun) + the HDR layer + branding, run ISCC, sign setup.exe. |
|
||
| `build-pf-vdisplay.ps1` | Build pf-vdisplay from source (the `drivers/` workspace) + clear FORCE_INTEGRITY + sign `.dll`/`.cat` + export `.cer`. |
|
||
| `build-gamepad-drivers.ps1` | Sign + catalog the gamepad drivers (`pf-gamepad` + `pf-xusb`) from the same workspace build (`-SkipBuild`), one shared cert. |
|
||
| `install-vbcable.ps1` | On-target: seed VB-Audio's cert into `TrustedPublisher`, silently install the bundled VB-CABLE (`-i -h`). Run by the installer's *Install VB-CABLE virtual audio* task; idempotent + always exits 0 (non-fatal). |
|
||
| `clear-force-integrity.ps1` | Clear the `/INTEGRITYCHECK` PE bit so a self-signed driver loads (reused by every driver build). |
|
||
| `stage-pf-vdisplay.ps1` | Stage the just-built pf-vdisplay bundle + fetch/verify the **pinned** nefcon release. |
|
||
| `../../scripts/windows/web-run.cmd` | The `PunktfunkWeb` task action: loads the mgmt token + login password env, runs the bundled `bun` on the Nitro server (`:47992`). |
|
||
| `drivers/` | The all-Rust IddCx **driver source** workspace: the `pf-vdisplay` crate on `wdk-sys` / windows-drivers-rs + the owned `pf-driver-proto` ABI + `wdk-iddcx` / `wdk-probe`, plus `deploy-dev.ps1` (build/sign/install for dev). |
|
||
| `reset-pf-vdisplay.ps1` | **Dev:** recover a wedged driver — stop host → reap ghost monitor nodes → reload the adapter → start host (no reboot). See *Dev iteration* below. |
|
||
| `redeploy-pf-vdisplay.ps1` | **Dev:** one-shot redeploy — (optional) build → stop host → `deploy-dev.ps1 -Install` → reload adapter → start host. |
|
||
| `pf-vkhdr-layer/` | **HDR Vulkan layer** (standalone `cdylib`): lets Vulkan games (Doom: The Dark Ages, etc.) enable HDR over the virtual display by advertising the HDR surface formats the NVIDIA/AMD ICDs hide on an indirect display. Built by the packer, laid into `{app}\vklayer`, registered under `HKLM64\…\Khronos\Vulkan\ImplicitLayers` (opt-out *Install the HDR Vulkan layer* task). Self-gated on the display's HDR state. See its README. |
|
||
|
||
> **Drivers are built from source, not vendored.** All three (pf-vdisplay + the gamepad pf-gamepad /
|
||
> pf-xusb) are members of the all-Rust `drivers/` workspace (windows-drivers-rs / IddCx) and are
|
||
> **rebuilt + signed every release** by `build-pf-vdisplay.ps1` + `build-gamepad-drivers.ps1` - the
|
||
> checked-in prebuilt binaries were deleted (a stale `.cat` once stopped covering its `.inf` →
|
||
> `SPAPI_E_FILE_HASH_NOT_IN_CATALOG` on every box, and a frozen binary predated a driver IOCTL the host
|
||
> needed). Building from source keeps `.dll`/`.inf`/`.cat` in lockstep. nefcon (the device-node tool -
|
||
> the install creates the `root\pf_vdisplay` node with it, **never** `devgen`, which leaves persistent
|
||
> phantom devices) is fetched + SHA-256-verified from its pinned release in `stage-pf-vdisplay.ps1`. See
|
||
> punktfunk-planning: `windows-build-and-packaging.md` (internal planning repo) for the toolchain
|
||
> + signing details.
|
||
|
||
## Dev iteration on the test box (driver)
|
||
|
||
Two helpers wrap the painful manual steps of iterating on the pf-vdisplay driver against a live host
|
||
service. Run **elevated**; both default to the `PunktfunkHost` service. (The `C:\t-goal1\...` probe
|
||
path below is the maintainer's test box — substitute your own `punktfunk-probe.exe` build.)
|
||
|
||
```powershell
|
||
# Recover a WEDGED driver. Symptom: every session fails with
|
||
# create virtual output: pf-vdisplay ADD ...: DeviceIoControl(0x222400): Element nicht gefunden (0x80070490)
|
||
# i.e. ERROR_NOT_FOUND — sustained ADD/REMOVE churn exhausted the IddCx monitor slots (ghost
|
||
# "Generic Monitor (punktfunk)" nodes pile up, target_ids climb). A host restart's CLEAR_ALL does NOT
|
||
# fix it; the driver instance must be reloaded. This clears the ghosts + cycles the adapter (no reboot —
|
||
# this box boots to Proxmox).
|
||
powershell -ExecutionPolicy Bypass -File reset-pf-vdisplay.ps1 -Verify -Probe C:\t-goal1\debug\punktfunk-probe.exe
|
||
|
||
# Redeploy a driver build cleanly (stop host → install with a strictly-increasing DriverVer → reload
|
||
# adapter → start host). -Build runs `cargo build` first, but ONLY from an MSVC dev shell
|
||
# (LIBCLANG_PATH + Version_Number=10.0.26100.0); otherwise build separately and omit -Build.
|
||
powershell -ExecutionPolicy Bypass -File redeploy-pf-vdisplay.ps1 -Build -Verify -Probe C:\t-goal1\debug\punktfunk-probe.exe
|
||
```
|
||
|
||
The driver should reclaim monitor slots on REMOVE so churn can't wedge it; until it does, `reset` is
|
||
the recovery. From a Linux box drive either over SSH, e.g.
|
||
`ssh user@box 'powershell -ExecutionPolicy Bypass -File C:\...\reset-pf-vdisplay.ps1'`.
|
||
|
||
## Build locally (Windows, MSVC + Windows SDK + Inno Setup)
|
||
|
||
```powershell
|
||
# 1. build the host (NVENC needs no import lib — its entry points are runtime-loaded; `qsv`
|
||
# statically links the vendored VPL dispatcher — needs cmake + a libclang, no FFmpeg)
|
||
cargo build --release -p punktfunk-host --features nvenc,qsv
|
||
|
||
# 2. pack (self-signed unless MSIX_CERT_PFX_B64/MSIX_CERT_PASSWORD are set; -NoDriver to skip pf-vdisplay)
|
||
pwsh -File packaging\windows\pack-host-installer.ps1 -Version 0.0.0-dev -TargetDir C:\t\release -OutDir C:\t\out
|
||
```
|
||
|
||
## Release
|
||
|
||
Push a `vX.Y.Z` tag — one tag releases every platform (see
|
||
[Release Channels](https://punktfunk.unom.io/docs/channels)). The workflow builds, signs, and
|
||
publishes `punktfunk-host-setup-X.Y.Z.exe` + the public `.cer`, refreshes the stable `latest/`
|
||
alias, and attaches the installer to the unified Gitea Release. Main pushes publish rolling
|
||
`<next-minor>.<run>` **canary** builds (base derived from the latest stable tag by
|
||
`scripts/ci/pf-version.ps1`) to the `canary/` alias.
|