microsoft/WSL

How to tell in Linux under WSL if using WSL1 vs WSL2 ?

k7aay opened this issue · 37 comments

k7aay commented

Documentation missing instructions in how to tell within Linux under WSL whether using WSL1 or WSL2.

Here is a helpful doc - https://docs.microsoft.com/en-us/windows/wsl/wsl2-install

while setting a distro to be backed by WSL 2, I would run the following command in Powershell:

wsl -s 2

You can find your Distro by running: wsl -l

You can find your Distro by running: wsl -l

Yes, but there is no particularly good way right now to know your own distribution name once you are inside. There was another ask for that recently #4479.

Probably (possibly?) one way is to run a regex on /proc/version. So WSL1 looks like:

Linux version 4.4.0-18985-Microsoft (Microsoft@Microsoft.com) (gcc version 5.4.0 (GCC) ) #1-Microsoft Fri Sep 13 14:26:00 PST 2019

While WSL2:

Linux version 4.19.67-microsoft-standard (oe-user@oe-host) (gcc version 8.2.0 (GCC)) #1 SMP Sun Aug 18 13:37:54 UTC 2019

If it were me I wouldn't make a hard bet on the WSL2 string being set in concrete, but conversely, I can't imagine the WSL1 format changing gratuitously (not without grumbling, anyway). Matching that "gcc version n.x.x" and testing for n < 8 probably pretty future proof. There are other heuristics that would work. Calling dmesg and looking for a stable string like "Hypervisor" (or pick another).

Here is a sample C program on how /init takes care of WSL1 vs WSL2:

#include <stdio.h>
#include <string.h>
#include <sys/utsname.h>

int main(void)
{
    struct utsname buf;
    memset(&buf, 0, sizeof buf);

    int ret = uname(&buf);
    if (ret == 0)
    {
        if (strstr(buf.release, "Microsoft"))
            printf("WSL1\n");
        else
            printf("WSL2\n");
    }
    else
        printf("uname error\n");

}

+1 for code always. But implied in my post was maybe better not to rely on oe-user@oe-host never ever changing. That strstr() is leaning pretty hard on a capital letter 'M'. [The string "icrosoft" matches both.]

There may be hundreds of ad-hoc way to do it. For example, only WSL2 has $WSL_INTEROP, /proc/config.gz, /dev/vsock, /dev/sda, tty shows /dev/pts etc.

Money ➡️ mouth:

// g++ -std=c++17 wslver.cpp -o wslver
#include <fstream>
#include <iostream>
#include <array>
#include <string>
#include <charconv>
#include <regex>
using namespace std::string_literals;

int main(int argc, const char *argv[])
{
    std::ifstream ifs;
    ifs.open("/proc/version");
    constexpr size_t bufsz = 128;
    std::array<char, bufsz> buf;
    ifs.getline(buf.data(), bufsz, '\n');
    std::string proc_ver(buf.data(), buf.size());
    std::regex rx("Linux version [0-9]+\\.[0-9]+\\.[0-9]+.*[Mm]icrosoft.*gcc version ([0-9]+)\\.[0-9]+\\.[0-9]+");
    std::smatch match;
    auto gcc_major = (std::regex_search(proc_ver, match, rx) && match.size() == 2) ? match[1] : "5"s;
    int maj = 0;
    std::from_chars(gcc_major.data(), gcc_major.data() + gcc_major.size(), maj);
    std::cout << ((maj < 8) ? "1\n" : "2\n");
    return 0;
}

detect wsl1:)
uname -r | grep Microsoft > /dev/null && echo "WSL1"

[ $(grep -oE 'gcc version ([0-9]+)' /proc/version | awk '{print $3}') -gt 5 ] && \
    echo "WSL2" || echo "WSL1"

Update (Windows 10 version 2004):

I'm brand new to WSL2, however...

  • uname -r says microsoft-standard in it, so all checks for the word [mM]icrosoft would fail
  • (opinion) The gcc method seems arbitrary, assuming WSL1 never changes to a new gcc
  • (opinion) WSL_INTEROP still works, and seems like the best test for now, but that's just my opinion

WSL2 has the directory /run/WSL/ and not WSL1.

I think that perhaps a belt & braces approach is needed. We have don't have any control over WSL1 getting a similar folder added. Likewise, whether 'Microsoft' has upper or lower case.

Using [[ -d "/run/WSL" ]] && echo "WSL2 present" is nice and easy, I acknowledge. But IMO a better method is to test for the underlying hyper-v running system that WSL2 uses.

Try lshw | grep smp for instance. WSL1 shows 'smp' while WSL2 will show both smp & vsyscall32.

We can write something like WSL2_Flag="$(lshw | grep vsyscall32) ; if [[ $WSL2_Flag ]] ; then echo "WSL2 present" ; fi

Or alternatively, we can look in '/proc'. We can't guarantee 'lshw' to be present in every Linux distro, although it usually is:

In WSL1 cat /proc/interrupts is empty whereas with WSL2 it is positively active, especially the line for HVS Hyper -V !

eg. WSL2_Flag="$(cat /proc/interrupts ) ; if [[ $WSL2_Flag ]] ; then echo "WSL2 present" ; fi

What about the environment variable WSL_DISTRO_NAME? [[ -n "$WSL_DISTRO_NAME" ]]. Is that a suitable & cheap way to detect WSL 1 or 2?

No. WSL_DISTRO_NAME variable is set by /init binary blob for both WSL1 and WSL2.

@Biswa96 I asked if it's suitable for WSL1 OR WSL 2, not WSL 2 vs WSL 1 so it sounds like the answer to my question is actually yes?

Yes but not retroactively.

@therealkenc is the way you described (distinguish between WSL 1 and WSL 2 using the GCC version) going to be guaranteed stable, even in future versions of WSL 1?

Which is the guaranteed way to distinguish between these two and nōn-WSL Linux then? The presence of the string icrosoft?

Someone suggested checking the root filesystem type, which for WSL 1 is wslfs, but that won’t help for WSL 2. Any other filesystem can be umounted… and some people have trouble with custom kernels and the detection mechanism you provided (especially in the “WSL vs Linux” case).

Anything that needs superuser privilegues (lshw…) is also out, for hopefully obvious reasons.

No guarantees in life. Although given that the unspoken textual API has now irretrievably leaked into userspace, yes, I would treat regex [Mm] difference as pretty future proof.

That said, users can (and do) compile their own kernels in WSL2 and then misname the their kernel, or compile with clang, so you can't count on the kernel string in WSL2 to be anything in particular. Fortunately (notwithstanding the env pollution) recent WSL has the WSL_DISTRO_NAME and WSL_INTEROP environment variables. WSL_INTEROP is only present in WSL2. Using them is not backwards compatible with older WSL though, and they won't EOL for years.

Possible approach:

  • Test for existence WSL_DISTRO_NAME. If it exists, test for WSL_INTEROP. Yes, WSL2; no, WSL1.
  • If WSL_DISTRO_NAME does not exist, fall back on the version string regex.

This might do. I was thinking of simplifying this a bit:

  • if / is wslfs, then it’s WSL 1, else it’s either Linux/Android or WSL 2
  • if WSL_INTEROP or even WSL_DISTRO_NAME is set or the version contains icrosoft it’s WSL 2, else Linux/Android

Doing this without /proc (which is not guaranteed to be mounted) is more tricky. After statfs("/", &buf), buf.f_type is 0x53464846 on my test setup. Is this true for all WSL 1 systems?

The WSL 2 detection is even more fragile. Addition of an API from the start would have helped :/

For version detection, uname(2) exists, so this can also be done without procfs. The WSL 2 caveats apply :/

Use uname(2) if you can't depend on /proc. The gcc test was belt+suspenders overkill, and given existence possibility clang probably does more harm than good. strstr(buf.release, "Microsoft") means WSL1. Else Linux/Android/WSL2. For your purposes WSL2 is Linux anyway.

WSL 2 probably is “don’t use” anyway

WSL2 should be perfectly fine. WSL1 looks like it might be "don't use anyway". Either that's a hard fail in your use-case, or it isn't a hard fail. If it isn't a hard fail, there is no reason for it to be a hard fail on Linux or Android either. Just let that function always succeed, and lie about success when it doesn't. If it is a hard fail, you don't need to test for WSL1. Let it fail.

The better approach than querying the OS is to test capabilities. Here, the runtime capability test is whether setsockopt(...IPV6_TCLASS...) works, not whether you are "on WSL1" or "on Linux". Maybe one day in some mythical future IPV6_TCLASS starts working on win32 (and by extension WSL1). I bring it up not for your specific use-case, but because near all of the "if(WSL)" scattered on the Interwebs are testing the wrong thing.

Perhaps an identifier can be added in a next version?

That's existence environment WSL_DISTRO_NAME for WSL plus && WSL_INTEROP to differentiate WSL1 v. WSL2. But you can't use it retroactively, so the uname(2) or /proc fallback is necessary regardless.

Hmm, the statfs on / will fail in chroots, too. I’m going with just uname(2) for WSL 1:

#include <sys/utsname.h>   
#include <stdlib.h>
#include <string.h>

static int
isWSL(void)
{
        int e = errno;
        struct utsname u;
        int uerr;

        /* check uname first (this is improbable to fail) */
        if ((uerr = uname(&u)) == 0) {
                if (strstr(u.release, "Microsoft") != NULL) {
                        /* pretty certainly WSL 1 */
                        errno = e;
                        return (1);
                }

If uname(2) can detect WSL 2 (no custom kernel), let’s use that:

                if (strstr(u.release, "microsoft") != NULL) {
                        /* probably WSL 2 */
                        errno = e;
                        return (2);
                }
        }

Otherwise, go on with the more fragile (but still working in an empty chroot, i.e. no /proc or /run or statfs()ing /) checks:

        /* check presence of environment variables next */
        /* hoping the user did not change them */

        if (getenv("WSL_INTEROP") != NULL) {
                /* relatively certainly WSL 2 */
                errno = e;
                return (2);
        }
        if (getenv("WSL_DISTRO_NAME") != NULL) {

We’ve got WSL here, but WSL 2 ought to have been caught in the check for WSL_INTEROP above. Nevertheless, the uname(2) check above bears more weight, so assume WSL 1 only if that one failed, WSL 2 with a custom kernel and that someone unset WSL_INTEROP if it passed:

                /* relatively certainly WSL under a WSL-1/2-capable NT */
                /* since we detect WSL 1 above, this is probably 2 */
                /* assume WSL 1 when uname(2) failed, though */
                errno = e;
                return (uerr ? 1 : 2);
        }

At this point, just assume nŌn-WSL (especally since we pretty much got WSL 1 separated out, and WSL 2 is mostly Linux anyway):

        /* other checks are even more fragile, so let’s go with not WSL */
        errno = e;
        return (0);
}
 
#ifdef WSLCHECK_MAIN
int
main(void)
{
        return (isWSL());
}
#endif

Given how WSL 2 not only has regressions in cross-subsystem file copying but also a completely inferiour network setup, let’s hope WSL 1 will continue to stay an option for a long time…

Given that this issue has a whole pile of hacks, instead of trying to choose a hack that makes me feel least uncomfortable, I went with testing several of them, and show a warning if it's getting contradicting answers.

I have this in my .zshenv, but given that I have this test, I can use one of these ways in, for example, random bash scripts --- knowing that I'll quickly learn when assumptions need to be adjusted...

export WSL=no WSLVER=""
if [[ "$(< /proc/version)" = *[Mm]icrosoft* ]]; then
  WSL=yes
  if [[ -e "/proc/config.gz" ]]; then WSLVER+="2"; else WSLVER+="1"; fi
  if [[ -e "/dev/vsock" ]];      then WSLVER+="2"; else WSLVER+="1"; fi
  if [[ -n "$WSL_INTEROP" ]];    then WSLVER+="2"; else WSLVER+="1"; fi
  if [[ -d "/run/WSL" ]];        then WSLVER+="2"; else WSLVER+="1"; fi
  if [[ -n "${WSLVER//1/}" && -n "${WSLVER//2/}" ]]; then
    echo "WSL version detection got multiple answers ($WSLVER), time to update this code!"
  fi
  WSLVER="${WSLVER:0:1}"
fi

(All of this would be unneeded if there was some proper way to distiguish the two... Why not add it simlarly to $WSL_DISTRO_NAME?)

if [[ "$(< /proc/version)" = [Mm]icrosoft ]]; then
This will not catch for WSL 2 with custom kernels.

Yeah, that just happens to work for my needs (what I was talking about is the stuff inside that, I just left the condition for context.)

You’re talking about $WSL_INTEROP then?

Yes, that's another way to currently distinguish them (and turns out that it's better than tty so I replaced it above).

The problem that I'm talking about is that all of these, including $WSL_INTEROP, are hacks (what happens when WSL1 gets that variable too?).

tl;dr: the uppercase/lowercase difference in uname is very likely to stay, and WSL 1 probably won’t get $WSL_INTEROP.

Are you saying this as someone who is working on WSL? Then make it a documented feature that can be relied on, and drop the "likely" and "probably" from that sentence.

(And if you're not, then these are your assumptions, and I prefer minimizing them.)

t-ru commented

Detecting WSL (bash code). Tested with

  • WSL 1
  • WSL 2 (standard kernel AND custom kernel)

The detection should be future safe. Code is from a new (currently not released) native systemd initialization for WSL

https://github.com/t-ru/wsl-misc/blob/master/detect-wsl-version/detect-wsl-version-v1.sh

t-ru commented

From a shell developer PoV this is horridly inefficient.

Maybe. But it's working.

Is your C-Code working under all conditions?
No.

Is your C-Code posted above inefficient?
Yepp.
Checking uname is stupid.
Checking WSL_INTEROP before WSL_DISTRO_NAME is also stupid.

You call mount twice, you can use grep -q instead of -c

many roads lead to rome

a temporary file is also not necessary,

The temporary file is required. The reason is that commands (grep, awk, head, ...) are not work on wsl.exe.
The way with temp file is always working.

I needed a C solution in the project that caused me to ask anyway.

Feel free and write your one holy grail solution.

t-ru commented

Why even use grep? Shows how little you understand the Unix toolkit and shell. (But then, you mentioned systemd, so this is to be expected.)

I know the tools very well. But I write the code on a system that has only a minimal set of packages installed. Writing tiny code on systems that have all the tools installed is easy. Another aspect is that WSL is from Microsoft. There, the clocks tick differently than usual. By the way, the code was written in less than 30 minutes.

Systemd is the devil and because it is so bad, all major Linux distributions have switched to Systemd. The problem with Systemd is not Systemd, but the users who don't understand it. It is the same as with Wayland.

I’m almost tempted to make a more efficient shell implementation of the same algorithm, but your attitude is… off-putting, at least.

The most important thing in an implementation is that it works. Optimization is phase 2. I published the code because there was no solution for the problem "detection of WSL" that works reliably so far. Feel free to adapt the code and write your own implementation.

t-ru commented

Bring butter to the fishes. Post better code. I will gladly take it over.

I found code from you

/* check uname first (this is improbable to fail) */
	if ((uerr = uname(&u)) == 0) {
		if (strstr(u.release, "Microsoft") != NULL) {
			/* pretty certainly WSL 1 */
			errno = e;
			return (1);
		}
		if (strstr(u.release, "microsoft") != NULL) {
			/* probably WSL 2 */
			errno = e;
			return (2);
		}
	}


if (getenv("WSL_INTEROP") != NULL) {
		/* relatively certainly WSL 2 */
		errno = e;
		return (2);
	}
	if (getenv("WSL_DISTRO_NAME") != NULL) {
		/* relatively certainly WSL under a WSL-1/2-capable NT */
		/* since we detect WSL 1 above, this is probably 2 */
		/* assume WSL 1 when uname(2) failed, though */
		errno = e;
		return (uerr ? 1 : 2);
	}

Top code... What is with WSL and custom kernels? Many, many "if" statements. Is this good code?
No. That's "divine code"

Write better code and post it. I'm waiting!!!