Skip to content
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

Add ProcessInfo implementations for other platforms #559

Merged
merged 11 commits into from
Apr 29, 2024
17 changes: 16 additions & 1 deletion Sources/FoundationEssentials/Platform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,27 @@ extension Platform {
extension Platform {
#if !FOUNDATION_FRAMEWORK
static func getHostname() -> String {
#if os(Windows)
var dwLength: DWORD = 0
GetComputerNameExA(ComputerNameDnsHostname, nil, &dwLength)
guard dwLength > 0 else { return "localhost" }

return withUnsafeTemporaryAllocation(of: Int8.self, capacity: dwLength + 1) { hostname in
guard GetComputerNameExA(ComputerNameDnsHostname, hostname.baseAddress!, &dwLength) else {
return "localhost"
}
return String(cString: hostname)
}
jmschonfeld marked this conversation as resolved.
Show resolved Hide resolved
#elseif os(WASI) // WASI does not have uname
return "localhost"
#else
return withUnsafeTemporaryAllocation(of: CChar.self, capacity: Platform.MAX_HOSTNAME_LENGTH + 1) {
guard gethostname($0.baseAddress!, numericCast(Platform.MAX_HOSTNAME_LENGTH)) == 0 else {
return ""
return "localhost"
}
return String(cString: $0.baseAddress!)
}
#endif
}
#endif // !FOUNDATION_FRAMEWORK
}
Expand Down
10 changes: 10 additions & 0 deletions Sources/FoundationEssentials/ProcessInfo/ProcessInfo+API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ public struct OperatingSystemVersion: Hashable, Codable, Sendable {
public let majorVersion: Int
public let minorVersion: Int
public let patchVersion: Int

public init() {
self.init(majorVersion: 0, minorVersion: 0, patchVersion: 0)
}

public init(majorVersion: Int, minorVersion: Int, patchVersion: Int) {
self.majorVersion = majorVersion
self.minorVersion = minorVersion
self.patchVersion = patchVersion
}
}

// MARK: - Getting Computer Information
Expand Down
190 changes: 177 additions & 13 deletions Sources/FoundationEssentials/ProcessInfo/ProcessInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ final class _ProcessInfo: Sendable {

var globallyUniqueString: String {
let uuid = UUID().uuidString
let pid = UInt64(getpid())
let pid = processIdentifier
#if canImport(Darwin)
let time: UInt64 = mach_absolute_time()
#else
Expand All @@ -117,7 +117,11 @@ final class _ProcessInfo: Sendable {
}

var processIdentifier: Int32 {
compnerd marked this conversation as resolved.
Show resolved Hide resolved
return getpid()
#if os(Windows)
return Int32(GetProcessId(GetCurrentProcess()))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unsafe isn't it? If the high bit is set, this will trap?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it will trap if high bits are set. Is it possible for Windows to return a process ID with high bits set? (on Darwin pid_t is defined as a 32-bit signed integer). If so, is there a behavior here that you think would be better? (I believe in SCF it will trap today if the high bits are set)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it is. The ProcessId is a DWORD (UInt32). I guess we could do a Int32(bitPattern:) initializer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but that would be a behavioral change from SCF today. Do you think that change / this new behavior would be expected for Windows clients of Foundation?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it would be more expected than a trap which would not make sense - I didn't create an Int32, why is this trapping?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I can update that to use a bit pattern then if you feel that'd be the better, more expected behavior for our windows clients

#else
return Int32(getpid())
#endif
}

var processName: String {
Expand All @@ -140,9 +144,28 @@ final class _ProcessInfo: Sendable {
return username
}
return ""
#else
// TODO: Windows
#elseif os(WASI)
// WASI does not have user concept
return ""
#elseif os(Windows)
return withUnsafeTemporaryAllocation(of: wchar_t.self, capacity: 1040) { usernameBuffer in
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this meant to be the user that the process is running as or whatever user is logged in? I wonder if we should query the information from the process.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it should be the "current user" (i.e. whichever user is logged in) based on what I've heard from @iCharlesHu - this code was mostly just copied from swift-corelibs-foundation as-is so I think it should behave the same as what SCF currently does

usernameBuffer[0] = 0
var size: DWORD = 1040
if GetUserNameW(usernameBuffer.baseAddress!, &size) {
// discount the extra NULL by decrementing the size
return String(decoding: usernameBuffer.prefix(size - 1), as: UTF16.self)
} else {
return "USERNAME".withCString(encodedAs: UTF16.self) { pwszName in
let dwLength = GetEnvironmentVariableW(pwszName, nil, 0)
return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { lpBuffer in
guard GetEnvironmentVariableW(pwszName, lpBuffer.baseAddress, dwLength) == dwLength - 1 else {
return ""
}
return String(decodingCString: lpBuffer.baseAddress!, as: UTF16.self)
}
}
}
}
#endif
}

Expand All @@ -154,8 +177,16 @@ final class _ProcessInfo: Sendable {
return String(cString: fullname)
}
return ""
#else
#elseif os(WASI)
return ""
#elseif os(Windows)
var ulLength: ULONG = 0
GetUserNameExW(NameDisplay, NULL, &ulLength)

return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: ulLength + 1) { wszBuffer in
GetUserNameExW(NameDisplay, wszBuffer.baseAddress!, &ulLength)
return String(decoding: wszBuffer.prefix(ulLength), as: UTF16.self)
}
#endif
}

Expand All @@ -182,17 +213,117 @@ extension _ProcessInfo {
return $0!
}
}

#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

var operatingSystemVersionString: String {
// TODO: Check for `/etc/os-release` for Linux once DataIO is ready
// https://github.com/apple/swift-foundation/issues/221
#if os(macOS)
#if os(macOS)
var versionString = "macOS"
#elseif os(Linux)
#elseif os(Linux)
if let osReleaseContents = try? Data(contentsOf: "/etc/os-release") {
let strContents = String(decoding: osReleaseContents, as: UTF8.self)
if let name = strContents.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(while: { $0 == "\"" }))
}
}

// Okay, we can't get a distro name, so try for generic info.
var versionString = "Linux"
#else
var versionString = ""
#endif
#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) {
jmschonfeld marked this conversation as resolved.
Show resolved Hide resolved
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
// On other systems at least return something.
return "Unknown"
#endif

#if canImport(Darwin) || os(Linux)
var uts: utsname = utsname()
if uname(&uts) == 0 {
let versionValue = withUnsafePointer(
Expand All @@ -204,9 +335,11 @@ extension _ProcessInfo {
}

return versionString
#endif
}

var operatingSystemVersion: (major: Int, minor: Int, patch: Int) {
#if canImport(Darwin) || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android)
var uts: utsname = utsname()
guard uname(&uts) == 0 else {
return (major: -1, minor: 0, patch: 0)
Expand All @@ -223,6 +356,19 @@ extension _ProcessInfo {
let minor = version.count >= 2 ? version[1] : 0
let patch = version.count >= 3 ? version[2] : 0
return (major: major, minor: minor, patch: patch)
#elseif os(Windows)
guard let osVersionInfo = self._rawOperatingSystemVersionInfo else {
return OperatingSystemVersion(majorVersion: -1, minorVersion: 0, patchVersion: 0)
}

return OperatingSystemVersion(
majorVersion: Int(osVersionInfo.dwMajorVersion),
minorVersion: Int(osVersionInfo.dwMinorVersion),
patchVersion: Int(osVersionInfo.dwBuildNumber)
)
#else
return OperatingSystemVersion(majorVersion: -1, minorVersion: 0, patchVersion: 0)
#endif
}

func isOperatingSystemAtLeast(_ version: (major: Int, minor: Int, patch: Int)) -> Bool {
Expand Down Expand Up @@ -261,8 +407,10 @@ extension _ProcessInfo {
return 0
}
return Int(count)
#else
#elseif os(Linux) || os(FreeBSD)
jmschonfeld marked this conversation as resolved.
Show resolved Hide resolved
return Int(sysconf(Int32(_SC_NPROCESSORS_CONF)))
#else
return 1
#endif
}

Expand All @@ -276,8 +424,20 @@ extension _ProcessInfo {
return 0
}
return Int(count)
#else
#elseif os(Linux) || os(FreeBSD)
return Int(sysconf(Int32(_SC_NPROCESSORS_ONLN)))
#elseif os(Windows)
var sysInfo = SYSTEM_INFO()
GetSystemInfo(&sysInfo)
let activeProcessorMask = sysInfo.dwActiveProcessorMask
// assumes sizeof(DWORD_PTR) is 64 bits or less
var v = activeProcessorMask
v = v - ((v >> 1) & 0x5555555555555555)
v = (v & 0x3333333333333333) + ((v >> 2) & 0x3333333333333333)
v = (v + (v >> 4)) & 0xf0f0f0f0f0f0f0f
return (v * 0x0101010101010101) >> ((sizeof(v) - 1) * 8)
jmschonfeld marked this conversation as resolved.
Show resolved Hide resolved
#else
return 0
#endif
}

Expand All @@ -293,10 +453,12 @@ extension _ProcessInfo {
}
return 0
}
#else
#elseif os(Linux) || os(FreeBSD)
jmschonfeld marked this conversation as resolved.
Show resolved Hide resolved
var memory = sysconf(Int32(_SC_PHYS_PAGES))
memory *= sysconf(Int32(_SC_PAGESIZE))
return UInt64(memory)
#else
return 0
#endif
}

Expand All @@ -305,6 +467,8 @@ extension _ProcessInfo {
let (_, secondsPerTick) = _systemClockTickRate
let time = mach_absolute_time()
return TimeInterval(time) * secondsPerTick
#elseif os(Windows)
return TimeInterval(GetTickCount64()) / 1000.0
#else
var ts = timespec()
guard clock_gettime(CLOCK_MONOTONIC, &ts) == 0 else {
Expand Down
2 changes: 2 additions & 0 deletions Sources/_CShims/platform_shims.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ _platform_shims_get_environ()
return *_NSGetEnviron();
#elif defined(_WIN32)
return _environ;
#elif TARGET_OS_WASI
return __wasilibc_get_environ();
#elif __has_include(<unistd.h>)
return environ;
#endif
Expand Down