mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-13 12:23:38 +01:00
The scripts were written for a KDE-only repository root (komorebi-for-kde/, scripts/ one level below it). They now live in scripts/linux/ of the consolidated repository: - run-kde looks for the repository two directories above itself (flake.nix at the root) and no longer knows the komorebi-for-kde/ subdirectory; its comments name nix/linux and docs/linux - vm-mac cd`s two levels up, usage and comments name scripts/linux/vm-mac - sync-e2e-core and scenario-status read nix/linux/tests/vm/lib, and usage lines name the new paths (scenario-status resolves the library with parents[2]) - wsl2-probe.sh names docs/linux/dev/wsl2-probe.md and prints "komorebi (KDE Plasma)" instead of komorebi-for-kde in its report header - compare-gen.py is stale by design: the docstring now says so a second time (layout) and names the new path of COMPARE.md; the logic is not repaired Flake attribute names the scripts call (nix run .#run-kde, .#wsl2-probe, legacyPackages.*.vm.NAME in vm-mac) are left for the one-flake revision. Verified: bash -n and shellcheck -S warning pass on the four shell scripts; the two Python scripts parse (ast); `bash run-kde --dry-run` run from /tmp prints KOMOREBI_REPO as this checkout; `sync-e2e-core --check e2e/lib` prints "in step"; `scenario-status --help` runs. Not run: the VM, vm-mac, wsl2-probe.
1558 lines
61 KiB
Bash
1558 lines
61 KiB
Bash
#!/usr/bin/env bash
|
|
# wsl2-probe: finds out what a NixOS-WSL2 virtual machine can do for a Plasma guest.
|
|
# Start it as `nix run .#wsl2-probe` (or `just wsl2-probe`) and paste the end of its output
|
|
# back. `--help` lists the options, docs/linux/dev/wsl2-probe.md says what each check means.
|
|
set -u
|
|
export LC_ALL=C
|
|
# wsl.exe writes UTF-16 unless it is told otherwise; WSLENV carries the variable to it
|
|
export WSL_UTF8=1
|
|
export WSLENV="${WSLENV:+$WSLENV:}WSL_UTF8"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
wsl2-probe: finds out what a NixOS-WSL2 virtual machine can do for a Plasma guest.
|
|
|
|
nix run .#wsl2-probe [-- OPTIONS] (or: just wsl2-probe)
|
|
|
|
It changes nothing on the machine. It reads files, boots a tiny guest (a kernel and an initrd
|
|
that print, time a loop and power off) under QEMU, and listens on local ports for a few seconds.
|
|
It prints PASS, WARN and FAIL lines, with the exact fix for the last two, writes the same
|
|
report to ./wsl2-probe-report.txt, and ends with a block to paste back.
|
|
|
|
P0 the environment: WSL2, kernel, memory, disk, .wslconfig, the CPU, Nix
|
|
P1 KVM: /dev/kvm, who can open it, a guest booted under KVM and under TCG, the speeds
|
|
P2 display: WSLg, and a QEMU window that stays open for a few seconds
|
|
P3 GPU: /dev/dxg, the Windows driver, the host's OpenGL renderer, a VirGL window
|
|
P4 the Windows side: interop, mstsc.exe, whether Windows reaches a port listening in WSL
|
|
P5 resources: whether memory, cores and disk are enough for the real guest
|
|
P6 virtiofs: /dev/fuse, virtiofsd, and a guest that mounts what virtiofsd serves
|
|
|
|
Options:
|
|
--only P1,P3 run only these probes (P0 always runs)
|
|
--hold SECONDS how long a window stays open (default 8)
|
|
--yes answer yes to "did a window appear?" without asking
|
|
--no-prompt do not ask anything (also the case when stdin is not a terminal)
|
|
--dry-run run P0 and P5 and print what the other probes would start, without starting it
|
|
--report FILE where to write the report (default ./wsl2-probe-report.txt)
|
|
-h, --help this text
|
|
|
|
The exit status is 0 when the probes ran, whatever they found; 2 for a mistake in the options.
|
|
EOF
|
|
}
|
|
|
|
DRY=0
|
|
ASSUME_YES=0
|
|
NO_PROMPT=0
|
|
HOLD=8
|
|
LOOPS=600000
|
|
ONLY=""
|
|
REPORT=wsl2-probe-report.txt
|
|
GUEST=${WSL2_PROBE_GUEST:-}
|
|
REV=${WSL2_PROBE_REV:-unknown}
|
|
|
|
need_value() {
|
|
if [ "$1" -lt 2 ]; then
|
|
echo "$2 needs a value (see --help)" >&2
|
|
exit 2
|
|
fi
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case $1 in
|
|
--only)
|
|
need_value $# --only
|
|
ONLY=${2^^}
|
|
shift
|
|
;;
|
|
--hold)
|
|
need_value $# --hold
|
|
HOLD=$2
|
|
shift
|
|
;;
|
|
--report)
|
|
need_value $# --report
|
|
REPORT=$2
|
|
shift
|
|
;;
|
|
--yes) ASSUME_YES=1 ;;
|
|
--no-prompt) NO_PROMPT=1 ;;
|
|
--dry-run) DRY=1 ;;
|
|
-h | --help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "unknown option: $1 (see --help)" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
case $HOLD in '' | *[!0-9]*)
|
|
echo "--hold needs a whole number of seconds" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
STATE=$(mktemp -d "${TMPDIR:-/tmp}/wsl2-probe.XXXXXX") || {
|
|
echo "cannot make a temporary directory" >&2
|
|
exit 2
|
|
}
|
|
mkdir -p "$STATE/kv"
|
|
: >"$STATE/verdicts"
|
|
: >"$STATE/facts"
|
|
: >"$STATE/pids"
|
|
|
|
# only what this script started is stopped: the listeners of P4 (the trap below calls it)
|
|
# shellcheck disable=SC2329
|
|
cleanup() {
|
|
local pid
|
|
while read -r pid; do
|
|
[ -n "$pid" ] && kill "$pid" 2>/dev/null
|
|
done <"$STATE/pids"
|
|
rm -rf "$STATE"
|
|
}
|
|
trap 'cleanup' EXIT
|
|
trap 'echo; echo "interrupted: the report so far is in $REPORT"; exit 130' INT TERM
|
|
|
|
if ! : >"$REPORT" 2>/dev/null; then
|
|
REPORT=${TMPDIR:-/tmp}/wsl2-probe-report.txt
|
|
: >"$REPORT"
|
|
fi
|
|
case $REPORT in
|
|
/*) REPORT_PATH=$REPORT ;;
|
|
*) REPORT_PATH=$PWD/$REPORT ;;
|
|
esac
|
|
|
|
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
|
C_PASS=$'\033[32m'
|
|
C_WARN=$'\033[33m'
|
|
C_FAIL=$'\033[31m'
|
|
C_INFO=$'\033[36m'
|
|
C_BOLD=$'\033[1m'
|
|
C_OFF=$'\033[0m'
|
|
else
|
|
C_PASS="" C_WARN="" C_FAIL="" C_INFO="" C_BOLD="" C_OFF=""
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# output: every line goes to the screen and to the report
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
out() {
|
|
printf '%s\n' "$*"
|
|
printf '%s\n' "$*" >>"$REPORT"
|
|
}
|
|
|
|
# a status line: [PASS] text
|
|
line() {
|
|
local status=$1 color
|
|
shift
|
|
case $status in
|
|
PASS) color=$C_PASS ;;
|
|
WARN) color=$C_WARN ;;
|
|
FAIL) color=$C_FAIL ;;
|
|
*) color=$C_INFO ;;
|
|
esac
|
|
printf '[%s%s%s] %s\n' "$color" "$status" "$C_OFF" "$*"
|
|
printf '[%s] %s\n' "$status" "$*" >>"$REPORT"
|
|
}
|
|
|
|
# verdict ID STATUS TEXT: a check with a result, which the summary lists
|
|
verdict() {
|
|
local id=$1 status=$2
|
|
shift 2
|
|
line "$status" "$id $*"
|
|
printf '%s|%s|%s\n' "$id" "$status" "$*" >>"$STATE/verdicts"
|
|
}
|
|
|
|
# fix LINE...: how to repair the check above
|
|
fix() {
|
|
local first=1 text
|
|
for text in "$@"; do
|
|
if [ "$first" = 1 ]; then
|
|
out " fix: $text"
|
|
first=0
|
|
else
|
|
out " $text"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# detail LINE...: more about the check above
|
|
detail() {
|
|
local text
|
|
for text in "$@"; do
|
|
out " $text"
|
|
done
|
|
}
|
|
|
|
# step TEXT: says what is about to take a while
|
|
step() { out " ... $*"; }
|
|
|
|
# rc_text STATUS: how a run ended, for a message
|
|
rc_text() {
|
|
if [ "$1" = 124 ]; then echo "the time limit stopped it"; else echo "exit status $1"; fi
|
|
}
|
|
|
|
# block PREFIX: prints stdin, with the prefix, to the screen and the report
|
|
block() {
|
|
local text
|
|
while IFS= read -r text; do
|
|
out "$1$text"
|
|
done
|
|
}
|
|
|
|
# fact PROBE KEY VALUE: a value read or measured, which the summary repeats
|
|
fact() {
|
|
local probe=$1 key=$2 value
|
|
shift 2
|
|
value=$(printf '%s' "$*" | tr '\n' ' ')
|
|
line INFO "$probe $key: $value"
|
|
printf '%s: %s\n' "$key" "$value" >>"$STATE/facts"
|
|
}
|
|
|
|
# fact_block PROBE KEY: a fact of several lines, read from stdin
|
|
fact_block() {
|
|
local text
|
|
text=$(cat)
|
|
line INFO "$1 $2:"
|
|
printf '%s\n' "$text" | block " | "
|
|
{
|
|
printf '%s:\n' "$2"
|
|
printf '%s\n' "$text" | sed 's/^/ /'
|
|
} >>"$STATE/facts"
|
|
}
|
|
|
|
heading() {
|
|
out ""
|
|
printf '%s== %s%s\n' "$C_BOLD" "$*" "$C_OFF"
|
|
printf '== %s\n' "$*" >>"$REPORT"
|
|
}
|
|
|
|
kv_set() { printf '%s\n' "$2" >"$STATE/kv/$1"; }
|
|
|
|
kv_get() {
|
|
if [ -f "$STATE/kv/$1" ]; then cat "$STATE/kv/$1"; else printf '%s\n' "${2:-}"; fi
|
|
}
|
|
|
|
want() {
|
|
[ -z "$ONLY" ] && return 0
|
|
case ",$ONLY," in *",$1,"*) return 0 ;; esac
|
|
return 1
|
|
}
|
|
|
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
now_ms() {
|
|
local t=${EPOCHREALTIME/[.,]/}
|
|
echo $((t / 1000))
|
|
}
|
|
|
|
gib() { awk -v k="${1:-0}" 'BEGIN { printf "%.1f", k / 1048576 }'; }
|
|
|
|
# list_dir DIR: the names in a directory, one per line; nothing when there is none
|
|
list_dir() { find "$1" -mindepth 1 -maxdepth 1 -printf '%f\n' 2>/dev/null | sort; }
|
|
|
|
# file_owner FILE: the mode, owner and group of a file
|
|
file_owner() { stat -c '%A %U %G' "$1" 2>/dev/null; }
|
|
|
|
# head_lines COUNT FILE: the first non-empty lines of a file, for an error message
|
|
head_lines() { tr -d '\r' 2>/dev/null <"$2" | grep -v '^$' | head -n "$1"; }
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# the machine
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
ME=$(id -un 2>/dev/null || echo "${USER:-user}")
|
|
DISTRO=${WSL_DISTRO_NAME:-NixOS}
|
|
STATE_DIR=${XDG_STATE_HOME:-$HOME/.local/state}/komorebi-run-kde
|
|
|
|
is_wsl() {
|
|
[ -n "${WSL_DISTRO_NAME:-}" ] || [ -d /run/WSL ] || grep -qi microsoft /proc/version 2>/dev/null
|
|
}
|
|
|
|
# a Windows program: on PATH (NixOS-WSL puts the Windows PATH there) or in System32
|
|
find_win() {
|
|
local name=$1 dir found
|
|
if found=$(command -v "$name" 2>/dev/null); then
|
|
echo "$found"
|
|
return 0
|
|
fi
|
|
for dir in /mnt/c/Windows/System32 /mnt/c/Windows /mnt/c/Windows/System32/WindowsPowerShell/v1.0; do
|
|
if [ -x "$dir/$name" ]; then
|
|
echo "$dir/$name"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# win PROGRAM ARGS...: runs a Windows program from a Windows directory (cmd.exe complains about
|
|
# a Linux one) and prints its output without carriage returns, after WIN_TIMEOUT seconds at most
|
|
win() {
|
|
local program=$1 path
|
|
shift
|
|
[ "$DRY" = 1 ] && return 127
|
|
path=$(find_win "$program") || return 127
|
|
(
|
|
cd /mnt/c 2>/dev/null || cd /
|
|
timeout "${WIN_TIMEOUT:-20}" "$path" "$@" 2>&1 </dev/null | tr -d '\r\0'
|
|
)
|
|
}
|
|
|
|
# the WSL path of a Windows path
|
|
win_to_wsl() {
|
|
local path=$1 drive rest
|
|
if have wslpath && wslpath -u "$path" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
rest=${path//\\//}
|
|
drive=${rest%%:*}
|
|
echo "/mnt/${drive,,}${rest#*:}"
|
|
}
|
|
|
|
# the .wslconfig of the Windows user, as a WSL path; nothing when interop is off
|
|
wslconfig_path() {
|
|
local cached profile
|
|
cached=$(kv_get wslconfig_path)
|
|
if [ -n "$cached" ]; then
|
|
[ "$cached" = none ] || echo "$cached"
|
|
return 0
|
|
fi
|
|
profile=$(win cmd.exe /c 'echo %USERPROFILE%' | grep -m1 -E '^[A-Za-z]:[\\]')
|
|
if [ -z "$profile" ]; then
|
|
kv_set wslconfig_path none
|
|
return 0
|
|
fi
|
|
kv_set wslconfig_path "$(win_to_wsl "$profile")/.wslconfig"
|
|
kv_set wslconfig_name "$profile\\.wslconfig"
|
|
kv_get wslconfig_path
|
|
}
|
|
|
|
# the .wslconfig as Windows names it, for a fix
|
|
wslconfig_name() {
|
|
local name
|
|
wslconfig_path >/dev/null
|
|
name=$(kv_get wslconfig_name)
|
|
echo "${name:-%USERPROFILE%\\.wslconfig}"
|
|
}
|
|
|
|
# wslconfig_get KEY: the value a key has in .wslconfig, nothing when it is not set
|
|
wslconfig_get() {
|
|
local file
|
|
file=$(wslconfig_path)
|
|
[ -n "$file" ] && [ -r "$file" ] || return 1
|
|
tr -d '\r' <"$file" | awk -F= -v key="$1" '
|
|
{ name = $1; gsub(/^[ \t]+|[ \t]+$/, "", name) }
|
|
tolower(name) == tolower(key) {
|
|
value = substr($0, index($0, "=") + 1)
|
|
sub(/[ \t]*[#;].*$/, "", value)
|
|
gsub(/^[ \t]+|[ \t]+$/, "", value)
|
|
print value
|
|
exit
|
|
}'
|
|
}
|
|
|
|
# the free space of the file system a path is on (or its nearest parent that exists)
|
|
df_avail_kib() {
|
|
local path=$1
|
|
while [ ! -e "$path" ] && [ "$path" != / ]; do path=$(dirname "$path"); done
|
|
df -Pk "$path" 2>/dev/null | awk 'NR == 2 { print $4 }'
|
|
}
|
|
|
|
df_fstype() {
|
|
local path=$1
|
|
while [ ! -e "$path" ] && [ "$path" != / ]; do path=$(dirname "$path"); done
|
|
df -PkT "$path" 2>/dev/null | awk 'NR == 2 { print $2 }'
|
|
}
|
|
|
|
mem_kib() { awk -v key="$1:" '$1 == key { print $2 }' /proc/meminfo; }
|
|
|
|
# the free space the Nix store and the guest share: under WSL the virtual disk looks huge, and
|
|
# what limits it is the Windows drive it lives on
|
|
disk_free_kib() {
|
|
local nix_free c_free
|
|
nix_free=$(df_avail_kib /nix)
|
|
if is_wsl && [ -d /mnt/c ]; then
|
|
c_free=$(df_avail_kib /mnt/c)
|
|
if [ -n "$c_free" ] && [ -n "$nix_free" ] && [ "$c_free" -lt "$nix_free" ]; then
|
|
echo "$c_free"
|
|
return
|
|
fi
|
|
fi
|
|
echo "$nix_free"
|
|
}
|
|
|
|
cpu_flag() { grep -m1 -o -w -E 'svm|vmx' /proc/cpuinfo 2>/dev/null | head -n1; }
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# the guest: a kernel and an initrd that print, time a loop and power off
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
case $(uname -m) in
|
|
x86_64)
|
|
QEMU=qemu-system-x86_64
|
|
MACHINE=q35
|
|
SERIAL=ttyS0
|
|
VGA=virtio-vga
|
|
VGA_GL=virtio-vga-gl
|
|
;;
|
|
aarch64)
|
|
QEMU=qemu-system-aarch64
|
|
MACHINE=virt,gic-version=max
|
|
SERIAL=ttyAMA0
|
|
VGA=virtio-gpu-pci
|
|
VGA_GL=virtio-gpu-gl-pci
|
|
;;
|
|
*)
|
|
QEMU=qemu-system-$(uname -m)
|
|
MACHINE=virt
|
|
SERIAL=ttyS0
|
|
VGA=virtio-vga
|
|
VGA_GL=virtio-vga-gl
|
|
;;
|
|
esac
|
|
|
|
guest_ready() {
|
|
[ -n "$GUEST" ] && [ -r "$GUEST/kernel" ] && [ -r "$GUEST/initrd" ] && have "$QEMU"
|
|
}
|
|
|
|
guest_missing() {
|
|
verdict "$1" SKIP "no guest or no QEMU: they come with nix run .#wsl2-probe"
|
|
if [ -z "$GUEST" ]; then
|
|
fix "start the script as nix run .#wsl2-probe from the checkout (it brings the guest and QEMU)"
|
|
else
|
|
fix "$QEMU is not on PATH: start the script as nix run .#wsl2-probe"
|
|
fi
|
|
}
|
|
|
|
# qemu_args MODE ACCEL... -- EXTRA...: fills QARGS with the guest's command line. MODE is the
|
|
# guest's probe.mode; ACCEL are the accelerators to try in order (kvm, tcg).
|
|
qemu_args() {
|
|
local mode=$1 cpu=host append
|
|
shift
|
|
QARGS=("$QEMU" -machine "$MACHINE")
|
|
while [ $# -gt 0 ] && [ "$1" != -- ]; do
|
|
QARGS+=(-accel "$1")
|
|
# a run that may fall back to software cannot ask for the host's CPU
|
|
[ "$1" = tcg ] && cpu=max
|
|
shift
|
|
done
|
|
shift
|
|
append="panic=-1 quiet probe.mode=$mode probe.hold=$HOLD probe.loops=$LOOPS"
|
|
case $mode in
|
|
bench) append="console=$SERIAL $append" ;;
|
|
virtiofs) append="console=$SERIAL $append probe.token=${P6_TOKEN:-none} probe.storefile=${P6_STOREFILE:-none}" ;;
|
|
*) append="console=$SERIAL console=tty0 probe.serial=$SERIAL $append" ;;
|
|
esac
|
|
QARGS+=(-cpu "$cpu" -smp 1 -m 512 -no-reboot -nodefaults
|
|
-kernel "$GUEST/kernel" -initrd "$GUEST/initrd" -append "$append"
|
|
-serial "file:$STATE/serial" "$@")
|
|
}
|
|
|
|
# run_guest LIMIT ENV... -- COMMAND...: runs the command for at most LIMIT seconds and sets
|
|
# RG_RC (its exit status, 124 when the limit stopped it), RG_MS (the wall time), RG_SERIAL
|
|
# (what the guest printed, without carriage returns) and RG_ERR (what QEMU printed)
|
|
run_guest() {
|
|
local limit=$1 start
|
|
local -a venv=()
|
|
shift
|
|
while [ $# -gt 0 ] && [ "$1" != -- ]; do
|
|
venv+=("$1")
|
|
shift
|
|
done
|
|
shift
|
|
: >"$STATE/serial"
|
|
RG_ERR=$STATE/qemu.err
|
|
RG_SERIAL=$STATE/serial.clean
|
|
start=$(now_ms)
|
|
env "${venv[@]}" timeout "$limit" "$@" >"$STATE/qemu.out" 2>"$RG_ERR" </dev/null
|
|
RG_RC=$?
|
|
RG_MS=$(($(now_ms) - start))
|
|
tr -d '\r' <"$STATE/serial" >"$RG_SERIAL"
|
|
cat "$STATE/qemu.out" >>"$RG_ERR"
|
|
}
|
|
|
|
# guest_value FILE KEY: a PROBE:KEY=value line the guest printed
|
|
guest_value() { sed -n "s/^PROBE:$2=//p" "$1" | head -n1; }
|
|
|
|
# ask_yes_no QUESTION: prints yes, no or "not asked"
|
|
ask_yes_no() {
|
|
local answer=""
|
|
if [ "$ASSUME_YES" = 1 ]; then
|
|
echo "yes (answered by --yes)"
|
|
return
|
|
fi
|
|
if [ "$NO_PROMPT" = 1 ] || [ ! -t 0 ]; then
|
|
echo "not asked"
|
|
return
|
|
fi
|
|
printf '%s (y/n) ' "$1" >&2
|
|
if read -r -t 120 answer; then
|
|
case $answer in
|
|
[yY]*) echo yes ;;
|
|
[nN]*) echo no ;;
|
|
*) echo "not understood: $answer" ;;
|
|
esac
|
|
else
|
|
echo "no answer"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# P0: the environment
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
p0_environment() {
|
|
heading "P0 environment"
|
|
local text path vendor model ver wslv config
|
|
|
|
if is_wsl; then
|
|
if grep -qi 'wsl2' /proc/version 2>/dev/null; then
|
|
verdict P0.1 PASS "this is WSL2 (distribution $DISTRO)"
|
|
else
|
|
verdict P0.1 WARN "this is WSL, but /proc/version does not say WSL2: $(head -n1 /proc/version)"
|
|
fix "in PowerShell: wsl --set-version $DISTRO 2"
|
|
fi
|
|
else
|
|
verdict P0.1 WARN "this is not WSL: the Windows side (P4) and WSLg (P2, P3) can only report what they find here"
|
|
fi
|
|
fact P0 wsl "WSL_DISTRO_NAME=${WSL_DISTRO_NAME:-unset}; /run/WSL $([ -d /run/WSL ] && echo present || echo absent); WSL_INTEROP=${WSL_INTEROP:-unset}"
|
|
fact P0 kernel "$(uname -srm)"
|
|
fact P0 proc_version "$(head -n1 /proc/version)"
|
|
fact P0 pid1 "$(ps -p 1 -o comm= 2>/dev/null || echo unknown)"
|
|
if [ -r /etc/os-release ]; then
|
|
text=$(
|
|
# shellcheck source=/dev/null
|
|
. /etc/os-release
|
|
echo "${PRETTY_NAME:-?} (VERSION_ID ${VERSION_ID:-?}, BUILD_ID ${BUILD_ID:-?})"
|
|
)
|
|
fact P0 os "$text"
|
|
fi
|
|
have nixos-version && fact P0 nixos_version "$(nixos-version 2>/dev/null)"
|
|
if [ -r /etc/nixos/flake.lock ] && have jq; then
|
|
text=$(jq -r '.nodes["nixos-wsl"].locked // empty | "\(.owner)/\(.repo) \(.rev) lastModified \(.lastModified)"' /etc/nixos/flake.lock 2>/dev/null)
|
|
[ -n "$text" ] && fact P0 nixos_wsl_input "$text"
|
|
fi
|
|
fact P0 wsl_commands "$(list_dir /run/current-system/sw/bin | grep -i wsl | tr '\n' ' ')"
|
|
|
|
fact P0 memory "MemTotal $(gib "$(mem_kib MemTotal)") GiB, MemAvailable $(gib "$(mem_kib MemAvailable)") GiB, SwapTotal $(gib "$(mem_kib SwapTotal)") GiB"
|
|
fact P0 cpus "$(nproc) (nproc)"
|
|
for path in /nix "$HOME" "$STATE_DIR" "$PWD" /mnt/c; do
|
|
[ -e "$path" ] || [ "$path" = "$STATE_DIR" ] || continue
|
|
fact P0 "free_disk $path" "$(gib "$(df_avail_kib "$path")") GiB ($(df_fstype "$path"))"
|
|
done
|
|
|
|
vendor=$(awk -F: '/^vendor_id/ { gsub(/^ /, "", $2); print $2; exit }' /proc/cpuinfo)
|
|
model=$(awk -F: '/^model name/ { gsub(/^ /, "", $2); print $2; exit }' /proc/cpuinfo)
|
|
fact P0 cpu "${vendor:-unknown}, ${model:-unknown}; virtualization flag: $(cpu_flag)"
|
|
|
|
ver=$(win cmd.exe /c ver | grep -m1 Windows)
|
|
if [ -n "$ver" ]; then
|
|
fact P0 windows "$ver"
|
|
wslv=$(win wsl.exe --version | tr '\n' ';')
|
|
[ -n "$wslv" ] && fact P0 wsl_exe_version "$wslv"
|
|
config=$(wslconfig_path)
|
|
if [ -r "$config" ]; then
|
|
fact P0 wslconfig_path "$config"
|
|
tr -d '\r' <"$config" | grep -v '^[#;]' | grep -v '^$' | head -n 40 | fact_block P0 wslconfig
|
|
else
|
|
fact P0 wslconfig "none at ${config:-?}: the defaults apply (half of the Windows memory, every processor)"
|
|
fi
|
|
elif [ "$DRY" = 1 ]; then
|
|
fact P0 windows "dry run: the Windows side is not asked"
|
|
elif is_wsl; then
|
|
fact P0 windows "interop is off, or cmd.exe cannot be found: the Windows version and .wslconfig are unknown (P4.1 has the fix)"
|
|
else
|
|
fact P0 windows "not WSL: there is no Windows side"
|
|
fi
|
|
|
|
p0_nix
|
|
}
|
|
|
|
p0_nix() {
|
|
local features trusted entry is_trusted=no
|
|
if ! have nix; then
|
|
verdict P0.2 FAIL "nix is not on PATH"
|
|
fix "use a NixOS-WSL distribution, or install Nix; this script is meant for nix run .#wsl2-probe"
|
|
return
|
|
fi
|
|
fact P0 nix "$(nix --version 2>/dev/null)"
|
|
features=$(nix config show experimental-features 2>/dev/null || nix show-config 2>/dev/null | sed -n 's/^experimental-features = //p')
|
|
case " $features " in
|
|
*" flakes "*) verdict P0.2 PASS "flakes are enabled (experimental-features = $features)" ;;
|
|
*)
|
|
verdict P0.2 WARN "flakes do not show as enabled (experimental-features = ${features:-empty})"
|
|
fix "NixOS: nix.settings.experimental-features = [ \"nix-command\" \"flakes\" ]; then sudo nixos-rebuild switch" \
|
|
"for one command: nix --extra-experimental-features 'nix-command flakes' run .#wsl2-probe"
|
|
;;
|
|
esac
|
|
trusted=$(nix config show trusted-users 2>/dev/null || nix show-config 2>/dev/null | sed -n 's/^trusted-users = //p')
|
|
fact P0 nix_trusted_users "${trusted:-unknown}; this user $ME, groups $(id -nG | tr ' ' ',')"
|
|
for entry in $trusted; do
|
|
case $entry in
|
|
"$ME") is_trusted=yes ;;
|
|
@*) id -nG | tr ' ' '\n' | grep -qx "${entry#@}" && is_trusted=yes ;;
|
|
esac
|
|
done
|
|
if [ "$is_trusted" = yes ]; then
|
|
verdict P0.3 PASS "$ME is a trusted nix user"
|
|
else
|
|
verdict P0.3 WARN "$ME is not a trusted nix user: nix run works, but a build cannot set options such as substituters"
|
|
fix "NixOS-WSL: nix.settings.trusted-users = [ \"root\" \"$ME\" ]; then sudo nixos-rebuild switch"
|
|
fi
|
|
fact P0 nix_substituters "$(nix config show substituters 2>/dev/null || nix show-config 2>/dev/null | sed -n 's/^substituters = //p')"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# P1: KVM
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
p1_kvm() {
|
|
heading "P1 KVM"
|
|
local flag param nested=""
|
|
flag=$(cpu_flag)
|
|
|
|
if [ -n "$flag" ]; then
|
|
verdict P1.1 PASS "the CPU exposes $flag to this machine"
|
|
elif [ "$(uname -m)" != x86_64 ]; then
|
|
verdict P1.1 INFO "this is $(uname -m), which has no svm or vmx flag: /dev/kvm (P1.2) says whether KVM works"
|
|
else
|
|
verdict P1.1 FAIL "the CPU flags have neither svm (AMD) nor vmx (Intel): no virtualization reaches this machine"
|
|
if is_wsl; then
|
|
fix "in $(wslconfig_name):" \
|
|
" [wsl2]" \
|
|
" nestedVirtualization=true" \
|
|
"then in PowerShell: wsl --shutdown, and reopen the distribution" \
|
|
"if it stays off: turn on SVM Mode (AMD) or Intel VT-x in the firmware setup, turn on the Windows" \
|
|
"feature Virtual Machine Platform, and run wsl --update (nested virtualization needs Windows 11)"
|
|
else
|
|
fix "turn on SVM Mode (AMD) or Intel VT-x in the firmware setup"
|
|
fi
|
|
fi
|
|
|
|
if [ ! -e /dev/kvm ]; then
|
|
verdict P1.2 FAIL "/dev/kvm does not exist"
|
|
if is_wsl; then
|
|
fix "WSL2 creates it only when nested virtualization works: see P1.1; .wslconfig [wsl2] must not have" \
|
|
"nestedVirtualization=false; then wsl --shutdown, and reopen"
|
|
elif [ "$(uname -m)" = x86_64 ]; then
|
|
fix "sudo modprobe kvm_amd (or kvm_intel), and turn on virtualization in the firmware setup"
|
|
else
|
|
fix "this machine offers no KVM (a virtual machine without nested virtualization, or a kernel without it)"
|
|
fi
|
|
kv_set kvm_usable no
|
|
elif (exec 9<>/dev/kvm) 2>/dev/null; then
|
|
verdict P1.2 PASS "/dev/kvm can be opened read-write by $ME ($(file_owner /dev/kvm))"
|
|
kv_set kvm_usable yes
|
|
else
|
|
verdict P1.2 FAIL "/dev/kvm exists, but $ME cannot open it read-write ($(file_owner /dev/kvm); groups: $(id -nG))"
|
|
fix "put $ME in the group kvm, in the NixOS-WSL configuration:" \
|
|
" users.users.$ME.extraGroups = [ \"kvm\" ];" \
|
|
"then: sudo nixos-rebuild switch" \
|
|
"then in PowerShell: wsl --terminate $DISTRO (groups are read at login), and reopen"
|
|
kv_set kvm_usable no
|
|
fi
|
|
for param in /sys/module/kvm_amd/parameters/nested /sys/module/kvm_intel/parameters/nested; do
|
|
[ -r "$param" ] && nested="$param = $(cat "$param")"
|
|
done
|
|
fact P1 nested_virtualization "${nested:-no kvm_amd or kvm_intel module parameter to read}"
|
|
|
|
if [ "$DRY" = 1 ]; then
|
|
dry_guest_plan
|
|
return
|
|
fi
|
|
if ! guest_ready; then
|
|
guest_missing P1.3
|
|
return
|
|
fi
|
|
fact P1 qemu "$("$QEMU" --version | head -n1)"
|
|
p1_boots
|
|
}
|
|
|
|
p1_boots() {
|
|
local host_ms kvm_ms="" tcg_ms="" flags item seen=""
|
|
# shellcheck disable=SC2016
|
|
host_ms=$("$GUEST/busybox" sh -c '. "$1"; probe_loop "$2"' sh "$GUEST/loop.sh" "$LOOPS" 2>/dev/null)
|
|
fact P1 loop_on_this_machine_ms "${host_ms:-unknown} for $LOOPS iterations of the guest's ash loop (the guest's busybox)"
|
|
|
|
if [ "$(kv_get kvm_usable)" = yes ]; then
|
|
step "booting the guest under KVM"
|
|
qemu_args bench kvm -- -display none
|
|
run_guest 90 -- "${QARGS[@]}"
|
|
kvm_ms=$(guest_value "$RG_SERIAL" LOOP_MS)
|
|
if [ "$RG_RC" = 0 ] && [ -n "$kvm_ms" ]; then
|
|
verdict P1.3 PASS "a guest booted under KVM and ran the loop in $kvm_ms ms (this machine: ${host_ms:-?} ms); boot to power-off $RG_MS ms"
|
|
flags=$(guest_value "$RG_SERIAL" CPU_FLAGS)
|
|
fact P1 guest_cpu "$(guest_value "$RG_SERIAL" CPU_MODEL), $(guest_value "$RG_SERIAL" CPU_COUNT) cpu, $(guest_value "$RG_SERIAL" MEM_KB) kB"
|
|
for item in svm vmx hypervisor avx avx2 sse4_2 aes; do
|
|
case " $flags " in *" $item "*) seen="$seen$item " ;; esac
|
|
done
|
|
fact P1 guest_flags_of_interest "${seen:-none}"
|
|
else
|
|
verdict P1.3 FAIL "the guest did not run under KVM ($(rc_text "$RG_RC"))"
|
|
head_lines 6 "$RG_ERR" | block " qemu: "
|
|
qemu_error_hint
|
|
kvm_ms=""
|
|
fi
|
|
else
|
|
verdict P1.3 SKIP "KVM is not usable (P1.1, P1.2)"
|
|
fi
|
|
|
|
step "booting the guest under TCG (software emulation: this takes longer)"
|
|
qemu_args bench tcg -- -display none
|
|
run_guest 300 -- "${QARGS[@]}"
|
|
tcg_ms=$(guest_value "$RG_SERIAL" LOOP_MS)
|
|
if [ "$RG_RC" = 0 ] && [ -n "$tcg_ms" ]; then
|
|
verdict P1.4 PASS "a guest booted under TCG (software emulation) and ran the loop in $tcg_ms ms; boot to power-off $RG_MS ms"
|
|
else
|
|
verdict P1.4 FAIL "the guest did not run under TCG ($(rc_text "$RG_RC")): QEMU itself does not work here"
|
|
head_lines 6 "$RG_ERR" | block " qemu: "
|
|
tcg_ms=""
|
|
fi
|
|
|
|
if [ -n "$kvm_ms" ] && [ -n "$tcg_ms" ]; then
|
|
local ratio
|
|
ratio=$(awk -v t="$tcg_ms" -v k="$kvm_ms" 'BEGIN { if (k < 1) k = 1; printf "%.1f", t / k }')
|
|
fact P1 kvm_vs_tcg "TCG $tcg_ms ms / KVM $kvm_ms ms = ${ratio}x"
|
|
if awk -v r="$ratio" 'BEGIN { exit !(r >= 3) }'; then
|
|
verdict P1.5 PASS "KVM is ${ratio}x faster than TCG: several times, as hardware acceleration should be"
|
|
elif awk -v r="$ratio" 'BEGIN { exit !(r >= 1.5) }'; then
|
|
verdict P1.5 WARN "KVM is only ${ratio}x faster than TCG: it works, but less than it should"
|
|
fix "run the probe again with nothing else busy on Windows; if it stays under 3x, say so when you paste this"
|
|
else
|
|
verdict P1.5 FAIL "KVM is ${ratio}x TCG: the guest ran, but not accelerated"
|
|
fix "see P1.1 to P1.3, and say so when you paste this"
|
|
fi
|
|
else
|
|
verdict P1.5 SKIP "no KVM against TCG ratio: one of the two runs is missing"
|
|
fi
|
|
}
|
|
|
|
qemu_error_hint() {
|
|
if grep -qi 'permission denied' "$RG_ERR" 2>/dev/null; then
|
|
fix "QEMU was refused /dev/kvm: see P1.2 (the group kvm)"
|
|
elif grep -qiE 'no such file|could not access kvm' "$RG_ERR" 2>/dev/null; then
|
|
fix "there is no usable /dev/kvm: see P1.1 and P1.2"
|
|
elif grep -qiE 'not supported|entry failed' "$RG_ERR" 2>/dev/null; then
|
|
fix "KVM exists, but a CPU feature it needs is not passed through: .wslconfig [wsl2] nestedVirtualization=true," \
|
|
"then wsl --shutdown; and SVM Mode (AMD) or VT-x (Intel) on in the firmware setup"
|
|
else
|
|
fix "QEMU could not start KVM: see P1.1 and P1.2, and say what QEMU printed when you paste this"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# P2: display
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
# resolve_display: sets DISPLAY_ENV (variables to add to the shell's), HAVE_WAYLAND, HAVE_X11 and
|
|
# DISPLAY_NOTE; fails when there is no display at all
|
|
resolve_display() {
|
|
local runtime=${XDG_RUNTIME_DIR:-} wayland=${WAYLAND_DISPLAY:-} x11=${DISPLAY:-}
|
|
DISPLAY_ENV=()
|
|
DISPLAY_NOTE=""
|
|
HAVE_WAYLAND=no
|
|
HAVE_X11=no
|
|
if [ -n "$wayland" ] && [ -S "${runtime:-/nonexistent}/$wayland" ]; then
|
|
HAVE_WAYLAND=yes
|
|
DISPLAY_NOTE="the shell's Wayland socket $runtime/$wayland"
|
|
elif [ -S /mnt/wslg/runtime-dir/wayland-0 ]; then
|
|
HAVE_WAYLAND=yes
|
|
DISPLAY_ENV+=("XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir" "WAYLAND_DISPLAY=wayland-0")
|
|
DISPLAY_NOTE="WSLg's Wayland socket /mnt/wslg/runtime-dir/wayland-0 (the shell does not export it)"
|
|
fi
|
|
if [ -n "$x11" ]; then
|
|
HAVE_X11=yes
|
|
DISPLAY_NOTE="${DISPLAY_NOTE:+$DISPLAY_NOTE, and }the shell's DISPLAY=$x11"
|
|
elif [ -S /tmp/.X11-unix/X0 ]; then
|
|
HAVE_X11=yes
|
|
DISPLAY_ENV+=("DISPLAY=:0")
|
|
DISPLAY_NOTE="${DISPLAY_NOTE:+$DISPLAY_NOTE, and }the X socket :0 (the shell does not export DISPLAY)"
|
|
fi
|
|
[ -n "$DISPLAY_NOTE" ]
|
|
}
|
|
|
|
display_fix() {
|
|
if ! is_wsl; then
|
|
fix "run the probe where a display exists: a desktop session, or for a test: xvfb-run -a nix run .#wsl2-probe"
|
|
elif [ -d /mnt/wslg ]; then
|
|
fix "WSLg is there, but this shell does not know it. A login shell that wsl.exe starts has these set;" \
|
|
"here: export XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir WAYLAND_DISPLAY=wayland-0 DISPLAY=:0" \
|
|
"(start the shell with wsl.exe -d $DISTRO, not through su or ssh)"
|
|
else
|
|
fix "WSLg is not running: update WSL (PowerShell: wsl --update, then wsl --shutdown)," \
|
|
".wslconfig [wsl2] must not have guiApplications=false; WSLg needs Windows 11 or a recent Windows 10"
|
|
fi
|
|
}
|
|
|
|
# window_run ENV... -- QEMU-DISPLAY-ARGS...: opens the guest in a window for HOLD seconds, and
|
|
# sets WIN_OK (yes or no) and WIN_WHY
|
|
window_run() {
|
|
local -a venv=() accels=(tcg)
|
|
while [ $# -gt 0 ] && [ "$1" != -- ]; do
|
|
venv+=("$1")
|
|
shift
|
|
done
|
|
shift
|
|
[ "$(kv_get kvm_usable)" = yes ] && accels=(kvm tcg)
|
|
qemu_args hold "${accels[@]}" -- "$@"
|
|
run_guest $((HOLD + 120)) "${venv[@]}" -- "${QARGS[@]}"
|
|
WIN_OK=no
|
|
if [ "$RG_RC" = 0 ] && grep -q '^PROBE:DONE' "$RG_SERIAL"; then
|
|
WIN_OK=yes
|
|
WIN_WHY="QEMU started, the guest ran $HOLD s with its window up and powered off by itself (exit 0, $((RG_MS / 1000)) s in all)"
|
|
elif [ "$RG_RC" = 124 ]; then
|
|
WIN_WHY="QEMU stayed up, but the guest did not finish in $((HOLD + 120)) s"
|
|
else
|
|
WIN_WHY="QEMU stopped ($(rc_text "$RG_RC")) before the guest finished"
|
|
fi
|
|
}
|
|
|
|
# the lines of QEMU's error that say there is no display
|
|
no_display_error() {
|
|
grep -qiE 'gtk initialization failed|cannot open display|could not open display|display .* is not available|no display|unable to init|failed to open display' "$RG_ERR" 2>/dev/null
|
|
}
|
|
|
|
p2_display() {
|
|
heading "P2 display"
|
|
fact P2 environment "WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-unset} DISPLAY=${DISPLAY:-unset} XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-unset}"
|
|
fact P2 wslg "/mnt/wslg $([ -d /mnt/wslg ] && echo present || echo absent); runtime-dir: $(list_dir /mnt/wslg/runtime-dir | tr '\n' ' '); /tmp/.X11-unix: $(list_dir /tmp/.X11-unix | tr '\n' ' ')"
|
|
|
|
if resolve_display; then
|
|
if [ ${#DISPLAY_ENV[@]} -eq 0 ]; then
|
|
verdict P2.1 PASS "a display is reachable: $DISPLAY_NOTE"
|
|
else
|
|
verdict P2.1 WARN "a display is reachable only through the defaults of WSLg: $DISPLAY_NOTE"
|
|
display_fix
|
|
fi
|
|
elif is_wsl; then
|
|
verdict P2.1 FAIL "no display: no Wayland socket, no DISPLAY, no WSLg socket"
|
|
display_fix
|
|
else
|
|
verdict P2.1 WARN "no display: this machine has none, a legitimate answer outside WSL; the window check runs and reports what QEMU says"
|
|
fi
|
|
|
|
if [ "$DRY" = 1 ]; then
|
|
qemu_args hold kvm tcg -- -vga none -device "$VGA" -display gtk
|
|
out " would run (a window of $HOLD s): env ${DISPLAY_ENV[*]:-} $(printf '%q ' "${QARGS[@]}")"
|
|
verdict P2.2 SKIP "dry run"
|
|
return
|
|
fi
|
|
if ! guest_ready; then
|
|
guest_missing P2.2
|
|
return
|
|
fi
|
|
|
|
detail ">>> Look at the Windows desktop now: a QEMU window should open for about $HOLD seconds."
|
|
window_run "${DISPLAY_ENV[@]}" -- -vga none -device "$VGA" -display gtk
|
|
if [ "$WIN_OK" = no ] && [ "$HAVE_X11$HAVE_WAYLAND" = yesyes ]; then
|
|
# the window system the toolkit picked failed: try each of the two by name
|
|
local backend
|
|
for backend in x11 wayland; do
|
|
detail "retrying with GDK_BACKEND=$backend"
|
|
window_run "${DISPLAY_ENV[@]}" "GDK_BACKEND=$backend" -- -vga none -device "$VGA" -display gtk
|
|
if [ "$WIN_OK" = yes ]; then
|
|
kv_set gdk_backend "$backend"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
if [ "$WIN_OK" = yes ]; then
|
|
verdict P2.2 PASS "$WIN_WHY"
|
|
ask_window P2.3 "the QEMU window"
|
|
else
|
|
if no_display_error; then
|
|
verdict P2.2 "$(is_wsl && echo FAIL || echo WARN)" "QEMU has no display to open its window on: $WIN_WHY"
|
|
display_fix
|
|
else
|
|
verdict P2.2 FAIL "$WIN_WHY"
|
|
fi
|
|
head_lines 8 "$RG_ERR" | block " qemu: "
|
|
[ -s "$RG_SERIAL" ] && detail "the guest's output: $(head_lines 3 "$RG_SERIAL" | tr '\n' ' ')"
|
|
fi
|
|
detail "frame rate of the guest's framebuffer: not measured (the guest is a text console)"
|
|
}
|
|
|
|
# ask_window ID WHAT: asks whether the user saw the window, and records the answer
|
|
ask_window() {
|
|
local answer
|
|
answer=$(ask_yes_no "Did $2 appear on the Windows desktop?")
|
|
case $answer in
|
|
yes) verdict "$1" PASS "you saw $2 on the Windows desktop" ;;
|
|
"yes ("*) verdict "$1" INFO "$2 was not looked at: $answer" ;;
|
|
no)
|
|
verdict "$1" FAIL "QEMU ran, but you saw no window on the Windows desktop"
|
|
fix "WSLg may be drawing on a display that is not in view: restart it (PowerShell: wsl --shutdown) and run again"
|
|
;;
|
|
*) verdict "$1" INFO "you were not asked whether $2 appeared, or did not answer ($answer)" ;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# P3: GPU
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
# variant_env NAME: sets VENV, the environment variables of an OpenGL variant
|
|
variant_env() {
|
|
VENV=()
|
|
case $1 in
|
|
wsl-lib) VENV=("LD_LIBRARY_PATH=/usr/lib/wsl/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}") ;;
|
|
d3d12) VENV=("GALLIUM_DRIVER=d3d12") ;;
|
|
wsl-lib+d3d12) VENV=("GALLIUM_DRIVER=d3d12" "LD_LIBRARY_PATH=/usr/lib/wsl/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}") ;;
|
|
esac
|
|
}
|
|
|
|
# renderer_class LINES: the best class among renderer lines: d3d12, hardware, software or none
|
|
renderer_class() {
|
|
local best=none lower text
|
|
while IFS= read -r text; do
|
|
lower=${text,,}
|
|
case $lower in
|
|
*d3d12*) best=d3d12 ;;
|
|
*llvmpipe* | *softpipe* | *swrast* | *"software rasterizer"*) [ "$best" = none ] && best=software ;;
|
|
"") ;;
|
|
*) [ "$best" != d3d12 ] && best=hardware ;;
|
|
esac
|
|
done <<<"$1"
|
|
echo "$best"
|
|
}
|
|
|
|
# gl_renderers VARIANT: the renderers eglinfo and glxinfo report in the variant's environment
|
|
gl_renderers() {
|
|
variant_env "$1"
|
|
env "${DISPLAY_ENV[@]}" "${VENV[@]}" timeout 25 eglinfo -B >"$STATE/egl.$1" 2>&1 </dev/null
|
|
awk '/platform:$/ { plat = $0; sub(/ platform:$/, "", plat) }
|
|
/renderer( string)?:/ { r = $0; sub(/^[^:]*:[ \t]*/, "", r); k = plat " " r; if (!(k in seen)) { seen[k] = 1; print "eglinfo " k } }' \
|
|
"$STATE/egl.$1"
|
|
if [ "$HAVE_X11" = yes ] && have glxinfo; then
|
|
env "${DISPLAY_ENV[@]}" "${VENV[@]}" timeout 25 glxinfo -B >"$STATE/glx.$1" 2>&1 </dev/null
|
|
sed -n 's/^OpenGL renderer string: /glxinfo X11 /p' "$STATE/glx.$1" | head -n1
|
|
fi
|
|
}
|
|
|
|
p3_gpu() {
|
|
heading "P3 GPU"
|
|
p3_files
|
|
if [ "$DRY" = 1 ]; then
|
|
out " would run: eglinfo -B and glxinfo -B in four environments (the shell's; LD_LIBRARY_PATH=/usr/lib/wsl/lib;"
|
|
out " GALLIUM_DRIVER=d3d12; both), then $QEMU -device help and -display help,"
|
|
qemu_args hold kvm tcg -- -vga none -device "$VGA_GL" -display gtk,gl=on
|
|
out " then the window: $(printf '%q ' "${QARGS[@]}")"
|
|
verdict P3.4 SKIP "dry run"
|
|
return
|
|
fi
|
|
resolve_display
|
|
p3_renderers
|
|
p3_qemu_gl
|
|
}
|
|
|
|
p3_files() {
|
|
local wsl_libs="" name driver_libs gpu
|
|
if [ -e /dev/dxg ]; then
|
|
verdict P3.1 PASS "/dev/dxg exists ($(file_owner /dev/dxg)): WSL2 passes the Windows GPU through"
|
|
elif is_wsl; then
|
|
verdict P3.1 FAIL "/dev/dxg does not exist: the GPU is not passed through to this WSL2 machine"
|
|
fix "update the Windows GPU driver to one with WSL support (WDDM 2.9 or newer: AMD Adrenalin, NVIDIA, Intel)," \
|
|
"then in PowerShell: wsl --update ; wsl --shutdown and reopen"
|
|
else
|
|
verdict P3.1 INFO "/dev/dxg does not exist: this is not WSL (DRM nodes: $(list_dir /dev/dri | tr '\n' ' '))"
|
|
fi
|
|
|
|
for name in libd3d12.so libd3d12core.so libdxcore.so; do
|
|
[ -e "/usr/lib/wsl/lib/$name" ] && wsl_libs="$wsl_libs $name"
|
|
done
|
|
fact P3 usr_lib_wsl_lib "$(list_dir /usr/lib/wsl/lib | tr '\n' ' ')"
|
|
fact P3 usr_lib_wsl_drivers "$(list_dir /usr/lib/wsl/drivers | head -n 8 | tr '\n' ' ')"
|
|
if [[ " $wsl_libs " == *" libd3d12.so "* ]] && [[ " $wsl_libs " == *" libdxcore.so "* ]]; then
|
|
verdict P3.2 PASS "the Windows driver libraries are in /usr/lib/wsl/lib:$wsl_libs"
|
|
elif is_wsl; then
|
|
verdict P3.2 WARN "the Windows driver libraries are not all in /usr/lib/wsl/lib (found:${wsl_libs:- none})"
|
|
fix "update WSL and the Windows GPU driver; WSL mounts /usr/lib/wsl when it starts (wsl --update ; wsl --shutdown)"
|
|
else
|
|
verdict P3.2 INFO "there is no /usr/lib/wsl/lib: this is not WSL"
|
|
fi
|
|
|
|
driver_libs=$(find /run/opengl-driver/lib -maxdepth 1 \( -name 'libd3d12*' -o -name 'libdxcore*' \) 2>/dev/null | tr '\n' ' ')
|
|
fact P3 run_opengl_driver "d3d12 libraries: ${driver_libs:-none}; libd3d12.so -> $(readlink /run/opengl-driver/lib/libd3d12.so 2>/dev/null); dri: $(list_dir /run/opengl-driver/lib/dri | head -n 6 | tr '\n' ' ')"
|
|
fact P3 ld_library_path "${LD_LIBRARY_PATH:-unset}"
|
|
if [ -n "$driver_libs" ]; then
|
|
verdict P3.3 PASS "wsl.useWindowsDriver is in effect: the Windows libraries are linked into /run/opengl-driver/lib"
|
|
elif is_wsl; then
|
|
verdict P3.3 WARN "no Windows driver library in /run/opengl-driver/lib: wsl.useWindowsDriver does not seem to be on"
|
|
fix "in the NixOS-WSL configuration:" \
|
|
" wsl.useWindowsDriver = true;" \
|
|
" hardware.graphics.enable = true;" \
|
|
"then: sudo nixos-rebuild switch"
|
|
else
|
|
verdict P3.3 INFO "not WSL: wsl.useWindowsDriver does not apply"
|
|
fi
|
|
|
|
gpu=$(win powershell.exe -NoProfile -NonInteractive -Command "Get-CimInstance Win32_VideoController | ForEach-Object { \$_.Name + ' ' + \$_.DriverVersion }" | tr '\n' ';')
|
|
case $gpu in
|
|
"" | *"not recognized"* | *Exception*) ;;
|
|
*) fact P3 windows_gpu "$gpu" ;;
|
|
esac
|
|
}
|
|
|
|
p3_renderers() {
|
|
local variant best="" renderers class
|
|
if ! have eglinfo; then
|
|
verdict P3.4 SKIP "eglinfo is not on PATH: start the script as nix run .#wsl2-probe"
|
|
return
|
|
fi
|
|
step "asking eglinfo and glxinfo for the host's OpenGL renderer in four environments"
|
|
for variant in default wsl-lib d3d12 wsl-lib+d3d12; do
|
|
renderers=$(gl_renderers "$variant")
|
|
if [ -z "$renderers" ]; then
|
|
fact P3 "host_gl[$variant]" "no renderer: $(head_lines 2 "$STATE/egl.$variant" | tr '\n' ' ')"
|
|
continue
|
|
fi
|
|
fact P3 "host_gl[$variant]" "$(printf '%s' "$renderers" | tr '\n' ';')"
|
|
class=$(renderer_class "$renderers")
|
|
if [ -z "$best" ] && { [ "$class" = d3d12 ] || [ "$class" = hardware ]; }; then
|
|
best=$variant
|
|
fi
|
|
done
|
|
kv_set gl_best_variant "$best"
|
|
if [ -n "$best" ]; then
|
|
verdict P3.4 PASS "the host's OpenGL gives a hardware renderer in the variant '$best'"
|
|
if [ "$best" != default ]; then
|
|
variant_env "$best"
|
|
detail "the real guest has to start QEMU with this environment: ${VENV[*]}"
|
|
fi
|
|
else
|
|
verdict P3.4 WARN "no variant gives a hardware renderer: OpenGL on the host is software (llvmpipe) or missing"
|
|
fix "NixOS-WSL: wsl.useWindowsDriver = true; hardware.graphics.enable = true; (see P3.3)," \
|
|
"and the Windows GPU driver must be current; the real guest can fall back to software (--gpu off)"
|
|
fi
|
|
}
|
|
|
|
p3_qemu_gl() {
|
|
local devices displays best backend tried=default
|
|
if ! have "$QEMU"; then
|
|
guest_missing P3.5
|
|
return
|
|
fi
|
|
devices=$("$QEMU" -device help 2>&1 | grep -oE 'virtio-(vga|gpu)[a-z-]*gl[a-z-]*' | sort -u | tr '\n' ' ')
|
|
displays=$("$QEMU" -display help 2>&1 | awk 'NR > 1 && NF == 0 { exit } NR > 1 { printf "%s ", $1 }')
|
|
fact P3 qemu_gl_devices "${devices:-none}"
|
|
fact P3 qemu_displays "$displays"
|
|
if [[ " $devices " == *" $VGA_GL "* ]] && [[ " $displays " == *" gtk "* ]]; then
|
|
verdict P3.5 PASS "this QEMU has $VGA_GL and the GTK display"
|
|
else
|
|
verdict P3.5 FAIL "this QEMU lacks $VGA_GL or the GTK display"
|
|
fix "it is the QEMU of nixpkgs (qemu_kvm); say so when you paste this, there is nothing to repair here"
|
|
return
|
|
fi
|
|
|
|
if ! guest_ready; then
|
|
guest_missing P3.6
|
|
return
|
|
fi
|
|
backend=$(kv_get gdk_backend)
|
|
[ -n "$backend" ] && DISPLAY_ENV+=("GDK_BACKEND=$backend")
|
|
detail ">>> Look at the Windows desktop now: a window should open for about $HOLD seconds, with VirGL (gl=on)."
|
|
window_run "${DISPLAY_ENV[@]}" -- -vga none -device "$VGA_GL" -display gtk,gl=on
|
|
best=$(kv_get gl_best_variant)
|
|
if [ "$WIN_OK" = no ] && [ -n "$best" ] && [ "$best" != default ]; then
|
|
detail "retrying with the environment of the variant '$best'"
|
|
variant_env "$best"
|
|
window_run "${DISPLAY_ENV[@]}" "${VENV[@]}" -- -vga none -device "$VGA_GL" -display gtk,gl=on
|
|
tried=$best
|
|
fi
|
|
if [ "$WIN_OK" = yes ]; then
|
|
verdict P3.6 PASS "$WIN_WHY, with $VGA_GL and gl=on (environment of the variant '$tried')"
|
|
detail "the renderer inside the guest is not checked here: the real guest reports it"
|
|
ask_window P3.7 "the VirGL window"
|
|
else
|
|
if no_display_error; then
|
|
verdict P3.6 "$(is_wsl && echo FAIL || echo WARN)" "QEMU has no display for the GL window: $WIN_WHY"
|
|
display_fix
|
|
else
|
|
verdict P3.6 FAIL "QEMU refused or lost the GL display: $WIN_WHY"
|
|
if grep -qiE 'gl|egl|epoxy|opengl' "$RG_ERR" 2>/dev/null; then
|
|
fix "QEMU could not get an OpenGL context from the host: P3.4 says which environment gives a hardware" \
|
|
"renderer; without one the real guest has to run with --gpu off"
|
|
fi
|
|
fi
|
|
head_lines 8 "$RG_ERR" | block " qemu: "
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# P4: the Windows side
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
# free_port: a TCP port above 20000 that nothing listens on
|
|
free_port() {
|
|
local port
|
|
for _ in $(seq 1 50); do
|
|
port=$((20000 + RANDOM % 30000))
|
|
if ! ss -ltn "sport = :$port" 2>/dev/null | grep -q LISTEN; then
|
|
echo "$port"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# start_listener ADDRESS PORT: a socat that answers a line, until the script ends
|
|
start_listener() {
|
|
socat "TCP-LISTEN:$2,bind=$1,reuseaddr,fork" "SYSTEM:echo wsl2-probe" >/dev/null 2>&1 &
|
|
echo $! >>"$STATE/pids"
|
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
ss -ltn "sport = :$2" 2>/dev/null | grep -q LISTEN && return 0
|
|
sleep 0.2
|
|
done
|
|
return 1
|
|
}
|
|
|
|
p4_windows() {
|
|
heading "P4 Windows side"
|
|
local ver mstsc cmdkey mode forwarding
|
|
|
|
if [ "$DRY" = 1 ]; then
|
|
out " would run: cmd.exe /c ver; find mstsc.exe and cmdkey.exe; wslinfo --networking-mode;"
|
|
out " socat listeners on two free ports (127.0.0.1 and 0.0.0.0), then powershell.exe:"
|
|
out " a TCP connection from Windows to each, as 127.0.0.1 and as localhost"
|
|
verdict P4.1 SKIP "dry run"
|
|
return
|
|
fi
|
|
if ! is_wsl; then
|
|
verdict P4.1 INFO "this is not WSL: there is no Windows side to check"
|
|
return
|
|
fi
|
|
|
|
ver=$(win cmd.exe /c ver | grep -m1 Windows)
|
|
if [ -n "$ver" ]; then
|
|
verdict P4.1 PASS "Windows programs run from WSL (interop): $ver"
|
|
else
|
|
verdict P4.1 FAIL "cmd.exe does not run from WSL: interop is off or cmd.exe is not on PATH"
|
|
fix "in the NixOS-WSL configuration:" \
|
|
" wsl.interop.includePath = true;" \
|
|
" wsl.interop.register = true;" \
|
|
" wsl.wslConf.interop.enabled = true;" \
|
|
"then: sudo nixos-rebuild switch; in PowerShell: wsl --terminate $DISTRO, and reopen"
|
|
return
|
|
fi
|
|
|
|
if mstsc=$(find_win mstsc.exe); then
|
|
verdict P4.2 PASS "mstsc.exe (the Windows RDP client) is reachable: $mstsc"
|
|
else
|
|
verdict P4.2 WARN "mstsc.exe is neither on PATH nor in /mnt/c/Windows/System32"
|
|
fix "the Remote Desktop client is part of Windows; the Windows PATH may be missing here: wsl.interop.includePath = true;"
|
|
fi
|
|
if cmdkey=$(find_win cmdkey.exe); then
|
|
fact P4 cmdkey "$cmdkey"
|
|
else
|
|
fact P4 cmdkey "not found: the RDP credentials would have to be typed"
|
|
fi
|
|
|
|
mode=$(wslinfo --networking-mode 2>/dev/null)
|
|
[ -n "$mode" ] || mode=$(wslconfig_get networkingMode)
|
|
forwarding=$(wslconfig_get localhostForwarding)
|
|
fact P4 networking_mode "${mode:-unknown (NAT is the default)}; localhostForwarding ${forwarding:-unset (true by default)}"
|
|
fact P4 wsl_addresses "$(ip -brief addr 2>/dev/null | grep -v '^lo ' | tr -s ' ' | tr '\n' ';')"
|
|
p4_forward
|
|
}
|
|
|
|
p4_forward() {
|
|
local port_a port_b script lines ip_a name_a ip_b
|
|
if ! have socat; then
|
|
verdict P4.3 SKIP "socat is not on PATH: start the script as nix run .#wsl2-probe"
|
|
return
|
|
fi
|
|
if ! { port_a=$(free_port) && port_b=$(free_port); }; then
|
|
verdict P4.3 SKIP "no free high port found"
|
|
return
|
|
fi
|
|
start_listener 127.0.0.1 "$port_a"
|
|
start_listener 0.0.0.0 "$port_b"
|
|
|
|
# one line: a script with newlines in it does not survive every way a command line reaches
|
|
# powershell.exe
|
|
script="foreach (\$t in @(@('127.0.0.1', $port_a), @('localhost', $port_a), @('127.0.0.1', $port_b), @('localhost', $port_b))) {"
|
|
script="$script \$r = 'closed'; try { \$c = New-Object Net.Sockets.TcpClient;"
|
|
script="$script \$w = \$c.BeginConnect(\$t[0], \$t[1], \$null, \$null);"
|
|
script="$script if (\$w.AsyncWaitHandle.WaitOne(3000) -and \$c.Connected) { \$r = 'open' }; \$c.Close() }"
|
|
script="$script catch { \$r = 'error ' + \$_.Exception.Message };"
|
|
script="$script \$t[0] + ':' + \$t[1] + ' ' + \$r }"
|
|
step "connecting from Windows (powershell.exe starts slowly)"
|
|
lines=$(WIN_TIMEOUT=60 win powershell.exe -NoProfile -NonInteractive -Command "$script")
|
|
if [ -z "$lines" ]; then
|
|
verdict P4.3 WARN "powershell.exe gave no answer, so the forward was not tested"
|
|
fix "powershell.exe may be blocked or missing. By hand: in WSL, socat TCP-LISTEN:3390,bind=127.0.0.1,fork -" \
|
|
"and in PowerShell: Test-NetConnection 127.0.0.1 -Port 3390"
|
|
return
|
|
fi
|
|
detail "from Windows (powershell.exe, a TCP connect with a 3 s limit): a listener on 127.0.0.1:$port_a and one on 0.0.0.0:$port_b"
|
|
printf '%s\n' "$lines" | block " "
|
|
ip_a=$(printf '%s\n' "$lines" | sed -n "s/^127.0.0.1:$port_a //p")
|
|
name_a=$(printf '%s\n' "$lines" | sed -n "s/^localhost:$port_a //p")
|
|
ip_b=$(printf '%s\n' "$lines" | sed -n "s/^127.0.0.1:$port_b //p")
|
|
fact P4 localhost_forward "listener on 127.0.0.1 reached as 127.0.0.1: ${ip_a:-?}, as localhost: ${name_a:-?}; listener on 0.0.0.0 reached as 127.0.0.1: ${ip_b:-?}"
|
|
|
|
if [ "$ip_a" = open ] && [ "$name_a" = open ]; then
|
|
verdict P4.3 PASS "Windows reaches a port WSL listens on at 127.0.0.1, as 127.0.0.1 and as localhost: mstsc /v:localhost:PORT can work"
|
|
elif [ "$ip_a" = open ]; then
|
|
verdict P4.3 WARN "Windows reaches 127.0.0.1 but not localhost (which may try ::1 first)"
|
|
fix "use mstsc.exe /v:127.0.0.1:PORT, as the real command will"
|
|
elif [ "$ip_b" = open ]; then
|
|
verdict P4.3 WARN "Windows reaches a listener on 0.0.0.0 but not one on 127.0.0.1"
|
|
fix "the guest's port would have to be published on 0.0.0.0, and not only on 127.0.0.1; say so when you paste this"
|
|
else
|
|
verdict P4.3 FAIL "Windows does not reach a port that WSL listens on"
|
|
fix "in $(wslconfig_name):" \
|
|
" [wsl2]" \
|
|
" localhostForwarding=true" \
|
|
" networkingMode=nat (mirrored mode shares the loopback instead: try the other mode)" \
|
|
"then in PowerShell: wsl --shutdown, reopen, and run the probe again"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# P5: resources
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
p5_resources() {
|
|
heading "P5 resources for the real guest"
|
|
detail "the guest wants 8 GiB of memory, 4 or more cores and about 25 GiB of disk, plus the Nix build"
|
|
local kib cpus free
|
|
kib=$(mem_kib MemTotal)
|
|
cpus=$(nproc)
|
|
free=$(disk_free_kib)
|
|
|
|
# 8 GiB for the guest, 2 GiB for WSL and Nix next to it
|
|
if [ "${kib:-0}" -ge $((10 * 1048576)) ]; then
|
|
verdict P5.1 PASS "memory: $(gib "$kib") GiB in this machine, 10 GiB wanted (8 for the guest, 2 for WSL and Nix)"
|
|
elif [ "${kib:-0}" -ge $((6 * 1048576)) ]; then
|
|
verdict P5.1 WARN "memory: $(gib "$kib") GiB in this machine, 10 GiB wanted: the guest would have to run with 6 GiB (--memory 6144)"
|
|
p5_memory_fix
|
|
else
|
|
verdict P5.1 FAIL "memory: $(gib "${kib:-0}") GiB in this machine, less than the 6 GiB that is the least the guest can run with"
|
|
p5_memory_fix
|
|
fi
|
|
|
|
if [ "$cpus" -ge 4 ]; then
|
|
verdict P5.2 PASS "cores: $cpus in this machine, 4 wanted"
|
|
else
|
|
verdict P5.2 WARN "cores: $cpus in this machine, 4 or more wanted: the guest and the build would be slow"
|
|
fix "in $(wslconfig_name) (now: processors=$(wslconfig_get processors || true)):" \
|
|
" [wsl2]" \
|
|
" processors=6" \
|
|
"then in PowerShell: wsl --shutdown, and reopen"
|
|
fi
|
|
|
|
# 25 GiB for the Plasma closure and the guest's disk, 10 GiB for the build
|
|
if [ -z "$free" ]; then
|
|
verdict P5.3 SKIP "disk: the free space could not be read"
|
|
elif [ "$free" -ge $((40 * 1048576)) ]; then
|
|
verdict P5.3 PASS "disk: $(gib "$free") GiB free for the Nix store and the guest (under WSL: the Windows drive under the virtual disk), 40 GiB wanted"
|
|
elif [ "$free" -ge $((25 * 1048576)) ]; then
|
|
verdict P5.3 WARN "disk: $(gib "$free") GiB free, 40 GiB wanted (25 for the closure and the guest's disk, 10 for the build): it will be tight"
|
|
p5_disk_fix
|
|
else
|
|
verdict P5.3 FAIL "disk: $(gib "$free") GiB free, 25 GiB is the least the closure and the guest's disk need"
|
|
p5_disk_fix
|
|
fi
|
|
}
|
|
|
|
p5_memory_fix() {
|
|
fix "in $(wslconfig_name) (now: memory=$(wslconfig_get memory || true)):" \
|
|
" [wsl2]" \
|
|
" memory=12GB" \
|
|
" swap=8GB" \
|
|
"then in PowerShell: wsl --shutdown, and reopen (the Windows machine needs about 16 GB for this)"
|
|
}
|
|
|
|
p5_disk_fix() {
|
|
fix "free space on the Windows drive that holds the distribution's virtual disk (C: unless it was moved)," \
|
|
"and in the distribution: nix-collect-garbage -d" \
|
|
"the virtual disk can be grown too (PowerShell): wsl --manage $DISTRO --resize 200GB"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# P6: virtiofs
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
# The real guest mounts the host's /nix/store and its shared directories by virtiofs (nixpkgs does
|
|
# so for every Linux host). For each of them the guest's runner starts a virtiofsd, which serves a
|
|
# directory over a socket, and QEMU joins the guest's virtio-fs device to that socket (vhost-user)
|
|
# with the guest's memory in a shared memfd. This probe builds the same chain with the same
|
|
# options, for the host's /nix/store (read-only) and a temporary directory (writable).
|
|
|
|
# the options of nixpkgs' runner (nixos/modules/virtualisation/qemu-vm.nix), after the socket and
|
|
# the directory; the runner's --cache is always for the store and auto for the other shares
|
|
P6_VIRTIOFSD_COMMON=(--sandbox=none --seccomp=none --no-announce-submounts
|
|
--translate-uid=host:65534:0:1 --translate-gid=host:65534:0:1)
|
|
|
|
# p6_daemon NAME DIR ro|rw CACHE: starts virtiofsd for the directory and waits for its socket;
|
|
# fails when it does not come up, with the daemon's output in $STATE/virtiofsd.NAME.log
|
|
p6_daemon() {
|
|
local name=$1 dir=$2 access=$3 cache=$4 mode=--writeback pid
|
|
[ "$access" = ro ] && mode=--readonly
|
|
virtiofsd --socket-path="$STATE/virtiofsd.$name.sock" --shared-dir="$dir" "$mode" \
|
|
--cache="$cache" "${P6_VIRTIOFSD_COMMON[@]}" >"$STATE/virtiofsd.$name.log" 2>&1 </dev/null &
|
|
pid=$!
|
|
echo "$pid" >>"$STATE/pids"
|
|
kv_set "virtiofsd_pid_$name" "$pid"
|
|
for _ in $(seq 1 50); do
|
|
[ -S "$STATE/virtiofsd.$name.sock" ] && return 0
|
|
kill -0 "$pid" 2>/dev/null || return 1
|
|
sleep 0.1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
p6_stop_daemons() {
|
|
local name pid
|
|
for name in store share; do
|
|
pid=$(kv_get "virtiofsd_pid_$name")
|
|
[ -n "$pid" ] && kill "$pid" 2>/dev/null
|
|
done
|
|
return 0
|
|
}
|
|
|
|
p6_fuse() {
|
|
if [ ! -e /dev/fuse ]; then
|
|
verdict P6.1 WARN "/dev/fuse does not exist: this kernel has no FUSE, or its device node was not made"
|
|
detail "virtiofsd does not open /dev/fuse (it speaks FUSE to the guest over a socket), so the guest can still start;" \
|
|
"what needs it is another FUSE program, not run-kde"
|
|
fix "NixOS: boot.kernelModules = [ \"fuse\" ]; then sudo nixos-rebuild switch" \
|
|
"WSL2: the kernel of WSL has FUSE built in (CONFIG_FUSE_FS=y in its 6.6 configuration), so a kernel= line" \
|
|
"in $(wslconfig_name) [wsl2] that names another kernel is the likely cause"
|
|
elif (exec 9<>/dev/fuse) 2>/dev/null; then
|
|
verdict P6.1 PASS "/dev/fuse can be opened read-write by $ME ($(file_owner /dev/fuse)); virtiofsd does not need it"
|
|
else
|
|
verdict P6.1 WARN "/dev/fuse exists but $ME cannot open it ($(file_owner /dev/fuse)); virtiofsd does not need it"
|
|
fix "NixOS: services.udev.extraRules = ''KERNEL==\"fuse\", MODE=\"0666\"''; then sudo nixos-rebuild switch" \
|
|
"for now: sudo chmod 666 /dev/fuse"
|
|
fi
|
|
}
|
|
|
|
p6_virtiofs() {
|
|
heading "P6 virtiofs (the guest's /nix/store and shared directories)"
|
|
local resolved storedir share sha_host accels=(tcg) item seen=""
|
|
p6_fuse
|
|
|
|
if [ "$DRY" = 1 ]; then
|
|
out " would run: virtiofsd ${P6_VIRTIOFSD_COMMON[*]} --readonly --cache=always for /nix/store, and --writeback for a temporary directory,"
|
|
out " then $QEMU with -object memory-backend-memfd,share=on, -machine memory-backend=, and a vhost-user-fs-pci device for each;"
|
|
out " the guest loads virtio_pci and virtiofs, mounts both, reads and writes, and powers off"
|
|
verdict P6.2 SKIP "dry run"
|
|
return
|
|
fi
|
|
if ! have virtiofsd; then
|
|
verdict P6.2 SKIP "virtiofsd is not on PATH: start the script as nix run .#wsl2-probe"
|
|
return
|
|
fi
|
|
fact P6 virtiofsd "$(command -v virtiofsd), $(virtiofsd --version 2>&1 | head -n1)"
|
|
if ! guest_ready; then
|
|
guest_missing P6.2
|
|
return
|
|
fi
|
|
if ! "$QEMU" -device help 2>&1 | grep -q 'vhost-user-fs-pci'; then
|
|
verdict P6.2 FAIL "this QEMU has no vhost-user-fs-pci device, which the guest's /nix/store needs"
|
|
fix "it is the QEMU of nixpkgs (qemu_kvm); say so when you paste this, there is nothing to repair here"
|
|
return
|
|
fi
|
|
|
|
resolved=$(readlink -f "$GUEST/busybox")
|
|
storedir=$(dirname "$(dirname "$(dirname "$resolved")")")
|
|
P6_STOREFILE=${resolved#"$storedir"/}
|
|
sha_host=$(sha256sum "$resolved" | cut -d' ' -f1)
|
|
P6_TOKEN=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
|
share=$STATE/p6-share
|
|
mkdir -p "$share"
|
|
printf '%s\n' "$P6_TOKEN" >"$share/token"
|
|
|
|
if p6_daemon store "$storedir" ro always && p6_daemon share "$share" rw auto; then
|
|
verdict P6.2 PASS "virtiofsd started as $ME (uid $(id -u)) and made its sockets: the store at $storedir read-only, a temporary directory writable"
|
|
fact P6 virtiofsd_log "$(grep -h -E 'WARN|ERROR' "$STATE"/virtiofsd.*.log | sed 's/^\[[^ ]* *\([A-Z]*\) *\([^]]*\)\] /\1 \2: /' | sort -u | tr '\n' ';')"
|
|
else
|
|
verdict P6.2 FAIL "virtiofsd did not start or did not make its socket"
|
|
head_lines 8 "$STATE/virtiofsd.store.log" | block " virtiofsd: "
|
|
head_lines 8 "$STATE/virtiofsd.share.log" | block " virtiofsd: "
|
|
fix "virtiofsd ran with the options of the guest's runner and could not serve a directory: the lines above say why." \
|
|
"A socket path is 107 bytes at most, so a long \$TMPDIR is one cause: TMPDIR=/tmp nix run .#wsl2-probe" \
|
|
"Say what virtiofsd printed when you paste this: the guest cannot start without it"
|
|
p6_stop_daemons
|
|
return
|
|
fi
|
|
|
|
[ -e /dev/kvm ] && (exec 9<>/dev/kvm) 2>/dev/null && accels=(kvm tcg)
|
|
step "booting the guest with its /nix/store and a shared directory by virtiofs"
|
|
qemu_args virtiofs "${accels[@]}" -- -display none \
|
|
-object memory-backend-memfd,id=mem0,size=512M,share=on -machine memory-backend=mem0 \
|
|
-chardev "socket,id=store,path=$STATE/virtiofsd.store.sock" -device vhost-user-fs-pci,chardev=store,tag=probestore \
|
|
-chardev "socket,id=share,path=$STATE/virtiofsd.share.sock" -device vhost-user-fs-pci,chardev=share,tag=probe
|
|
run_guest 300 -- "${QARGS[@]}"
|
|
p6_stop_daemons
|
|
|
|
if [ "$RG_RC" != 0 ] || ! grep -q '^PROBE:DONE' "$RG_SERIAL"; then
|
|
verdict P6.3 FAIL "the guest with virtio-fs devices did not run to its end ($(rc_text "$RG_RC"))"
|
|
head_lines 8 "$RG_ERR" | block " qemu: "
|
|
[ -s "$RG_SERIAL" ] && detail "the guest's output: $(head_lines 6 "$RG_SERIAL" | tr '\n' ' ')"
|
|
if grep -qiE 'memory-backend|share|vhost' "$RG_ERR" 2>/dev/null; then
|
|
fix "QEMU refused the shared memory or the vhost-user socket: say what it printed when you paste this"
|
|
else
|
|
fix "say what QEMU and the guest printed when you paste this"
|
|
fi
|
|
return
|
|
fi
|
|
verdict P6.3 PASS "QEMU started the guest with a shared memfd and two vhost-user-fs devices, on ${accels[0]}; boot to power-off $RG_MS ms"
|
|
for item in virtio_pci virtiofs; do
|
|
seen="$seen$item:$(guest_value "$RG_SERIAL" "MODULE_$item") "
|
|
done
|
|
fact P6 guest_modules "$seen"
|
|
fact P6 guest_filesystems "$(guest_value "$RG_SERIAL" FILESYSTEMS)"
|
|
|
|
p6_check_store "$sha_host"
|
|
p6_check_share
|
|
}
|
|
|
|
# p6_why MOUNT: what the guest said about a mount, without its "failed: " in front
|
|
p6_why() {
|
|
local why=${1#failed: }
|
|
echo "${why:-the guest said nothing}"
|
|
}
|
|
|
|
# p6_check_store SHA: the guest read the host's /nix/store through virtiofs
|
|
p6_check_store() {
|
|
local mount sha write
|
|
mount=$(guest_value "$RG_SERIAL" STORE_MOUNT)
|
|
sha=$(guest_value "$RG_SERIAL" STORE_SHA256)
|
|
write=$(guest_value "$RG_SERIAL" STORE_WRITE)
|
|
fact P6 guest_store "mount ${mount:-no answer}; sha256 of $P6_STOREFILE ${sha:-none}; write ${write:-no answer}"
|
|
if [ "$mount" != ok ]; then
|
|
verdict P6.4 FAIL "the guest could not mount the host's /nix/store by virtiofs ($(p6_why "$mount"))"
|
|
p6_guest_fix
|
|
elif [ "$sha" != "$1" ]; then
|
|
verdict P6.4 FAIL "the guest mounted /nix/store but read another file than the host has (sha256 ${sha:-none}, expected $1)"
|
|
elif [ "$write" != refused ]; then
|
|
verdict P6.4 WARN "the guest read /nix/store by virtiofs, but could write to it ($write): the share is meant to be read-only"
|
|
else
|
|
verdict P6.4 PASS "the guest mounted the host's /nix/store by virtiofs read-only and read $P6_STOREFILE with the host's checksum"
|
|
fi
|
|
}
|
|
|
|
# p6_check_share: the guest read and wrote a writable shared directory
|
|
p6_check_share() {
|
|
local mount got write host_text
|
|
mount=$(guest_value "$RG_SERIAL" SHARE_MOUNT)
|
|
got=$(guest_value "$RG_SERIAL" SHARE_READ)
|
|
write=$(guest_value "$RG_SERIAL" SHARE_WRITE)
|
|
host_text=$(head -n1 "$STATE/p6-share/from-guest" 2>/dev/null)
|
|
fact P6 guest_share "mount ${mount:-no answer}; listing $(guest_value "$RG_SERIAL" SHARE_LISTING); the host user's file seen as $(guest_value "$RG_SERIAL" SHARE_READ_OWNER); the guest's file seen as $(guest_value "$RG_SERIAL" SHARE_WRITE_OWNER)"
|
|
fact P6 host_share "the guest's file on the host: $(file_owner "$STATE/p6-share/from-guest" || echo none) (host user $ME, uid $(id -u))"
|
|
if [ "$mount" != ok ]; then
|
|
verdict P6.5 FAIL "the guest could not mount a writable shared directory by virtiofs ($(p6_why "$mount"))"
|
|
p6_guest_fix
|
|
elif [ "$got" != "$P6_TOKEN" ]; then
|
|
verdict P6.5 FAIL "the guest read '${got:-nothing}' from the shared directory, not the token the host wrote"
|
|
elif [ "$write" != ok ] || [ "$host_text" != "$P6_TOKEN" ]; then
|
|
verdict P6.5 FAIL "the guest could not write the shared directory, or the host did not see what it wrote (write: ${write:-no answer}; host read: ${host_text:-nothing})"
|
|
else
|
|
verdict P6.5 PASS "the guest read the host's file and wrote a file that the host reads, through a writable virtiofs share"
|
|
fi
|
|
}
|
|
|
|
p6_guest_fix() {
|
|
if [ "$(guest_value "$RG_SERIAL" MODULE_virtiofs)" != loaded ]; then
|
|
fix "the guest kernel could not load virtiofs: $(guest_value "$RG_SERIAL" MODULE_virtiofs)" \
|
|
"(a kernel of nixpkgs has it as a module; say so when you paste this)"
|
|
else
|
|
fix "virtiofsd ran and the guest has the module, but the mount failed: look at the virtiofsd lines (P6 virtiofsd_log)" \
|
|
"and say what the guest printed when you paste this"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------------------------
|
|
# the dry run, the summary and the main loop
|
|
# ---------------------------------------------------------------------------------------------
|
|
|
|
dry_guest_plan() {
|
|
detail "dry run: nothing is started. The guest would be:"
|
|
qemu_args bench kvm -- -display none
|
|
out " under KVM: $(printf '%q ' "${QARGS[@]}")"
|
|
qemu_args bench tcg -- -display none
|
|
out " under TCG: $(printf '%q ' "${QARGS[@]}")"
|
|
out " and, on this machine: busybox sh -c '. loop.sh; probe_loop $LOOPS'"
|
|
verdict P1.3 SKIP "dry run"
|
|
}
|
|
|
|
# the worst status among the checks of one probe
|
|
probe_status() {
|
|
awk -F'|' -v prefix="$1." '
|
|
index($1, prefix) == 1 {
|
|
if ($2 == "FAIL") fail = 1
|
|
else if ($2 == "WARN") warn = 1
|
|
else if ($2 == "PASS") pass = 1
|
|
else if ($2 == "SKIP") skip = 1
|
|
}
|
|
END { print fail ? "FAIL" : warn ? "WARN" : pass ? "PASS" : skip ? "SKIP" : "INFO" }' "$STATE/verdicts"
|
|
}
|
|
|
|
summary() {
|
|
local probe title text
|
|
out ""
|
|
out "================================ PASTE THIS ================================"
|
|
out "komorebi (KDE Plasma) wsl2-probe, revision $REV, $(date -u +%Y-%m-%dT%H:%M:%SZ), $(uname -m)$([ "$DRY" = 1 ] && echo ', DRY RUN')"
|
|
for probe in P0 P1 P2 P3 P4 P5 P6; do
|
|
[ "$probe" = P0 ] || want "$probe" || continue
|
|
case $probe in
|
|
P0) title="environment" ;;
|
|
P1) title="KVM" ;;
|
|
P2) title="display (a QEMU window through WSLg)" ;;
|
|
P3) title="GPU (VirGL)" ;;
|
|
P4) title="Windows side (mstsc, the localhost forward)" ;;
|
|
P5) title="resources for the real guest" ;;
|
|
P6) title="virtiofs (the guest's /nix/store and shares)" ;;
|
|
esac
|
|
out "$probe $title: $(probe_status "$probe")"
|
|
awk -F'|' -v prefix="$probe." 'index($1, prefix) == 1 { printf " %s %s %s\n", $1, $2, $3 }' "$STATE/verdicts" | block ""
|
|
done
|
|
out "-- facts"
|
|
while IFS= read -r text; do
|
|
out "$text"
|
|
done <"$STATE/facts"
|
|
out "============================== END OF PASTE ================================"
|
|
out ""
|
|
out "The report is in $REPORT_PATH"
|
|
out "Paste the block between the two lines of equals signs; the whole report if it is asked for."
|
|
}
|
|
|
|
out "wsl2-probe of komorebi (KDE Plasma), revision $REV: $(date -u +%Y-%m-%dT%H:%M:%SZ)$([ "$DRY" = 1 ] && echo ' (dry run)')"
|
|
out "It changes nothing: it reads the machine, boots a tiny guest that powers itself off, and listens on local ports for a few seconds."
|
|
if [ "$DRY" = 0 ] && { want P2 || want P3; }; then
|
|
out "It takes about two minutes. A window opens for $HOLD seconds in P2 and in P3: look at the Windows desktop."
|
|
fi
|
|
|
|
# each probe runs in a subshell of its own: one that dies leaves the others alone, and the file
|
|
# it writes at the end says that it did not die
|
|
run_probe() {
|
|
local probe=$1
|
|
[ "$probe" = P0 ] || want "$probe" || return 0
|
|
case $probe in
|
|
P0) (p0_environment; : >"$STATE/done.P0") ;;
|
|
P1) (p1_kvm; : >"$STATE/done.P1") ;;
|
|
P2) (p2_display; : >"$STATE/done.P2") ;;
|
|
P3) (p3_gpu; : >"$STATE/done.P3") ;;
|
|
P4) (p4_windows; : >"$STATE/done.P4") ;;
|
|
P5) (p5_resources; : >"$STATE/done.P5") ;;
|
|
P6) (p6_virtiofs; : >"$STATE/done.P6") ;;
|
|
esac
|
|
if [ ! -e "$STATE/done.$probe" ]; then
|
|
verdict "$probe.99" FAIL "the probe stopped before it finished (a bug in the probe): the lines above are what it found"
|
|
fi
|
|
}
|
|
|
|
for probe in P0 P1 P2 P3 P4 P5 P6; do
|
|
run_probe "$probe"
|
|
done
|
|
|
|
summary
|
|
exit 0
|