Conversation
The kernel config command was concatenated from string fragments inside
an @(...) array literal. In PowerShell a newline inside an array literal
separates elements, so a trailing "+" does not continue the expression --
the array parsed to 12 elements instead of 6.
As a result "bash -c" received only the first fragment,
"cd ~/src/wsl-kernel && zcat /proc/config.gz > .config &&", a script
ending in a dangling "&&", and failed with:
-c: line 2: syntax error: unexpected end of file
The remaining fragments were passed as $0, $1, ... which is why the
error quoted "./scripts/config --set-val CONFIG_ANDROID_BINDER_IPC y"
as the shell name.
Build the command in a variable using backtick line continuations, then
pass it as a single argument.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Install-Waydroid.ps1 is CRLF, so every multi-line string literal carries
CR characters, and bash treats CR as part of the token. Two failures:
1. The /etc/wsl.conf step opens a heredoc with << "PYEOF". The CR after
the closing quote makes the delimiter PYEOF<CR>, which no line matches,
so bash consumed the terminator as script text:
warning: here-document at line 1 delimited by end-of-file
NameError: name 'PYEOF' is not defined
The file was actually written, but python3 exited 1 and the installer
aborted.
2. The launcher .sh templates were cp'd into /opt/waydroid-launcher
verbatim, keeping CRLF. The CR after "#!/bin/bash" makes the kernel
look for an interpreter literally named "/bin/bash<CR>", so every
launcher would have died at runtime with exit 127:
cannot execute: required file not found
Fixes:
- Invoke-Wsl normalizes CRLF to LF on every argument before handing it to
wsl.exe, so any multi-line payload is what a Linux shell expects.
- The launcher install strips CR with tr instead of a plain cp. (tr rather
than a sed "$"-anchored substitution, because "$" collides with
PowerShell variable expansion inside a double-quoted string.)
- .gitattributes forces LF on *.sh and *.py so a fresh clone does not
reintroduce the problem, while keeping *.ps1 / *.bat / *.tmpl CRLF.
Verified against a live Ubuntu WSL2 distro: the wsl.conf step now exits 0
and writes the expected [boot] section, and a launcher installed through
the new path reports "ASCII text executable" with a bare LF shebang and
passes bash -n.
The launcher runs Android inside a nested Weston compositor (the documented
workaround for Waydroid rendering a stuck 1x1 buffer against WSLg's
compositor), but nothing ever installed weston. A completed install would
produce desktop shortcuts that silently do nothing:
/tmp/nested-weston.log
nohup: failed to run command 'weston': No such file or directory
/tmp/waydroid-session.log
Wayland socket '/run/user/0/wayland-1' doesn't exist;
are you running a Wayland compositor?
With no weston there is no wayland-1 socket, so both `waydroid session
start` and `waydroid show-full-ui` fail immediately. Because
waydroid-start-user.sh uses `set -u` without `set -e` and backgrounds
every real command via setsid/nohup with output redirected to logs, all
three failures were invisible and the script still exited 0 -- the .bat
printed "Done." and closed on a 5s timer.
Changes:
- Install weston as its own idempotent step. Deliberately not folded into
the waydroid install block: that block is skipped wholesale when
waydroid is already present, which would leave weston missing on a
re-run or an upgrade. Hard-fails with an actionable message if weston is
still absent afterwards.
- waydroid-start-user.sh checks for weston up front and exits 1 with an
explanation, instead of running headlong into an unusable display.
- Start-Waydroid.bat checks errorlevel and pauses, so the failure is
readable instead of scrolling past before the window closes.
Verified on Ubuntu 24.04 / WSL2: with weston 13.0.0 installed the launcher
brings the session up ("Android with user 0 is ready", status RUNNING,
wayland-1 socket created). With weston hidden from PATH the new guard exits
1 and prints the install hint.
The step that hides Waydroid's per-app .desktop files ran immediately after `waydroid init`, before any session had ever started. Waydroid generates those files when a session first starts, so the glob matched nothing and the `mv` silently did nothing -- `2>/dev/null; true` swallowed the error and the step reported success. The result was the exact failure the step exists to prevent: after a completed install every Android app was still registered with WSLg, and Windows indexed all of them into the Start Menu. Observed on a real install -- 12 files left in ~/.local/share/applications, 0 in ~/.local/share/applications-disabled. Move it to the end, after the smoke test has brought a session up and the files actually exist. Also report how many were moved, so a future silent no-op is visible, and warn when -SkipSmokeTest means no session has run yet and there is genuinely nothing to move. Verified against a live install: the relocated step moves 12 files and reports 12; the count command returns "0" (not empty) when the directory does not exist, so the -SkipSmokeTest warning branch fires correctly.
Clicking "Start Waydroid" a second time left Android running but invisible,
with no way to recover short of a full stop.
waydroid-start-user.sh unconditionally began with:
pkill -f 'weston --backend=wayland-backend'
so a second click killed the compositor out from under the running session.
The session process survives, but its Wayland connection is gone, and the
script's next step then hits:
[19:34:20] Session is already running
`waydroid session start` is a no-op while a session exists, so the session
never reattaches to the newly spawned weston. Confirmed with `ss -xp`:
nothing at all connected to /run/user/1000/wayland-1, while the session
process from the first start was still alive and the weston it had been
talking to was a different, already-dead pid. `waydroid show-full-ui`
returns 0 and does nothing. The result is an empty Weston window and no
Android UI.
Handle the two states explicitly instead:
- Session RUNNING and weston alive: just re-show the UI and exit. Do not
touch the compositor the session is attached to.
- Session RUNNING but no compositor (the stuck state above): stop the
session first, then start cleanly, since it cannot be reattached.
Verified end to end on Ubuntu 24.04 / WSL2:
- from stopped: starts, "Android with user 0 is ready", session RUNNING
- second click while healthy: prints "Waydroid is already running;
re-showing the UI", weston pid 8528 before and 8528 after
- click while stuck: prints "Stopping a session left without a compositor
before restarting", tears down, comes back up on a fresh weston with
"Android with user 0 is ready"
…ndow
The installer mounted a tmpfs at /mnt/shared_memory via a wsl.conf boot
hook, believing it fixed WSLg's "[WARN:COPY MODE]" black-window bug. It
causes that bug. The tmpfs shadows the shared-memory transport WSLg uses to
hand buffers to the Windows RDP client, so WSLg falls back to copy mode and
renders every window solid black.
Diagnosis on a real install:
- The Weston window's title read "[WARN:COPY MODE] West...", and Android
inside it was black -- despite a fully healthy stack: binder OK, container
and session RUNNING, hwcomposer connected (wl_display_connect returned a
live handle, EGL init all EGL_SUCCESS), the /run/xdg/wayland-0 bind mount
inode-identical to the host socket, SurfaceFlinger booted, 77 visible
layers, launcher the top-resumed activity.
- Isolated it by running a plain weston-terminal directly against WSLg, with
no Waydroid or nested Weston involved. It was black and in copy mode too,
proving the fault was WSLg-wide rather than anything Waydroid does.
- Removing the boot hook and doing a full `wsl --shutdown` made that same
terminal render correctly, and Waydroid's Android UI then displayed.
The hook could never have worked as intended anyway: it loses the race it
was written to win. Measured across two cold starts, WSLg's compositor came
up ~60ms before the mount landed:
WSLg compositor: 19:15:45.178 shared_memory: 19:15:45.236
WSLg compositor: 20:34:05.405 shared_memory: 20:34:05.468
Changes:
- Do not write the boot hook, and actively remove it when a previous run of
this installer left one behind -- otherwise upgrading fixes nothing.
- README: correct the cause/fix table, which stated the relationship
backwards, and rewrite the troubleshooting entry to tell people to test a
plain GUI app first and to use `wsl --shutdown` rather than
`wsl -t <distro>` (terminating one distro leaves the WSLg system distro
running with the broken state, so the symptom persists and looks
unfixable).
- The Start Menu .desktop mitigation kept, but its stated rationale no
longer references a race that no longer exists; it now just keeps a dozen
Android app icons out of the Start Menu.
Verified: the new logic strips a seeded stale hook while preserving
`systemd = true` and the `[user]` section, and the script parses clean.
Android booted with an IP address but no gateway, so nothing inside it could
reach the network:
# ip route
192.168.240.0/24 dev eth0 proto kernel scope link src 192.168.240.112
Host-side NAT was already correct -- waydroid0 up at 192.168.240.1,
xt_MASQUERADE loaded, and the MASQUERADE rule present in the legacy tables.
The gap is entirely inside Android: netd never finishes bringing the link up
because its BPF and xt_quota setup fails against the WSL kernel. Both show up
in logcat:
E NetworkStats: Unable to swap active stats map:
Address family not supported by protocol (code 97)
E BandwidthController: Updating quota globalAlert failed
fopen("/proc/net/xt_quota/globalAlert", "we") failed
DHCP still hands out a lease, so this presents as working networking -- an IP,
a route to its own subnet, no gateway. Adding the route by hand restored
connectivity immediately (8.8.8.8 at 15ms, google.com at 69ms, DNS included),
confirming nothing else was wrong.
Adds waydroid-fix-network.sh, run as a third step from Start-Waydroid.bat
after the session is up. Notes:
- Runs as root: "waydroid shell" refuses to run as a normal user, so this
cannot live in waydroid-start-user.sh.
- Reads the gateway from the waydroid0 bridge instead of hardcoding
192.168.240.1; waydroid picks the subnet at init time.
- Retries for up to 60s, since Android may still be coming up, and exits 0
if it cannot help so it never blocks the launcher.
Verified end to end: the launcher prints "Added Android default route via
192.168.240.1" and google.com resolves and pings from inside Android.
The previous version added the default route with `ip route add`, which does
not survive and does not help apps:
- Android uses fwmark policy routing. App traffic resolves in netd's
per-network table (`eth0`), not `main`, so a route in `main` is the wrong
table -- `ip route show table eth0` stayed empty.
- netd reconciles `main` and deletes routes it does not own, so the route
disappeared within about a minute and internet died again.
Go through `ndc` instead. netd then owns the routes, they persist, and app
DNS starts working because the resolver is bound to the same network. Order
matters: netd rejects the gateway route with "addRoute() failed (Network is
unreachable)" until the connected subnet route exists in that table first.
The netd network id is read from Android's own routing rules
(fwmark 0x1<netid>/0x1ffff) rather than assuming the usual 100, and the
gateway and subnet come from the waydroid0 bridge rather than being
hardcoded.
Verified by tearing the routes back down and re-running the script:
eth0 table now: []
ping: connect: Network is unreachable
-> Android network configured: default via 192.168.240.1 on netd network 100.
default via 192.168.240.1 dev eth0 proto static
192.168.240.0/24 dev eth0 proto static scope link
google.com: 2 received, 0% packet loss, rtt 72.2ms
Apps recovered too -- Play's "Unable to resolve host play.googleapis.com"
errors stopped and Finsky/PlayCommon uploads began succeeding.
Known remaining gap: ConnectivityService still reports "Active default
network: none" because EthernetService never registers a NetworkAgent (the
framework's INetdEventListener never comes up, so EthernetTracker never
learns eth0 exists). Sockets and DNS work, so browsers are fine, but apps
that gate on ConnectivityManager -- Google Play among them -- still refuse.
Any app crash took down the whole Android framework. ActivityManager tries to
show its "app has stopped" dialog, adding that window fails in this compositor
setup, and the exception lands on system_server's android.ui thread:
E AndroidRuntime: *** FATAL EXCEPTION IN SYSTEM PROCESS: android.ui
java.lang.RuntimeException: Adding window failed
at android.view.ViewRootImpl.setView(ViewRootImpl.java:1316)
at android.app.Dialog.show(Dialog.java:352)
at com.android.server.am.ErrorDialogController...
system_server dies, the framework restarts, and the network goes with it. The
symptom is not "Android keeps restarting" -- it is intermittent internet:
things work for a minute, then stop. Browsers survive longest because they
hold open sockets, while Google Play re-queries ConnectivityManager and
reports no connection.
`settings put global hide_error_dialogs 1` removes the code path. The value
lives in /data and persists; re-applying it is harmless.
Renames waydroid-fix-network.sh to waydroid-post-boot.sh, since it now covers
two unrelated post-boot repairs, and keeps both idempotent so the launcher can
run it on every start.
Before: system_server restarting repeatedly, "Active default network: none".
After: same pid across 47 minutes, "Active default network: 100", zero
FATAL EXCEPTION IN SYSTEM PROCESS, and Google Play loads search results.
README also documents the image-type switch trap found alongside this:
`waydroid init -s GAPPS -f` swaps the system image but keeps /data, and the
mismatched package UIDs break com.android.networkstack, which blocks
EthernetService in awaitIpClientStart() and leaves ConnectivityService with no
default network.
The nested compositor was launched at a fixed 1280x800, so Android came up landscape and, at the image's stock 180 density, reported ~1138dp of width -- firmly in tablet-layout territory. Default to 720x1280 portrait instead. Width and height are now named variables at the top of waydroid-start-user.sh, and waydroid-post-boot.sh derives the display density from the actual width so Android lands near a phone's 360dp. Changing the geometry no longer means hand-tuning a density to match. Also rewrites waydroid-post-boot.sh, which had two bugs of my own making: - The density step ran before Android could answer, and the routing step exited early on success, so the density step was skipped entirely on any boot where routing was already fine. There is now one wait for Android up front and no early exits, so every step runs on every start. - The idempotency check read "Physical density", but "wm density" reports a manual setting as a separate "Override density" line and leaves the physical value alone -- so the check never matched and the script re-applied the density on every run. It now reads the override when one is present. Verified: after a reset, run 1 prints "Set display density to 320 so a 720px screen reports ~360dp (phone layout)" and run 2 is a silent no-op. Final state is 720x1248 (Weston keeps a few rows for its panel) at override density 320, which is exactly 360dp wide. Note Waydroid restarts the session when the compositor window is resized, so the size is fixed at launch rather than tracked live; that is called out in a comment next to the variables.
Records what actually went wrong on an end-to-end install, separated into bugs in this installer and behavior of Waydroid/WSL itself. Most of these fail silently or report an error that points somewhere unhelpful, so the symptom is written down alongside the cause. Installer bugs: the PowerShell array-literal concatenation that truncated the kernel config command; CRLF breaking every shell payload sent into WSL (both the heredoc terminator and the launcher shebangs); weston never being installed; the Start Menu mitigation running before the files it moves exist; and the launcher killing the compositor out from under a running session on a second click. Waydroid/WSL behavior: Android getting a DHCP lease but no gateway because netd's BPF/xt_quota setup fails on the WSL kernel and EthernetService blocks in awaitIpClientStart(), including why the route has to go through ndc rather than "ip route add"; and app-error dialogs killing system_server, which presents as intermittent internet rather than as a crash loop. New troubleshooting entries for ARM-only apps on an x86_64 image, the ~24h Play certification cache that a checkin does not refresh (and why clearing com.google.android.gsf is the wrong way to force it), session restarts on window resize, and the persist.waydroid.width/height properties that crash-loop the hwcomposer. Also adds a Display size section covering WIDTH/HEIGHT and the derived density, a Known limitations section (single API level, x86_64 only, software rendering, fixed window size, "wsl -t" not restarting WSLg), and refreshes Package contents, which had drifted -- waydroid-post-boot.sh and .gitattributes were missing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes a PowerShell parsing bug that made
Install-Waydroid.ps1fail at the "Configuring kernel" step with:Why
The kernel config command was built by concatenating string fragments inside an
@(...)array literal:In PowerShell a newline inside an array literal separates elements, so a trailing
+does not continue the expression. That array parses to 12 elements, not 6 — verified:So
bash -creceived only the first fragment — a script ending in a dangling&&— hence the syntax error. The remaining fragments were passed as$0,$1, …, which is why the error message quotes./scripts/config --set-val CONFIG_ANDROID_BINDER_IPC yas the shell name.A useful tell in the failing output: the echoed command line has doubled spaces at each fragment boundary, because
Write-Inforenders$WslArgs -join ' 'and each fragment already ended in a space.How
Build the command in a
$configCmdvariable using backtick line continuations (which do continue across newlines), then pass it as a single argument. Added a comment explaining the trap so it doesn't get reintroduced.Verification
[System.Management.Automation.Language.Parser]::ParseFileon the edited script: no errors.wsl-kernelclone (Ubuntu WSL2 distro): exit 0,make olddefconfigcompleted, and.configcontains:Notes for the reviewer
Install-Waydroid.ps1andUninstall-Waydroid.ps1for the same pattern (a line ending in+inside an array literal). This was the only occurrence.