Skip to content

Commit be64bea

Browse files
fix(setup): parse WSL version independent of locale
Parse the WSL package version from the stable WSL product-token line instead of matching localized label text. Add regression coverage for the localized MessagePackageVersions labels currently published by microsoft/WSL, plus NUL-stripped UTF-16 output and adjacent component-version guard cases. Addresses #661, but the issue should remain open until this is included in a release and verified there. Runtime verification on Chinese-localized Windows: #661 (comment) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent cd283cd commit be64bea

2 files changed

Lines changed: 115 additions & 10 deletions

File tree

src/OpenClaw.SetupEngine/SetupSteps.cs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ public static string SafeWindowsWorkingDirectory
3737
internal static class WslInstallSupport
3838
{
3939
private static readonly Version s_minDirectNamedInstallVersion = new(2, 4, 4);
40+
private static readonly System.Text.RegularExpressions.Regex s_wslProductTokenRegex = new(
41+
@"(?<![A-Za-z0-9])WSL(?![A-Za-z0-9])",
42+
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.CultureInvariant);
43+
private static readonly System.Text.RegularExpressions.Regex s_semanticVersionRegex = new(
44+
@"(?<![\d.])(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?(?![\d.])",
45+
System.Text.RegularExpressions.RegexOptions.CultureInvariant);
4046
public const string UpdateUrl = "https://aka.ms/wslstorepage";
4147

4248
public static string UpdateInstructions
@@ -54,27 +60,38 @@ public static bool ContainsDistro(string output, string distroName)
5460

5561
public static bool TryParseWslVersion(string output, out Version version)
5662
{
57-
var match = System.Text.RegularExpressions.Regex.Match(
58-
Normalize(output),
59-
@"WSL\s+version:\s*(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?",
60-
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
61-
62-
if (!match.Success)
63+
// Match the product token and version shape instead of localized label text.
64+
// WSL is the stable product acronym; labels around it vary by Windows language
65+
// and by UTF-16LE/NUL-stripped output shape.
66+
foreach (var rawLine in Normalize(output).Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
6367
{
64-
version = new Version();
65-
return false;
68+
var line = rawLine.Trim();
69+
if (!s_wslProductTokenRegex.IsMatch(line))
70+
continue;
71+
72+
var match = s_semanticVersionRegex.Match(line);
73+
if (!match.Success)
74+
continue;
75+
76+
version = ParseVersionMatch(match);
77+
return true;
6678
}
6779

80+
version = new Version();
81+
return false;
82+
}
83+
84+
private static Version ParseVersionMatch(System.Text.RegularExpressions.Match match)
85+
{
6886
var major = int.Parse(match.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture);
6987
var minor = int.Parse(match.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture);
7088
var build = int.Parse(match.Groups[3].Value, System.Globalization.CultureInfo.InvariantCulture);
7189
var revision = match.Groups[4].Success
7290
? int.Parse(match.Groups[4].Value, System.Globalization.CultureInfo.InvariantCulture)
7391
: -1;
74-
version = revision >= 0
92+
return revision >= 0
7593
? new Version(major, minor, build, revision)
7694
: new Version(major, minor, build);
77-
return true;
7895
}
7996

8097
public static bool SupportsDirectNamedInstall(Version version)

tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ public async Task CreateWslInstance_RemovesStaleFileAtInstallPathBeforeInstallin
496496
public void WslInstallSupport_ParsesVersionAndVerboseDistroList()
497497
{
498498
Assert.True(WslInstallSupport.TryParseWslVersion("WSL version: 2.7.3.0", out var version));
499+
Assert.Equal(new Version(2, 7, 3, 0), version);
499500
Assert.True(WslInstallSupport.SupportsDirectNamedInstall(version));
500501

501502
Assert.True(WslInstallSupport.TryGetDistroVersion(
@@ -505,6 +506,93 @@ public void WslInstallSupport_ParsesVersionAndVerboseDistroList()
505506
Assert.Equal(2, distroVersion);
506507
}
507508

509+
// Regression: wsl.exe emits UTF-16LE on some Windows builds, and localized
510+
// Windows changes the human-readable label around the stable WSL product token.
511+
[Theory]
512+
[InlineData("WSL version: 2.7.3.0", "2.7.3.0")] // English
513+
[InlineData("WSL-Version: 2.7.7.0", "2.7.7.0")] // German / NUL-stripped UTF-16
514+
[InlineData("WSL-Version: 2.7.7.0\nKernelversion: 6.18.26.1-1\nWSLg-Version: 1.0.73.2\nWindows-Version: 10.0.26300.8553", "2.7.7.0")]
515+
[InlineData("Versión de WSL: 2.7.3.0", "2.7.3.0")] // Spanish
516+
[InlineData("Versión de WSL: 2.7.3.0\nKernel: 5.15.0.1", "2.7.3.0")] // Spanish with trailing lines
517+
[InlineData("WSL バージョン: 2.7.8.0", "2.7.8.0")] // Japanese-style label
518+
[InlineData("WSL版本: 2.7.9.0", "2.7.9.0")] // No separator after WSL
519+
public void WslInstallSupport_TryParseWslVersion_HandlesLocalizedAndHyphenatedLabels(string output, string expectedVersion)
520+
{
521+
Assert.True(WslInstallSupport.TryParseWslVersion(output, out var version),
522+
$"Expected TryParseWslVersion to succeed for: {output}");
523+
Assert.Equal(Version.Parse(expectedVersion), version);
524+
Assert.True(WslInstallSupport.SupportsDirectNamedInstall(version),
525+
$"Expected parsed version {version} to satisfy minimum install requirement");
526+
}
527+
528+
// Mirrors microsoft/WSL localization/strings/*/Resources.resw MessagePackageVersions.
529+
[Theory]
530+
[InlineData("cs-CZ", "Verze WSL: 2.7.3.0")]
531+
[InlineData("da-DK", "WSL-version: 2.7.3.0")]
532+
[InlineData("de-DE", "WSL-Version: 2.7.3.0")]
533+
[InlineData("en-GB", "WSL version: 2.7.3.0")]
534+
[InlineData("en-US", "WSL version: 2.7.3.0")]
535+
[InlineData("es-ES", "Versión de WSL: 2.7.3.0")]
536+
[InlineData("fi-FI", "WSL-versio: 2.7.3.0")]
537+
[InlineData("fr-FR", "Version WSL : 2.7.3.0")]
538+
[InlineData("hu-HU", "WSL-verzió: 2.7.3.0")]
539+
[InlineData("it-IT", "Versione WSL: 2.7.3.0")]
540+
[InlineData("ja-JP", "WSL バージョン: 2.7.3.0")]
541+
[InlineData("ko-KR", "WSL 버전: 2.7.3.0")]
542+
[InlineData("nb-NO", "WSL-versjon: 2.7.3.0")]
543+
[InlineData("nl-NL", "WSL-versie: 2.7.3.0")]
544+
[InlineData("pl-PL", "Wersja podsystemu WSL: 2.7.3.0")]
545+
[InlineData("pt-BR", "Versão do WSL: 2.7.3.0")]
546+
[InlineData("pt-PT", "Versão WSL: 2.7.3.0")]
547+
[InlineData("ru-RU", "Версия WSL: 2.7.3.0")]
548+
[InlineData("sv-SE", "WSL-version: 2.7.3.0")]
549+
[InlineData("tr-TR", "WSL sürümü: 2.7.3.0")]
550+
[InlineData("zh-CN", "WSL 版本: 2.7.3.0")]
551+
[InlineData("zh-TW", "WSL 版本: 2.7.3.0")]
552+
public void WslInstallSupport_TryParseWslVersion_HandlesMicrosoftLocalizedPackageVersionLabels(
553+
string locale,
554+
string output)
555+
{
556+
Assert.True(WslInstallSupport.TryParseWslVersion(output, out var version),
557+
$"Expected TryParseWslVersion to succeed for {locale}: {output}");
558+
Assert.Equal(new Version(2, 7, 3, 0), version);
559+
}
560+
561+
[Theory]
562+
[InlineData("WSL-Version: 2.7.7.0", "2.7.7.0")]
563+
[InlineData("Versión de WSL: 2.7.3.0", "2.7.3.0")]
564+
public void WslInstallSupport_TryParseWslVersion_NulStrippedUtf16_ParsesCorrectVersion(string raw, string expectedVersion)
565+
{
566+
// Simulate UTF-16LE NUL-byte injection then NUL-stripping.
567+
var utf16Encoded = string.Join("\0", raw.ToCharArray()) + "\0";
568+
var stripped = utf16Encoded.Replace("\0", "");
569+
Assert.True(WslInstallSupport.TryParseWslVersion(stripped, out var version),
570+
$"Expected TryParseWslVersion to succeed for NUL-stripped: {raw}");
571+
Assert.Equal(Version.Parse(expectedVersion), version);
572+
}
573+
574+
[Fact]
575+
public void WslInstallSupport_TryParseWslVersion_IgnoresAdjacentWslAndWindowsVersionLines()
576+
{
577+
var output = "WSLg-Version: 1.0.73.2\n"
578+
+ "Windows-Version: 10.0.26300.8553\n"
579+
+ "Kernelversion: 6.18.26.1-1\n"
580+
+ "WSL-Version: 2.7.7.0\n";
581+
582+
Assert.True(WslInstallSupport.TryParseWslVersion(output, out var version));
583+
Assert.Equal(new Version(2, 7, 7, 0), version);
584+
}
585+
586+
[Fact]
587+
public void WslInstallSupport_TryParseWslVersion_FailsWhenOnlyAdjacentComponentVersionsArePresent()
588+
{
589+
var output = "WSLg-Version: 1.0.73.2\n"
590+
+ "Windows-Version: 10.0.26300.8553\n"
591+
+ "Kernelversion: 6.18.26.1-1\n";
592+
593+
Assert.False(WslInstallSupport.TryParseWslVersion(output, out _));
594+
}
595+
508596
[Fact]
509597
public void WslInstallSupport_TryGetEnvironmentIssue_DetectsFirmwareVirtualizationOff()
510598
{

0 commit comments

Comments
 (0)