Skip to content

Commit 28d88b1

Browse files
authored
Merge 39f0812 into 8c95e44
2 parents 8c95e44 + 39f0812 commit 28d88b1

2 files changed

Lines changed: 128 additions & 25 deletions

File tree

source/winVersion.py

Lines changed: 87 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,55 @@
88
making sure NVDA can run on a minimum supported version of Windows.
99
"""
1010

11+
from typing import Optional
1112
import sys
1213
import os
1314
import functools
15+
import winreg
16+
from buildVersion import version_year
17+
18+
19+
# Records a mapping between Windows builds and release names.
20+
# These include build 10240 for Windows 10 1507 and releases with multiple release builds.
21+
# These are applicable to Windows 10 as they report the same system version (10.0).
22+
_BUILDS_TO_RELEASE_NAMES = {
23+
10240: "Windows 10 1507",
24+
10586: "Windows 10 1511",
25+
14393: "Windows 10 1607",
26+
15063: "Windows 10 1703",
27+
16299: "Windows 10 1709",
28+
17134: "Windows 10 1803",
29+
17763: "Windows 10 1809",
30+
18362: "Windows 10 1903",
31+
18363: "Windows 10 1909",
32+
19041: "Windows 10 2004",
33+
19042: "Windows 10 20H2",
34+
19043: "Windows 10 21H1"
35+
}
36+
37+
38+
@functools.lru_cache(maxsize=1)
39+
def _getRunningVersionNameFromWinReg() -> str:
40+
"""Returns the Windows release name defined in Widnows Registry.
41+
This is applicable on Windows 10 Version 1511 (build 10586) and later.
42+
"""
43+
# Release name is recorded in Windows Registry from Windows 10 Version 1511 (build 10586) onwards.
44+
if getWinVer() < WIN10_1511:
45+
raise RuntimeError("Release name is not recorded in Windows Registry on this version of Windows")
46+
# Cache the version in use on the system.
47+
with winreg.OpenKey(
48+
winreg.HKEY_LOCAL_MACHINE, r"Software\Microsoft\Windows NT\CurrentVersion"
49+
) as currentVersion:
50+
# Version 20H2 and later where a separate display version string is used.
51+
try:
52+
releaseId = winreg.QueryValueEx(currentVersion, "DisplayVersion")[0]
53+
except OSError:
54+
# Don't set anything if this is Windows 10 1507 or earlier.
55+
try:
56+
releaseId = winreg.QueryValueEx(currentVersion, "ReleaseID")[0]
57+
except OSError:
58+
releaseId = ""
59+
return releaseId
1460

1561

1662
@functools.total_ordering
@@ -26,20 +72,26 @@ def __init__(
2672
major: int = 0,
2773
minor: int = 0,
2874
build: int = 0,
75+
releaseName: Optional[str] = "",
2976
servicePack: str = "",
3077
productType: str = ""
3178
):
3279
self.major = major
3380
self.minor = minor
3481
self.build = build
82+
if releaseName:
83+
self.releaseName = releaseName
84+
else:
85+
self.releaseName = self._getWindowsReleaseName()
3586
self.servicePack = servicePack
3687
self.productType = productType
3788

38-
def _windowsVersionToReleaseName(self):
39-
"""Returns release names for a given Windows version.
89+
def _getWindowsReleaseName(self) -> str:
90+
"""Returns the public release name for a given Windows release based on major, minor, and build.
91+
This is useful if release names are not defined when constructing this class.
4092
For example, 6.1 will return 'Windows 7'.
4193
For Windows 10, feature update release name will be included.
42-
On server systems, client release names will be returned.
94+
On server systems, unless noted otherwise, client release names will be returned.
4395
For example, 'Windows 10 1809' will be returned on Server 2019 systems.
4496
"""
4597
if (self.major, self.minor) == (6, 1):
@@ -49,17 +101,18 @@ def _windowsVersionToReleaseName(self):
49101
elif (self.major, self.minor) == (6, 3):
50102
return "Windows 8.1"
51103
elif self.major == 10:
52-
buildsToReleases = {build: release for release, build in WIN10_RELEASE_NAME_TO_BUILDS.items()}
53-
if self.build in buildsToReleases:
54-
return f"Windows 10 {buildsToReleases[self.build]}"
55-
else:
56-
# Windows Insider build.
57-
return "Windows 10 prerelease"
104+
# From Version 1511 (build 10586), release Id/display version comes from Windows Registry.
105+
# However there are builds with no release name (Version 1507/10240)
106+
# or releases with different builds.
107+
# Look these up first before asking Windows Registry.
108+
if self.build in _BUILDS_TO_RELEASE_NAMES:
109+
return _BUILDS_TO_RELEASE_NAMES[self.build]
110+
return "Windows 10 unknown"
58111
else:
59-
raise RuntimeError("Unknown Windows release")
112+
return "Windows release unknown"
60113

61114
def __repr__(self):
62-
winVersionText = [self._windowsVersionToReleaseName()]
115+
winVersionText = [self.releaseName]
63116
winVersionText.append(f"({self.major}.{self.minor}.{self.build})")
64117
if self.servicePack != "":
65118
winVersionText.append(f"service pack {self.servicePack}")
@@ -103,10 +156,16 @@ def getWinVer():
103156
"""Returns a record of current Windows version NVDA is running on.
104157
"""
105158
winVer = sys.getwindowsversion()
159+
# #12509: on Windows 10, fetch whatever Windows Registry says for the current build.
160+
try:
161+
releaseName = f"Windows 10 {_getRunningVersionNameFromWinReg()}"
162+
except RuntimeError:
163+
releaseName = None
106164
return WinVersion(
107165
major=winVer.major,
108166
minor=winVer.minor,
109167
build=winVer.build,
168+
releaseName=releaseName,
110169
servicePack=winVer.service_pack,
111170
productType=("workstation", "domain controller", "server")[winVer.product_type - 1]
112171
)
@@ -124,20 +183,23 @@ def isUwpOcrAvailable():
124183
return os.path.isdir(UWP_OCR_DATA_PATH)
125184

126185

127-
WIN10_RELEASE_NAME_TO_BUILDS = {
128-
"1507": 10240,
129-
"1511": 10586,
130-
"1607": 14393,
131-
"1703": 15063,
132-
"1709": 16299,
133-
"1803": 17134,
134-
"1809": 17763,
135-
"1903": 18362,
136-
"1909": 18363,
137-
"2004": 19041,
138-
"20H2": 19042,
139-
"21H1": 19043,
140-
}
186+
# Deprecated: Windows 10 releases will be obtained from Windows Registry, no entries will be added.
187+
# The below map will be removed in 2022.1.
188+
if version_year < 2022:
189+
WIN10_RELEASE_NAME_TO_BUILDS = {
190+
"1507": 10240,
191+
"1511": 10586,
192+
"1607": 14393,
193+
"1703": 15063,
194+
"1709": 16299,
195+
"1803": 17134,
196+
"1809": 17763,
197+
"1903": 18362,
198+
"1909": 18363,
199+
"2004": 19041,
200+
"20H2": 19042,
201+
"21H1": 19043,
202+
}
141203

142204

143205
def isFullScreenMagnificationAvailable():

tests/unit/test_winVersion.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,44 @@ def test_moreRecentWinVer(self):
3535
self.assertGreaterEqual(
3636
audioDuckingAvailable, minimumWinVer
3737
)
38+
39+
def test_winVerKnownReleaseNameForWinVersionConstant(self):
40+
# Test the fact that later Windows releases provide version information in a consistent manner,
41+
# specifically, via Windows Registry on Windows 10 1511 and later.
42+
# Test with Windows Server 2016 (client release name: Windows 10 1607).
43+
server2016 = winVersion.WIN10_1607
44+
self.assertEqual(server2016.releaseName, "Windows 10 1607")
45+
46+
def test_winVerKnownBuildToReleaseName(self):
47+
# Specifically to test if the correct release name is returned for use in getWinVer() function.
48+
# Try Windows 10 1809.
49+
knownMajor, knownMinor, knownBuild = 10, 0, 17763
50+
knownPublicRelease = winVersion.WinVersion(
51+
major=knownMajor, minor=knownMinor, build=knownBuild
52+
)
53+
self.assertEqual(knownPublicRelease.releaseName, "Windows 10 1809")
54+
55+
def test_winVerReleaseNameFromWindowsRegistry(self):
56+
# Test to make sure something is indeed returned from Windows Registry
57+
# when fetching release names for Windows 10 releases.
58+
# Try Windows Insider Preview build 21390, which is recorded as 'Dev".
59+
# But on public releases, version recorded on Windows Registry is returned.
60+
# This will fail if release name cannot be obtained from Windows Registry
61+
# ("unknown" will be recorded in release name text),
62+
# usually if Release Id and/or display version key is not defined.
63+
major, minor, build = 10, 0, 21390
64+
insiderBuild = winVersion.WinVersion(
65+
major=major, minor=minor, build=build
66+
)
67+
self.assertNotIn(
68+
"unknown", insiderBuild.releaseName
69+
)
70+
71+
def test_winVerUnknownBuildToReleaseName(self):
72+
# It might be possible that Microsoft could use major.minor versions other than 10.0 in future releases.
73+
# Try Windows 8.1 which is actually version 6.3.
74+
unknownMajor, unknownMinor, unknownBuild = 8, 1, 0
75+
badWin81Info = winVersion.WinVersion(
76+
major=unknownMajor, minor=unknownMinor, build=unknownBuild
77+
)
78+
self.assertEqual(badWin81Info.releaseName, "Windows release unknown")

0 commit comments

Comments
 (0)