Skip to content

Commit 4b137c8

Browse files
authored
Merge pull request swiftlang#2824 from gwynne/gwynne/improve-processinfo-osverstring
Make ProcessInfo.operatingSystemVersionString useful on Linux and Windows
2 parents 4b6d7f6 + af3bfa2 commit 4b137c8

File tree

2 files changed

+138
-23
lines changed

2 files changed

+138
-23
lines changed

Sources/Foundation/ProcessInfo.swift

Lines changed: 123 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,126 @@ open class ProcessInfo: NSObject {
8080
return CFUUIDCreateString(kCFAllocatorSystemDefault, uuid)._swiftObject
8181
}
8282

83-
open var operatingSystemVersionString: String {
84-
let fallback = "Unknown"
85-
#if os(Linux)
86-
let version = try? String(contentsOf: URL(fileURLWithPath: "/proc/version_signature", isDirectory: false), encoding: .utf8)
87-
return version ?? fallback
83+
#if os(Windows)
84+
internal var _rawOperatingSystemVersionInfo: RTL_OSVERSIONINFOEXW? {
85+
guard let ntdll = ("ntdll.dll".withCString(encodedAs: UTF16.self) {
86+
LoadLibraryExW($0, nil, DWORD(LOAD_LIBRARY_SEARCH_SYSTEM32))
87+
}) else {
88+
return nil
89+
}
90+
defer { FreeLibrary(ntdll) }
91+
typealias RTLGetVersionTy = @convention(c) (UnsafeMutablePointer<RTL_OSVERSIONINFOEXW>) -> NTSTATUS
92+
guard let pfnRTLGetVersion = unsafeBitCast(GetProcAddress(ntdll, "RtlGetVersion"), to: Optional<RTLGetVersionTy>.self) else {
93+
return nil
94+
}
95+
var osVersionInfo = RTL_OSVERSIONINFOEXW()
96+
osVersionInfo.dwOSVersionInfoSize = DWORD(MemoryLayout<RTL_OSVERSIONINFOEXW>.size)
97+
guard pfnRTLGetVersion(&osVersionInfo) == 0 else {
98+
return nil
99+
}
100+
return osVersionInfo
101+
}
102+
#endif
103+
104+
internal lazy var _operatingSystemVersionString: String = {
105+
#if canImport(Darwin)
106+
// Just use CoreFoundation on Darwin
107+
return CFCopySystemVersionString()?._swiftObject ?? "Darwin"
108+
#elseif os(Linux)
109+
// Try to parse a `PRETTY_NAME` out of `/etc/os-release`.
110+
if let osReleaseContents = try? String(contentsOf: URL(fileURLWithPath: "/etc/os-release", isDirectory: false)),
111+
let name = osReleaseContents.split(separator: "\n").first(where: { $0.hasPrefix("PRETTY_NAME=") })
112+
{
113+
// This is extremely simplistic but manages to work for all known cases.
114+
return String(name.dropFirst("PRETTY_NAME=".count).trimmingCharacters(in: .init(charactersIn: "\"")))
115+
}
116+
117+
// Okay, we can't get a distro name, so try for generic info.
118+
var versionString = "Linux"
119+
120+
// Try to get a release version number from `uname -r`.
121+
var utsNameBuffer = utsname()
122+
if uname(&utsNameBuffer) == 0 {
123+
let release = withUnsafePointer(to: &utsNameBuffer.release.0) { String(cString: $0) }
124+
if !release.isEmpty {
125+
versionString += " \(release)"
126+
}
127+
}
128+
129+
return versionString
130+
#elseif os(Windows)
131+
var versionString = "Windows"
132+
133+
guard let osVersionInfo = self._rawOperatingSystemVersionInfo else {
134+
return versionString
135+
}
136+
137+
// Windows has no canonical way to turn the fairly complex `RTL_OSVERSIONINFOW` version info into a string. We
138+
// do our best here to construct something consistent. Unfortunately, to provide a useful result, this requires
139+
// hardcoding several of the somewhat ambiguous values in the table provided here:
140+
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw#remarks
141+
switch (osVersionInfo.dwMajorVersion, osVersionInfo.dwMinorVersion) {
142+
case (5, 0): versionString += " 2000"
143+
case (5, 1): versionString += " XP"
144+
case (5, 2) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " XP Professional x64"
145+
case (5, 2) where osVersionInfo.wSuiteMask == VER_SUITE_WH_SERVER: versionString += " Home Server"
146+
case (5, 2): versionString += " Server 2003"
147+
case (6, 0) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " Vista"
148+
case (6, 0): versionString += " Server 2008"
149+
case (6, 1) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " 7"
150+
case (6, 1): versionString += " Server 2008 R2"
151+
case (6, 2) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " 8"
152+
case (6, 2): versionString += " Server 2012"
153+
case (6, 3) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " 8.1"
154+
case (6, 3): versionString += " Server 2012 R2" // We assume the "10,0" numbers in the table for this are a typo
155+
case (10, 0) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " 10"
156+
case (10, 0): versionString += " Server 2019" // The table gives identical values for 2016 and 2019, so we just assume 2019 here
157+
case let (maj, min): versionString += " \(maj).\(min)" // If all else fails, just give the raw version number
158+
}
159+
versionString += " (build \(osVersionInfo.dwBuildNumber))"
160+
// For now we ignore the `szCSDVersion`, `wServicePackMajor`, and `wServicePackMinor` values.
161+
return versionString
162+
#elseif os(FreeBSD)
163+
// Try to get a release version from `uname -r`.
164+
var versionString = "FreeBSD"
165+
var utsNameBuffer = utsname()
166+
if uname(&utsNameBuffer) == 0 {
167+
let release = withUnsafePointer(to: &utsNameBuffer.release.0) { String(cString: $0) }
168+
if !release.isEmpty {
169+
versionString += " \(release)"
170+
}
171+
}
172+
return versionString
173+
#elseif os(OpenBSD)
174+
// TODO: `uname -r` probably works here too.
175+
return "OpenBSD"
176+
#elseif os(Android)
177+
/// In theory, we need to do something like this:
178+
///
179+
/// var versionString = "Android"
180+
/// let property = String(unsafeUninitializedCapacity: PROP_VALUE_MAX) { buf in
181+
/// __system_property_get("ro.build.description", buf.baseAddress!)
182+
/// }
183+
/// if !property.isEmpty {
184+
/// versionString += " \(property)"
185+
/// }
186+
/// return versionString
187+
return "Android"
188+
#elseif os(PS4)
189+
return "PS4"
190+
#elseif os(Cygwin)
191+
// TODO: `uname -r` probably works here too.
192+
return "Cygwin"
193+
#elseif os(Haiku)
194+
return "Haiku"
195+
#elseif os(WASI)
196+
return "WASI"
88197
#else
89-
return CFCopySystemVersionString()?._swiftObject ?? fallback
198+
// On other systems at least return something.
199+
return "Unknown"
90200
#endif
91-
}
201+
}()
202+
open var operatingSystemVersionString: String { return _operatingSystemVersionString }
92203

93204
open var operatingSystemVersion: OperatingSystemVersion {
94205
// The following fallback values match Darwin Foundation
@@ -108,27 +219,16 @@ open class ProcessInfo: NSObject {
108219
}
109220
versionString = productVersion._swiftObject
110221
#elseif os(Windows)
111-
guard let ntdll = ("ntdll.dll".withCString(encodedAs: UTF16.self) {
112-
LoadLibraryExW($0, nil, DWORD(LOAD_LIBRARY_SEARCH_SYSTEM32))
113-
}) else {
114-
return OperatingSystemVersion(majorVersion: fallbackMajor, minorVersion: fallbackMinor, patchVersion: fallbackPatch)
115-
}
116-
defer { FreeLibrary(ntdll) }
117-
typealias RTLGetVersionTy = @convention(c) (UnsafeMutablePointer<RTL_OSVERSIONINFOW>) -> NTSTATUS
118-
guard let pfnRTLGetVersion = unsafeBitCast(GetProcAddress(ntdll, "RtlGetVersion"), to: Optional<RTLGetVersionTy>.self) else {
119-
return OperatingSystemVersion(majorVersion: fallbackMajor, minorVersion: fallbackMinor, patchVersion: fallbackPatch)
120-
}
121-
var osVersionInfo = RTL_OSVERSIONINFOW()
122-
osVersionInfo.dwOSVersionInfoSize = DWORD(MemoryLayout<RTL_OSVERSIONINFOW>.size)
123-
guard pfnRTLGetVersion(&osVersionInfo) == 0 else {
222+
guard let osVersionInfo = self._rawOperatingSystemVersionInfo else {
124223
return OperatingSystemVersion(majorVersion: fallbackMajor, minorVersion: fallbackMinor, patchVersion: fallbackPatch)
125224
}
225+
126226
return OperatingSystemVersion(
127227
majorVersion: Int(osVersionInfo.dwMajorVersion),
128228
minorVersion: Int(osVersionInfo.dwMinorVersion),
129229
patchVersion: Int(osVersionInfo.dwBuildNumber)
130230
)
131-
#else
231+
#elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
132232
var utsNameBuffer = utsname()
133233
guard uname(&utsNameBuffer) == 0 else {
134234
return OperatingSystemVersion(majorVersion: fallbackMajor, minorVersion: fallbackMinor, patchVersion: fallbackPatch)
@@ -138,6 +238,8 @@ open class ProcessInfo: NSObject {
138238
}
139239
let idx = release.firstIndex(of: "-") ?? release.endIndex
140240
versionString = String(release[..<idx])
241+
#else
242+
return OperatingSystemVersion(majorVersion: fallbackMajor, minorVersion: fallbackMinor, patchVersion: fallbackPatch)
141243
#endif
142244
let versionComponents = versionString.split(separator: ".").map(String.init).compactMap({ Int($0) })
143245
let majorVersion = versionComponents.dropFirst(0).first ?? fallbackMajor

Tests/Foundation/Tests/TestProcessInfo.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,24 @@ class TestProcessInfo : XCTestCase {
2222
let processInfo = ProcessInfo.processInfo
2323
let versionString = processInfo.operatingSystemVersionString
2424
XCTAssertFalse(versionString.isEmpty)
25-
25+
26+
#if os(Linux)
27+
// Since the list of supported distros tends to change, at least check that it used os-release (if it's there).
28+
if let distroId = try? String(contentsOf: URL(fileURLWithPath: "/etc/os-release", isDirectory: false)),
29+
distroId.contains("PRETTY_NAME")
30+
{
31+
XCTAssertTrue(distroId.contains(versionString))
32+
} else {
33+
XCTAssertTrue(versionString.contains("Linux"))
34+
}
35+
#elseif os(Windows)
36+
XCTAssertTrue(versionString.hasPrefix("Windows"))
37+
#endif
38+
2639
let version = processInfo.operatingSystemVersion
2740
XCTAssert(version.majorVersion != 0)
2841

29-
#if os(Linux) || canImport(Darwin)
42+
#if canImport(Darwin) || os(Linux) || os(Windows)
3043
let minVersion = OperatingSystemVersion(majorVersion: 1, minorVersion: 0, patchVersion: 0)
3144
XCTAssertTrue(processInfo.isOperatingSystemAtLeast(minVersion))
3245
#endif

0 commit comments

Comments
 (0)