Skip to content

Make ProcessInfo.operatingSystemVersionString useful on Linux and Windows #2824

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 123 additions & 21 deletions Sources/Foundation/ProcessInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,126 @@ open class ProcessInfo: NSObject {
return CFUUIDCreateString(kCFAllocatorSystemDefault, uuid)._swiftObject
}

open var operatingSystemVersionString: String {
let fallback = "Unknown"
#if os(Linux)
let version = try? String(contentsOf: URL(fileURLWithPath: "/proc/version_signature", isDirectory: false), encoding: .utf8)
return version ?? fallback
#if os(Windows)
internal var _rawOperatingSystemVersionInfo: RTL_OSVERSIONINFOEXW? {
guard let ntdll = ("ntdll.dll".withCString(encodedAs: UTF16.self) {
LoadLibraryExW($0, nil, DWORD(LOAD_LIBRARY_SEARCH_SYSTEM32))
}) else {
return nil
}
defer { FreeLibrary(ntdll) }
typealias RTLGetVersionTy = @convention(c) (UnsafeMutablePointer<RTL_OSVERSIONINFOEXW>) -> NTSTATUS
guard let pfnRTLGetVersion = unsafeBitCast(GetProcAddress(ntdll, "RtlGetVersion"), to: Optional<RTLGetVersionTy>.self) else {
return nil
}
var osVersionInfo = RTL_OSVERSIONINFOEXW()
osVersionInfo.dwOSVersionInfoSize = DWORD(MemoryLayout<RTL_OSVERSIONINFOEXW>.size)
guard pfnRTLGetVersion(&osVersionInfo) == 0 else {
return nil
}
return osVersionInfo
}
#endif

internal lazy var _operatingSystemVersionString: String = {
#if canImport(Darwin)
// Just use CoreFoundation on Darwin
return CFCopySystemVersionString()?._swiftObject ?? "Darwin"
#elseif os(Linux)
// Try to parse a `PRETTY_NAME` out of `/etc/os-release`.
if let osReleaseContents = try? String(contentsOf: URL(fileURLWithPath: "/etc/os-release", isDirectory: false)),
let name = osReleaseContents.split(separator: "\n").first(where: { $0.hasPrefix("PRETTY_NAME=") })
{
// This is extremely simplistic but manages to work for all known cases.
return String(name.dropFirst("PRETTY_NAME=".count).trimmingCharacters(in: .init(charactersIn: "\"")))
}

// Okay, we can't get a distro name, so try for generic info.
var versionString = "Linux"

// Try to get a release version number from `uname -r`.
var utsNameBuffer = utsname()
if uname(&utsNameBuffer) == 0 {
let release = withUnsafePointer(to: &utsNameBuffer.release.0) { String(cString: $0) }
if !release.isEmpty {
versionString += " \(release)"
}
}

return versionString
#elseif os(Windows)
var versionString = "Windows"

guard let osVersionInfo = self._rawOperatingSystemVersionInfo else {
return versionString
}

// Windows has no canonical way to turn the fairly complex `RTL_OSVERSIONINFOW` version info into a string. We
// do our best here to construct something consistent. Unfortunately, to provide a useful result, this requires
// hardcoding several of the somewhat ambiguous values in the table provided here:
// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_osversioninfoexw#remarks
switch (osVersionInfo.dwMajorVersion, osVersionInfo.dwMinorVersion) {
case (5, 0): versionString += " 2000"
case (5, 1): versionString += " XP"
case (5, 2) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " XP Professional x64"
case (5, 2) where osVersionInfo.wSuiteMask == VER_SUITE_WH_SERVER: versionString += " Home Server"
case (5, 2): versionString += " Server 2003"
case (6, 0) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " Vista"
case (6, 0): versionString += " Server 2008"
case (6, 1) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " 7"
case (6, 1): versionString += " Server 2008 R2"
case (6, 2) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " 8"
case (6, 2): versionString += " Server 2012"
case (6, 3) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " 8.1"
case (6, 3): versionString += " Server 2012 R2" // We assume the "10,0" numbers in the table for this are a typo
case (10, 0) where osVersionInfo.wProductType == VER_NT_WORKSTATION: versionString += " 10"
case (10, 0): versionString += " Server 2019" // The table gives identical values for 2016 and 2019, so we just assume 2019 here
case let (maj, min): versionString += " \(maj).\(min)" // If all else fails, just give the raw version number
}
versionString += " (build \(osVersionInfo.dwBuildNumber))"
// For now we ignore the `szCSDVersion`, `wServicePackMajor`, and `wServicePackMinor` values.
return versionString
#elseif os(FreeBSD)
// Try to get a release version from `uname -r`.
var versionString = "FreeBSD"
var utsNameBuffer = utsname()
if uname(&utsNameBuffer) == 0 {
let release = withUnsafePointer(to: &utsNameBuffer.release.0) { String(cString: $0) }
if !release.isEmpty {
versionString += " \(release)"
}
}
return versionString
#elseif os(OpenBSD)
// TODO: `uname -r` probably works here too.
return "OpenBSD"
#elseif os(Android)
/// In theory, we need to do something like this:
///
/// var versionString = "Android"
/// let property = String(unsafeUninitializedCapacity: PROP_VALUE_MAX) { buf in
/// __system_property_get("ro.build.description", buf.baseAddress!)
/// }
/// if !property.isEmpty {
/// versionString += " \(property)"
/// }
/// return versionString
return "Android"
#elseif os(PS4)
return "PS4"
#elseif os(Cygwin)
// TODO: `uname -r` probably works here too.
return "Cygwin"
#elseif os(Haiku)
return "Haiku"
#elseif os(WASI)
return "WASI"
#else
return CFCopySystemVersionString()?._swiftObject ?? fallback
// On other systems at least return something.
return "Unknown"
#endif
}
}()
open var operatingSystemVersionString: String { return _operatingSystemVersionString }

open var operatingSystemVersion: OperatingSystemVersion {
// The following fallback values match Darwin Foundation
Expand All @@ -108,27 +219,16 @@ open class ProcessInfo: NSObject {
}
versionString = productVersion._swiftObject
#elseif os(Windows)
guard let ntdll = ("ntdll.dll".withCString(encodedAs: UTF16.self) {
LoadLibraryExW($0, nil, DWORD(LOAD_LIBRARY_SEARCH_SYSTEM32))
}) else {
return OperatingSystemVersion(majorVersion: fallbackMajor, minorVersion: fallbackMinor, patchVersion: fallbackPatch)
}
defer { FreeLibrary(ntdll) }
typealias RTLGetVersionTy = @convention(c) (UnsafeMutablePointer<RTL_OSVERSIONINFOW>) -> NTSTATUS
guard let pfnRTLGetVersion = unsafeBitCast(GetProcAddress(ntdll, "RtlGetVersion"), to: Optional<RTLGetVersionTy>.self) else {
return OperatingSystemVersion(majorVersion: fallbackMajor, minorVersion: fallbackMinor, patchVersion: fallbackPatch)
}
var osVersionInfo = RTL_OSVERSIONINFOW()
osVersionInfo.dwOSVersionInfoSize = DWORD(MemoryLayout<RTL_OSVERSIONINFOW>.size)
guard pfnRTLGetVersion(&osVersionInfo) == 0 else {
guard let osVersionInfo = self._rawOperatingSystemVersionInfo else {
return OperatingSystemVersion(majorVersion: fallbackMajor, minorVersion: fallbackMinor, patchVersion: fallbackPatch)
}

return OperatingSystemVersion(
majorVersion: Int(osVersionInfo.dwMajorVersion),
minorVersion: Int(osVersionInfo.dwMinorVersion),
patchVersion: Int(osVersionInfo.dwBuildNumber)
)
#else
#elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
var utsNameBuffer = utsname()
guard uname(&utsNameBuffer) == 0 else {
return OperatingSystemVersion(majorVersion: fallbackMajor, minorVersion: fallbackMinor, patchVersion: fallbackPatch)
Expand All @@ -138,6 +238,8 @@ open class ProcessInfo: NSObject {
}
let idx = release.firstIndex(of: "-") ?? release.endIndex
versionString = String(release[..<idx])
#else
return OperatingSystemVersion(majorVersion: fallbackMajor, minorVersion: fallbackMinor, patchVersion: fallbackPatch)
#endif
let versionComponents = versionString.split(separator: ".").map(String.init).compactMap({ Int($0) })
let majorVersion = versionComponents.dropFirst(0).first ?? fallbackMajor
Expand Down
17 changes: 15 additions & 2 deletions Tests/Foundation/Tests/TestProcessInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,24 @@ class TestProcessInfo : XCTestCase {
let processInfo = ProcessInfo.processInfo
let versionString = processInfo.operatingSystemVersionString
XCTAssertFalse(versionString.isEmpty)


#if os(Linux)
// Since the list of supported distros tends to change, at least check that it used os-release (if it's there).
if let distroId = try? String(contentsOf: URL(fileURLWithPath: "/etc/os-release", isDirectory: false)),
distroId.contains("PRETTY_NAME")
{
XCTAssertTrue(distroId.contains(versionString))
} else {
XCTAssertTrue(versionString.contains("Linux"))
}
#elseif os(Windows)
XCTAssertTrue(versionString.hasPrefix("Windows"))
#endif

let version = processInfo.operatingSystemVersion
XCTAssert(version.majorVersion != 0)

#if os(Linux) || canImport(Darwin)
#if canImport(Darwin) || os(Linux) || os(Windows)
let minVersion = OperatingSystemVersion(majorVersion: 1, minorVersion: 0, patchVersion: 0)
XCTAssertTrue(processInfo.isOperatingSystemAtLeast(minVersion))
#endif
Expand Down