Skip to content

Instantly share code, notes, and snippets.

@andyneff
Last active January 17, 2023 21:15
Show Gist options
  • Save andyneff/712b2b02e1757f23526776a997856c29 to your computer and use it in GitHub Desktop.
Save andyneff/712b2b02e1757f23526776a997856c29 to your computer and use it in GitHub Desktop.
Modified Cygwin startxwin script that syncs X11 with WSL

Using cygwin X in WSL1 and MingW (and others)

Cygwin comes with an X Server that can be useful with tools other than Cygwin itself. With a few modifications, we can add cygwin's X Server support to other shell implementations including cygwin, mingw64 (such as git for windows), msys2, WSL1, WSL2, and others.

With a few minor modifications, we can save the DISPLAY variable to files accessible by any shell. Once the DISPLAY value is accessible, an rc file can load it and it will automatically be used.

Optionally, we can even add a "one click" link to the start menu to start the xserver

TL;DR

Open Command Prompt

mkdir C:\cygwin64
powershell -NoProfile -Command "Invoke-WebRequest -Uri "https://cygwin.com/setup-x86_64.exe" -OutFile "c:\cygwin64\setup-x86_64.exe"
cd \cygwin64
setup-x86_64.exe -l C:\cygwin64\packages -qnNd -P xorg-server -P xinit -P libiconv
.\bin\bash.exe -l

Now that you are in cygwin bash

cd /
curl -LO https://gist.githubusercontent.com/andyneff/712b2b02e1757f23526776a997856c29/raw/startx.bat
curl -LO https://gist.github.com/andyneff/712b2b02e1757f23526776a997856c29/raw/startxwin

And Add this to your WSL .bashrc

if [ -f ~/.displayrc ]; then
  source ~/.displayrc
  # This prevents an annoying Terminal hang every time an ssh client tried to access a dead X session
  if ! timeout 1 xhost &> /dev/null; then
    unset DISPLAY
    rm ~/.displayrc
  fi
fi

Finally run: C:\cygwin64\startx.bat

Getting started

  1. Download cygwin installer and place it in C:\cygwin64\setup-x86_64.exe

  2. Open up a command prompt to C:\cygwin64 and run: setup-x86_64.exe -l C:\cygwin64\packages -qnNd -P xorg-server -P xinit -P libiconv (all you need to do is click Next and Pick the mirrors. The required packages will already be selected)

  3. Download these two files (startx.bat and startxwin and put them in C:\cywin64\. (If you put them somewhere else, you will need to update the first two %~dp0s in startx.bat to point to C:\cywin64\)

    • Recommended way:
    • Start Cygwin
    cd /
    curl -LO https://gist.githubusercontent.com/andyneff/712b2b02e1757f23526776a997856c29/raw/startx.bat
    curl -LO https://gist.github.com/andyneff/712b2b02e1757f23526776a997856c29/raw/startxwin
    
  4. Edit your .bashrc (Your WSL bashrc, for example) to include the following (other other rc files if you don't use bash)

    if [ -f ~/.displayrc ]; then
      source ~/.displayrc
    fi
  5. (Optional) There are other ways to do this, but here is one way to add a shortcut to your start menu

    • Create a shortcut to startx.bat (anywhere, your Desktop for example). Name it and give it the icon you want
    • Move the shortcut to one of the two locations:
      • C:\ProgramData\Microsoft\Windows\Start Menu (for all users, need admin access)
      • C:\%USERPROFILE%\AppData\Roaming\Microsoft\Windows\Start Menu (for just you)
    • Now the shortcut will show up on your Start Menu under Recently Added
      • Sometimes you need to type in the name to search for it. If you named the shortcut "StartX", type that in to find it
      • Widows Search
    • Right click the icon, and select "Pin to Start"
  6. Run the startx.bat file for the first time. When prompted by Windows Firewall (or your firewall) Allow xwin.exe to communicate on the appropriate networks (Domain and Private, for example. Public networks is strongly discouraged, for privacy and security concerns)

  7. Now you should see two X Icons in your system tray

FAQ

  1. What about WSL2's X Server?
    • I suspect if the WSL2 server leave nothing lacking, this will all become obsolete. Lets hope that happens!
  2. Does this work on WSL2?
    • It should...
    • localhost from WSL 2 does not work: microsoft/WSL#4619
    • IP for vEthernet (WSL) is used instead of localhost
      • If there are multiple Network adapters with WSL in the name, this could cause error.
  3. What about WSL2 with VPN?
    • There are many things that can go wrong with WSL2 on VPN, and many of them will need to be solved manually. The most extreme would be to use VPN Kit to add internet to WSL2
@echo off
setlocal enableextensions
set TERM=
"%~dp0bin\cygstart" --hide "%~dp0bin\bash" --login "%~dp0startxwin"
#!/bin/sh
# This is just a sample implementation of a slightly less primitive
# interface than xinit. It looks for user .xinitrc and .xserverrc
# files, then system xinitrc and xserverrc files, else lets xinit choose
# its default. The system xinitrc should probably do things like check
# for .Xresources files and merge them in, start up a window manager,
# and pop a clock and several xterms.
#
# Site administrators are STRONGLY urged to write nicer versions.
#
unset DBUS_SESSION_BUS_ADDRESS
unset SESSION_MANAGER
userclientrc=$HOME/.startxwinrc
sysclientrc=/etc/X11/xinit/startxwinrc
userserverrc=$HOME/.xserverrc
sysserverrc=/etc/X11/xinit/xserverrc
defaultclient=xterm
defaultserver=/usr/bin/XWin
defaultclientargs=""
defaultserverargs="-listen tcp"
defaultdisplay=":0"
clientargs=""
serverargs=""
vtarg=""
display=""
enable_xauth=1
# Automatically determine an unused $DISPLAY
d=0
while true ; do
[ -e "/tmp/.X$d-lock" -o -S "/tmp/.X11-unix/X$d" ] || break
kill -0 `cat /tmp/.X$d-lock` 2>/dev/null || break
d=$(($d + 1))
done
defaultdisplay=":$d"
unset d
whoseargs="client"
while [ x"$1" != x ]; do
case "$1" in
# '' required to prevent cpp from treating "/*" as a C comment.
/''*|\./''*)
if [ "$whoseargs" = "client" ]; then
if [ x"$client" = x ] && [ x"$clientargs" = x ]; then
client="$1"
else
clientargs="$clientargs $1"
fi
else
if [ x"$server" = x ] && [ x"$serverargs" = x ]; then
server="$1"
else
serverargs="$serverargs $1"
fi
fi
;;
--)
whoseargs="server"
;;
*)
if [ "$whoseargs" = "client" ]; then
clientargs="$clientargs $1"
else
# display must be the FIRST server argument
if [ x"$serverargs" = x ] && \
expr "$1" : ':[0-9][0-9]*$' > /dev/null 2>&1; then
display="$1"
else
serverargs="$serverargs $1"
fi
fi
;;
esac
shift
done
# process client arguments
if [ x"$client" = x ]; then
client=$defaultclient
# For compatibility reasons, only use startxwinrc if there were no client command line arguments
if [ x"$clientargs" = x ]; then
if [ -f "$userclientrc" ] && [ ! -x "$userclientrc" ]; then
echo "Skipping $userclientrc; present but not executable"
fi
if [ -x "$userclientrc" ]; then
client=$userclientrc
elif [ -f "$sysclientrc" ]; then
client=$sysclientrc
fi
fi
fi
# if no client arguments, use defaults
if [ x"$clientargs" = x ]; then
clientargs=$defaultclientargs
fi
# process server arguments
if [ x"$server" = x ]; then
server=$defaultserver
# For compatibility reasons, only use xserverrc if there were no server command line arguments
if [ x"$serverargs" = x -a x"$display" = x ]; then
if [ -f "$userserverrc" ]; then
server=$userserverrc
elif [ -f "$sysserverrc" ]; then
server=$sysserverrc
fi
fi
fi
# if no server arguments, use defaults
if [ x"$serverargs" = x ]; then
serverargs=$defaultserverargs
fi
serverargs="-multiwindow $serverargs"
# if no vt is specified add vtarg (which may be empty)
have_vtarg="no"
for i in $serverargs; do
if expr match "$i" '^vt[0-9]\+$' > /dev/null; then
have_vtarg="yes"
fi
done
if [ "$have_vtarg" = "no" ]; then
serverargs="$serverargs $vtarg"
fi
# if no display, use default
if [ x"$display" = x ]; then
display=$defaultdisplay
fi
# The wsl CLI API changed between V1 and V2 when I used the insider edition.
# This has nothing to do with if the Linux is running in V1 or V2, only if the
# cli installed supports only V1 or V2 too, so I know which args to use.
if wsl -l -q > /dev/null 2>&1; then
WSL_VERSION=2
else
WSL_VERSION=1
fi
if [ x"$enable_xauth" = x1 ] ; then
if [ x"$XAUTHORITY" = x ]; then
XAUTHORITY=$HOME/.Xauthority
export XAUTHORITY
fi
removelist=
# set up default Xauth info for this machine
case `uname` in
Linux*)
if [ -z "`hostname --version 2>&1 | grep GNU`" ]; then
hostname=`hostname -f`
else
hostname=`hostname`
fi
;;
*)
hostname=`hostname`
;;
esac
authdisplay=${display:-:0}
mcookie=`/usr/bin/mcookie`
if test x"$mcookie" = x; then
echo "Couldn't create cookie"
exit 1
fi
dummy=0
# create a file with auth information for the server. ':0' is a dummy.
xserverauthfile=$HOME/.serverauth.$$
trap "rm -f '$xserverauthfile'" HUP INT QUIT ILL TRAP KILL BUS TERM
touch "$xserverauthfile"
xauth -q -f "$xserverauthfile" add ":${dummy}" . "${mcookie}"
xserverauthfilequoted=$(echo ${xserverauthfile} | sed "s/'/'\\\\''/g")
serverargs=${serverargs}" -auth '"${xserverauthfilequoted}"'"
display_names=("${authdisplay}" "${hostname}${authdisplay}")
OLD_IFS="${IFS}"
IFS=$'\n' # Incase spaces are allowed, YOU KNOW WINDOWS!
case "${WSL_VERSION}" in
1)
wsls=($(wsl -l | (iconv -f UCS-2LE -t ASCII//TRANSLIT || tr -d '\0') | sed -e '1d' -e 's/ (Default)//' | tr -d '\r'))
;;
2)
wsls=($(wsl -l -q | (iconv -f UCS-2LE -t ASCII//TRANSLIT || tr -d '\0') | tr -d '\r'))
# WSL 2 can't use localhost, this netstat command works on normal and Sonicwall Mobile conntect
# Other VPNs may not work as expected.
# Works on WSL2 using Sonicwall Netextender when using VPN Kit
# wsl2_accessible_ip="$(netstat -rvn | grep '^ *0\.0\.0\.0' | awk 'NR==1{print $4}')"
wsl2_accessible_ip="$(ipconfig | sed -En '/vEthernet \(WSL\)/,/Default Gateway/s/.*IPv4 Address( *\.)* *: *//p' | tr -d '\r\n')"
# This will not work right if multiple ethernet devices say WSL
# You may want to use this line on your particualr computer setup
# wsl2_accessible_ip="$(powershell "(Get-NetIPAddress -AddressFamily IPV4 -InterfaceAlias *WSL*).IPAddress" | tr -d '\r\n')"
if [ -n "${wsl2_accessible_ip}" ]; then
display_names+=("${wsl2_accessible_ip}${authdisplay}")
fi
;;
*)
echo "Unknown version of WSL detected"
exit 123
;;
esac
IFS="${OLD_IFS}"
unset OLD_IFS
# now add the same credentials to the client authority file
# if '$displayname' already exists do not overwrite it as another
# server man need it. Add them to the '$xserverauthfile' instead.
for displayname in "${display_names[@]}"; do
authcookie=`xauth list "${displayname}" | \
sed -n "s/.*${displayname}[[:space:]*].*[[:space:]*]//p"` 2>/dev/null;
if [ "z${authcookie}" = "z" ] ; then
xauth -q add "${displayname}" . "${mcookie}"
for wsl in "${wsls[@]}"; do
wsl -d "${wsl}" sh -c "
if command -v xauth > /dev/null 2>&1; then
if [ -n \"\${WSL_INTEROP-}\" ]; then
xauth -q add '${wsl2_accessible_ip}${display}' . '${mcookie}'
fi
xauth -q add '${displayname}' . '${mcookie}'
fi"
done
removelist="${display_names[*]}"
else
dummy=$(($dummy+1));
xauth -q -f "$xserverauthfile" add ":${dummy}" . "${authcookie}"
for wsl in "${wsls[@]}"; do
wsl -d "${wsl}" sh -c "
if command -v xauth > /dev/null 2>&1; then
if [ -n \"\${WSL_INTEROP-}\" ]; then
xauth -q -f '${xserverauthfile}' add '${wsl2_accessible_ip}${dummy}' . '${mcookie}'
fi
xauth -q -f '${xserverauthfile}' add ':${dummy}' . '${authcookie}'
fi"
done
fi
done
fi
# Copy to WSLs' home
for wsl in "${wsls[@]}"; do
wsl -d "${wsl}" sh -c '
# If running WSL2
if [ -n "${WSL_INTEROP-}" ]; then'"
echo 'export DISPLAY=${wsl2_accessible_ip}${display}' > ~/.displayrc
else
echo 'export DISPLAY=${display}' > ~/.displayrc
fi
"
done
# Copy to cygwin's home
echo "export DISPLAY=${display}" > ~/.displayrc
# Copy to Windows home, used by mingw et al.
echo "export DISPLAY=${display}" > "${USERPROFILE}/.displayrc"
echo "Starting xinit..."
eval xinit \"$client\" $clientargs -- \"$server\" $display $serverargs
retval=$?
echo "Cleaning up xinit..."
if [ x"$enable_xauth" = x1 ] ; then
if [ x"$removelist" != x ]; then
xauth remove $removelist
for wsl in "${wsls[@]}"; do
wsl -d "${wsl}" sh -c "if command -v xauth > /dev/null 2>&1; then xauth remove $removelist; fi"
done
fi
if [ x"$xserverauthfile" != x ]; then
rm -f "$xserverauthfile"
for wsl in "${wsls[@]}"; do
wsl -d "${wsl}" rm -f "$xserverauthfile"
done
fi
fi
exit $retval
@andyneff
Copy link
Author

andyneff commented Jan 6, 2022

Removed localhost from DISPLAY for both WSL1 and local to windows host (was not needed). WSL 1 was no longer working via localhost (2022). So maybe MS changes how the localhost was shared in WSL 1

@ajkessel
Copy link

ajkessel commented Jan 8, 2022

Thanks for this! I'm having trouble getting it to work with WSL2. The first problem is wsl.exe wasn't in the cygwin path and for some reason, cygwin shell can't see c:\windows\system32\wsl.exe but only the "real" executable which is in C:\Windows\WinSxS\amd64_microsoft-windows-lxss-wsl_31bf3856ad364e35_10.0.19041.1320_none_f7a60f7f841c91dd. So I added that to my path for now.

The script launches X fine and I can see it is properly detected the IP address of the WSL instance and adding it to the authorized list. But when I source .displayrc from WSL and try to execute an X application, it is stuck for several minutes and then reports "Error: Can't open display: 172.26.144.1:0". Something is going on because if I change DISPLAY to some random bad IP address, it fails immediately. But when I have it set to the correct address, it just pauses for several minutes and then gives this error.

Could this possibly be an ipv6 issue? When I run xauth list after launching your script, I see several entries that appear to be ipv6 addresses and do not actually see the IPv4 address of my WSL2 instance. I tried manually adding the IPv6 address via xauth add but it didn't fix the problem.

Any tips for troubleshooting?

@andyneff
Copy link
Author

  1. When you run xauth list, you want to run xauth -n list just to disable the "smart" mechanism that would try to resolve the name of each IP Address
  2. There are a few IP address that will work, the one that usually starts in 172 always works "for me", and logically, should always work. Try manually setting to these correct IP addresses, and see if any of them work for you. For example, when I'm off of VPN, I can use my main IP address and it works (when I'm on VPN, I can use the VPN IP and it works)
  3. Are you using Antivirus other than Windows Defender? Antivirus can block the port we need (TCP port 6000 for :0, 6001 for :1, etc...). If using Windows Defender, you would have had to say yes to allowing the port when it first popped up. If you don't remember what you said to that, you will have to dig through the firewall settings to update it.
  4. Are you on VPN? VPN can potentially add all sorts of issues. We have solutions if this is your case.

@ajkessel
Copy link

Thanks! Turns out there was a Windows Defender rule blocking the connection. I don't recall ever setting it but it's possible I answered "no" long ago. Deleting that rule allowed your solution to work, with the one tweak discussed above about locating the wsl.exe executable.

@andyneff
Copy link
Author

Windows 11 can finally stop using this script: https://docs.nvidia.com/cuda/wsl-user-guide/index.html#wsl2-system-requirements
Windows 10 still needs insider program, which isn't for everyone :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment