3405691582 03fe46f43b
OpenBSD support. (#1126)
* Advise porter on where to make necessary change.

In #1075 the change was already made for BSD (thank you!); my working
edit had this guidance to ensure future porters get an error directing
them where to make a necessary change.

Otherwise, the FoundationEssentials build will fail and complain these
variables are not defined but not have guidance as to where they are
sourced from.

* OpenBSD does not support extended attributes.

* OpenBSD does not have secure_getenv.

* Remaining OpenBSD changes.

* OpenBSD also needs `pthread_mutex_t?`.

* Originally I followed Darwin's check with `d_namlen`, but this should
  work too.

* Correct statvfs type casts for OpenBSD.

On OpenBSD, fsblkcnt_t -- the type of f_blocks -- is a UInt64; therefore,
so must `blockSize` be.

Ultimately, both sides of the `totalSizeBytes` multiplication should
probably be type cast for all platforms, but that's a more significant
functional change for another time.

* Default activeProcessorCount to 1, not 0.

After a rather tedious debugging session trying to figure out why
swiftpm-bootstrap appeared to be deadlocked, this turned out to be the
culprit. Perhaps this should be #error instead, but for now, set a
sensible default.

* Use sysconf for activeProcessorCount.

This is what Dispatch does in some places for OpenBSD anyway, so do
likewise here.
2025-04-07 10:46:18 -07:00

630 lines
22 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
internal import _FoundationCShims
#if canImport(Darwin)
import Darwin
#elseif canImport(Android)
@preconcurrency import Bionic
import unistd
#elseif canImport(Glibc)
@preconcurrency import Glibc
#elseif canImport(Musl)
@preconcurrency import Musl
#elseif os(Windows)
import WinSDK
#elseif os(WASI)
@preconcurrency import WASILibc
#endif
#if !NO_PROCESS
final class _ProcessInfo: Sendable {
static let processInfo: _ProcessInfo = _ProcessInfo()
private let state: LockedState<State>
// Host name resolution CAN take infinite time,
// so at the bare min do not share the lock with the
// rest of the state
private let _hostName: LockedState<String?>
internal init() {
let state: State = State()
self.state = LockedState(initialState: state)
self._hostName = LockedState(initialState: nil)
}
var arguments: [String] {
// Bin compat: always use full executable path
// for arg0. CommandLine.arguments.first may not
// always be the full executable path, most
// noticeably when you launch the process via `$PATH`
// instead of full path.
return state.withLock {
if let existing = $0.arguments {
return existing
}
var current = CommandLine.arguments
// Replace the process path
if let fullPath = Platform.getFullExecutablePath() {
// Swift's CommandLine.arguments can be empty
if current.isEmpty {
current = [fullPath]
} else {
current[0] = fullPath
}
}
$0.arguments = current
return current
}
}
var environment: [String : String] {
return withCopiedEnv { environments in
var results: [String : String] = [:]
for env in environments {
let environmentString = String(cString: env)
#if os(Windows)
// Windows GetEnvironmentStringsW API can return
// magic environment variables set by the cmd shell
// that starts with `=`
// We should exclude these values
if environmentString.utf8.first == ._equal {
continue
}
#endif // os(Windows)
guard let delimiter = environmentString.firstIndex(of: "=") else {
continue
}
let key = String(environmentString[environmentString.startIndex ..< delimiter])
let value = String(environmentString[environmentString.index(after: delimiter) ..< environmentString.endIndex])
results[key] = value
}
return results
}
}
private func withCopiedEnv<R>(_ body: ([UnsafeMutablePointer<CChar>]) -> R) -> R {
var values: [UnsafeMutablePointer<CChar>] = []
#if os(Windows)
guard let pwszEnvironmentBlock = GetEnvironmentStringsW() else {
return body([])
}
defer { FreeEnvironmentStringsW(pwszEnvironmentBlock) }
var pwszEnvironmentEntry: LPWCH? = pwszEnvironmentBlock
while let value = pwszEnvironmentEntry {
let entry = String(decodingCString: value, as: UTF16.self)
if entry.isEmpty { break }
values.append(entry.withCString { _strdup($0)! })
pwszEnvironmentEntry = pwszEnvironmentEntry?.advanced(by: wcslen(value) + 1)
}
#else
// This lock is taken by calls to getenv, so we want as few callouts to other code as possible here.
_platform_shims_lock_environ()
guard let environments: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?> =
_platform_shims_get_environ() else {
_platform_shims_unlock_environ()
return body([])
}
var curr = environments
while let value = curr.pointee {
values.append(strdup(value))
curr = curr.advanced(by: 1)
}
_platform_shims_unlock_environ()
#endif
defer { values.forEach { free($0) } }
return body(values)
}
var globallyUniqueString: String {
let uuid = UUID().uuidString
let pid = processIdentifier
#if canImport(Darwin)
let time: UInt64 = mach_absolute_time()
#elseif os(Windows)
var ullTime: ULONGLONG = 0
QueryUnbiasedInterruptTimePrecise(&ullTime)
let time: UInt64 = ullTime
#else
var ts: timespec = timespec()
#if os(FreeBSD) || os(OpenBSD)
clock_gettime(CLOCK_MONOTONIC, &ts)
#else
clock_gettime(CLOCK_MONOTONIC_RAW, &ts)
#endif
let time: UInt64 = UInt64(ts.tv_sec) * 1000000000 + UInt64(ts.tv_nsec)
#endif
let timeString = String(time, radix: 16, uppercase: true)
let padding = String(repeating: "0", count: 16 - timeString.count)
return "\(uuid)-\(pid)-\(padding)\(timeString)"
}
var processIdentifier: Int32 {
#if os(Windows)
return Int32(bitPattern: UInt32(GetProcessId(GetCurrentProcess())))
#else
return Int32(getpid())
#endif
}
var processName: String {
get {
return state.withLock {
if let name = $0.processName {
return name
}
let processName = _ProcessInfo._getProcessName()
$0.processName = processName
return processName
}
}
set {
state.withLock{ $0.processName = newValue }
}
}
var userName: String {
#if canImport(Darwin) || canImport(Android) || canImport(Glibc) || canImport(Musl)
// Darwin and Linux
let (euid, _) = Platform.getUGIDs()
if let username = Platform.name(forUID: euid) {
return username
} else if let username = self.environment["USER"] {
return username
}
return ""
#elseif os(WASI)
// WASI does not have user concept
return ""
#elseif os(Windows)
var dwSize: DWORD = 0
_ = GetUserNameW(nil, &dwSize)
return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwSize)) {
guard GetUserNameW($0.baseAddress!, &dwSize) 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)
}
}
}
return String(decodingCString: $0.baseAddress!, as: UTF16.self)
}
#endif
}
var fullUserName: String {
#if canImport(Android) && (arch(i386) || arch(arm))
// On LP32 Android, pw_gecos doesn't exist and is presumed to be NULL.
return ""
#elseif canImport(Darwin) || canImport(Android) || canImport(Glibc) || canImport(Musl)
let (euid, _) = Platform.getUGIDs()
if let fullName = Platform.fullName(forUID: euid) {
return fullName
}
return ""
#elseif os(WASI)
return ""
#elseif os(Windows)
var ulLength: ULONG = 0
_ = GetUserNameExW(NameDisplay, nil, &ulLength)
return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(ulLength)) { wszBuffer in
guard GetUserNameExW(NameDisplay, wszBuffer.baseAddress!, &ulLength) == 0 else {
return ""
}
return String(decoding: wszBuffer.prefix(Int(ulLength)), as: UTF16.self)
}
#endif
}
#if canImport(Darwin)
private var _systemClockTickRate: (ticksPerSecond: TimeInterval, secondsPerTick: TimeInterval) {
var info = mach_timebase_info()
mach_timebase_info(&info)
let tps = (1.0E9 / TimeInterval(info.numer)) * TimeInterval(info.denom)
let spt = 1.0 / tps
return (ticksPerSecond: tps, secondsPerTick: spt)
}
#endif
}
// MARK: - Getting Host Information
extension _ProcessInfo {
var hostName: String {
return _hostName.withLock {
if let name = $0 {
return name
}
// Resolve hostname
$0 = Platform.getHostname()
return $0!
}
}
#if os(Windows)
internal var _rawOSVersion: RTL_OSVERSIONINFOEXW? {
guard let ntdll = "ntdll.dll".withCString(encodedAs: UTF16.self, LoadLibraryW) 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 {
#if os(macOS)
var versionString = "macOS"
#elseif os(Linux)
if let osReleaseContents = try? Data(contentsOf: URL(filePath: "/etc/os-release", directoryHint: .notDirectory)) {
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"
#elseif os(Windows)
guard let osVersionInfo = self._rawOSVersion else {
return "Windows"
}
// 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
let release = switch (osVersionInfo.dwMajorVersion, osVersionInfo.dwMinorVersion, osVersionInfo.dwBuildNumber) {
case (5, 0, _): "2000"
case (5, 1, _): "XP"
case (5, 2, _) where osVersionInfo.wProductType == VER_NT_WORKSTATION: "XP Professional x64"
case (5, 2, _) where osVersionInfo.wSuiteMask == VER_SUITE_WH_SERVER: "Home Server"
case (5, 2, _): "Server 2003"
case (6, 0, _) where osVersionInfo.wProductType == VER_NT_WORKSTATION: "Vista"
case (6, 0, _): "Server 2008"
case (6, 1, _) where osVersionInfo.wProductType == VER_NT_WORKSTATION: "7"
case (6, 1, _): "Server 2008 R2"
case (6, 2, _) where osVersionInfo.wProductType == VER_NT_WORKSTATION: "8"
case (6, 2, _): "Server 2012"
case (6, 3, _) where osVersionInfo.wProductType == VER_NT_WORKSTATION: "8.1"
case (6, 3, _): "Server 2012 R2" // We assume the "10,0" numbers in the table for this are a typo
case (10, 0, ..<22000) where osVersionInfo.wProductType == VER_NT_WORKSTATION: "10"
case (10, 0, 22000...) where osVersionInfo.wProductType == VER_NT_WORKSTATION: "11"
case (10, 0, _): "Server 2019" // The table gives identical values for 2016 and 2019, so we just assume 2019 here
case let (maj, min, _): "Unknown (\(maj).\(min))" // If all else fails, just give the raw version number
}
// For now we ignore the `szCSDVersion`, `wServicePackMajor`, and `wServicePackMinor` values.
return "Windows \(release) (build \(osVersionInfo.dwBuildNumber))"
#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 os(macOS) || os(Linux)
var uts: utsname = utsname()
if uname(&uts) == 0 {
let versionValue = withUnsafePointer(
to: &uts.release.0, { String(cString: $0) })
if !versionValue.isEmpty {
versionString += " \(versionValue)"
}
}
return versionString
#endif
}
var operatingSystemVersion: (major: Int, minor: Int, patch: Int) {
#if canImport(Darwin) || os(Linux) || os(FreeBSD) || os(OpenBSD) || canImport(Android)
var uts: utsname = utsname()
guard uname(&uts) == 0 else {
return (major: -1, minor: 0, patch: 0)
}
var versionString = withUnsafePointer(
to: &uts.release.0, { String(cString: $0) })
if let dashIndex = versionString.firstIndex(of: "-") {
versionString = String(versionString[versionString.startIndex ..< dashIndex])
}
let version = versionString.split(separator: ".")
.compactMap { Int($0) }
let major = version.count >= 1 ? version[0] : -1
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._rawOSVersion else {
return (major: -1, minor: 0, patch: 0)
}
return(
major: Int(osVersionInfo.dwMajorVersion),
minor: Int(osVersionInfo.dwMinorVersion),
patch: Int(osVersionInfo.dwBuildNumber)
)
#else
return (major: -1, minor: 0, patch: 0)
#endif
}
func isOperatingSystemAtLeast(_ version: (major: Int, minor: Int, patch: Int)) -> Bool {
let (currentMajor, currentMinor, currentPatch) = operatingSystemVersion
if currentMajor < version.major {
return false
}
if currentMajor > version.major {
return true
}
if currentMinor < version.minor {
return false
}
if currentMinor > version.minor {
return true
}
if currentPatch < version.patch {
return false
}
if currentPatch > version.patch {
return true
}
return true
}
}
// MARK: - Getting Computer Information
extension _ProcessInfo {
var processorCount: Int {
#if canImport(Darwin)
var count: Int32 = -1
var mib: [Int32] = [CTL_HW, HW_NCPU]
var countSize = MemoryLayout<Int32>.size
let status = sysctl(&mib, UInt32(mib.count), &count, &countSize, nil, 0)
guard status == 0 else {
return 0
}
return Int(count)
#elseif os(Windows)
var siInfo = SYSTEM_INFO()
GetSystemInfo(&siInfo)
return Int(siInfo.dwNumberOfProcessors)
#elseif os(Linux) || os(FreeBSD) || canImport(Android)
return Int(sysconf(Int32(_SC_NPROCESSORS_CONF)))
#else
return 1
#endif
}
var activeProcessorCount: Int {
#if canImport(Darwin)
var count: Int32 = -1
var mib: [Int32] = [CTL_HW, HW_AVAILCPU]
var countSize = MemoryLayout<Int32>.size
let status = sysctl(&mib, UInt32(mib.count), &count, &countSize, nil, 0)
guard status == 0 else {
return 0
}
return Int(count)
#elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || canImport(Android)
#if os(Linux)
if let fsCount = Self.fsCoreCount() {
return fsCount
}
#endif
return Int(sysconf(Int32(_SC_NPROCESSORS_ONLN)))
#elseif os(Windows)
var sysInfo = SYSTEM_INFO()
GetSystemInfo(&sysInfo)
return sysInfo.dwActiveProcessorMask.nonzeroBitCount
#else
return 1
#endif
}
#if os(Linux)
// Support for CFS quotas for cpu count as used by Docker.
// Based on swift-nio code, https://github.com/apple/swift-nio/pull/1518
private static let cfsQuotaURL = URL(filePath: "/sys/fs/cgroup/cpu/cpu.cfs_quota_us", directoryHint: .notDirectory)
private static let cfsPeriodURL = URL(filePath: "/sys/fs/cgroup/cpu/cpu.cfs_period_us", directoryHint: .notDirectory)
private static let cpuSetURL = URL(filePath: "/sys/fs/cgroup/cpuset/cpuset.cpus", directoryHint: .notDirectory)
private static func firstLineOfFile(_ url: URL) throws -> Substring {
let data = try Data(contentsOf: url)
if let string = String(data: data, encoding: .utf8), let line = string.split(separator: "\n").first {
return line
} else {
return ""
}
}
private static func countCoreIds(cores: Substring) -> Int? {
let ids = cores.split(separator: "-", maxSplits: 1)
guard let first = ids.first.flatMap({ Int($0, radix: 10) }),
let last = ids.last.flatMap({ Int($0, radix: 10) }),
last >= first
else {
return nil
}
return 1 + last - first
}
private static func coreCount(cpuset cpusetURL: URL) -> Int? {
guard let cpuset = try? firstLineOfFile(cpusetURL).split(separator: ","),
!cpuset.isEmpty
else { return nil }
if let first = cpuset.first, let count = countCoreIds(cores: first) {
return count
} else {
return nil
}
}
private static func coreCount(quota quotaURL: URL, period periodURL: URL) -> Int? {
guard let quota = try? Int(firstLineOfFile(quotaURL)),
quota > 0
else { return nil }
guard let period = try? Int(firstLineOfFile(periodURL)),
period > 0
else { return nil }
return (quota - 1 + period) / period // always round up if fractional CPU quota requested
}
private static func fsCoreCount() -> Int? {
if let quota = coreCount(quota: cfsQuotaURL, period: cfsPeriodURL) {
return quota
} else if let cpusetCount = coreCount(cpuset: cpuSetURL) {
return cpusetCount
} else {
return nil
}
}
#endif
var physicalMemory: UInt64 {
#if canImport(Darwin)
var memory: UInt64 = 0
var memorySize = MemoryLayout<UInt64>.size
let name = "hw.memsize"
return name.withCString {
let status = sysctlbyname($0, &memory, &memorySize, nil, 0)
if status == 0 {
return memory
}
return 0
}
#elseif os(Windows)
var totalMemoryKB: ULONGLONG = 0
guard GetPhysicallyInstalledSystemMemory(&totalMemoryKB) else {
return 0
}
return totalMemoryKB * 1024
#elseif os(Linux) || os(FreeBSD) || canImport(Android)
var memory = sysconf(Int32(_SC_PHYS_PAGES))
memory *= sysconf(Int32(_SC_PAGESIZE))
return UInt64(memory)
#else
return 0
#endif
}
var systemUptime: TimeInterval {
#if canImport(Darwin)
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 {
return 0
}
return TimeInterval(ts.tv_sec) +
TimeInterval(ts.tv_nsec) / 1.0E9;
#endif
}
}
extension _ProcessInfo {
struct State {
var processName: String?
var arguments: [String]?
}
private static func _getProcessName() -> String {
guard let processPath = CommandLine.arguments.first else {
return ""
}
return processPath.lastPathComponent
}
#if os(macOS)
private func _getSVUID() -> UInt32? {
var mib: (Int32, Int32, Int32, Int32) = (
CTL_KERN,
KERN_PROC,
KERN_PROC_PID,
getpid()
)
var kinfo: kinfo_proc = kinfo_proc()
var klen = MemoryLayout<kinfo_proc>.size
let ret = withUnsafeMutablePointer(to: &mib) {
return $0.withMemoryRebound(to: Int32.self, capacity: 4) { ptr in
return sysctl(ptr, 4, &kinfo, &klen, nil, 0)
}
}
if ret != 0 {
return nil
}
return kinfo.kp_eproc.e_pcred.p_svuid
}
#endif // os(macOS)
}
#endif // !NO_PROCESS