Browse Source

Linux Preparation (#3115)

* Adopt 5.1 formatting.

* Turn Protector into Protected property wrapper, add tests.

* Update README for Swift 5.1 / Xcode 11 requirement.

* Formatting.

* Fix build after master merge.

* Add protocol abstraction around locks, MutexLock.

* Replace CFAbsoluteTimeGetCurrent usage.

* Update User-Agent generation.

* Replace arc4random().

* Add Events typealias.

* Replace CFString conversion to internal code.

* Replace CF usage with direct type encoding check.

* Enable PR builds on all branches.

* Only include MutexLock on Linux.

Co-Authored-By: Jeff Kelley <147458+SlaunchaMan@users.noreply.github.com>

* Make lock type explicitly per platform.

Co-Authored-By: Jeff Kelley <147458+SlaunchaMan@users.noreply.github.com>

* Make UnfairLock Apple platform specific.

Co-Authored-By: Jeff Kelley <147458+SlaunchaMan@users.noreply.github.com>

* Clean up styling around platform checks.

* Remove explicit return for non returning func.

* Build fixes after master merge.

* Document why objCType checking works.

* Add precondition checks to MutexLock.

* Match platform wrapping style.

Co-authored-by: Jeff Kelley <147458+SlaunchaMan@users.noreply.github.com>
Jon Shier 5 years ago
parent
commit
52eb559f61

+ 10 - 0
Alamofire.xcodeproj/project.pbxproj

@@ -60,6 +60,10 @@
 		31501E882196962A005829F2 /* ParameterEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31501E872196962A005829F2 /* ParameterEncoderTests.swift */; };
 		31501E892196962A005829F2 /* ParameterEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31501E872196962A005829F2 /* ParameterEncoderTests.swift */; };
 		31501E8A2196962A005829F2 /* ParameterEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31501E872196962A005829F2 /* ParameterEncoderTests.swift */; };
+		315A4C56241EF28B00D57C7A /* StringEncoding+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315A4C55241EF28B00D57C7A /* StringEncoding+Alamofire.swift */; };
+		315A4C57241EF28B00D57C7A /* StringEncoding+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315A4C55241EF28B00D57C7A /* StringEncoding+Alamofire.swift */; };
+		315A4C58241EF28B00D57C7A /* StringEncoding+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315A4C55241EF28B00D57C7A /* StringEncoding+Alamofire.swift */; };
+		315A4C59241EF28B00D57C7A /* StringEncoding+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315A4C55241EF28B00D57C7A /* StringEncoding+Alamofire.swift */; };
 		31727418218BAEC90039FFCC /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31727417218BAEC90039FFCC /* HTTPMethod.swift */; };
 		31727419218BAEC90039FFCC /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31727417218BAEC90039FFCC /* HTTPMethod.swift */; };
 		3172741A218BAEC90039FFCC /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31727417218BAEC90039FFCC /* HTTPMethod.swift */; };
@@ -360,6 +364,7 @@
 		312D1E0C1FC2551400E51FF1 /* AdvancedUsage.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = AdvancedUsage.md; path = Documentation/AdvancedUsage.md; sourceTree = "<group>"; };
 		31425AC0241F098000EE3CCC /* InternalRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalRequestTests.swift; sourceTree = "<group>"; };
 		31501E872196962A005829F2 /* ParameterEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParameterEncoderTests.swift; sourceTree = "<group>"; };
+		315A4C55241EF28B00D57C7A /* StringEncoding+Alamofire.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StringEncoding+Alamofire.swift"; sourceTree = "<group>"; };
 		316250E41F00ABE900E207A6 /* ISSUE_TEMPLATE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = ISSUE_TEMPLATE.md; path = .github/ISSUE_TEMPLATE.md; sourceTree = "<group>"; };
 		316250E51F00ACD000E207A6 /* PULL_REQUEST_TEMPLATE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = PULL_REQUEST_TEMPLATE.md; path = .github/PULL_REQUEST_TEMPLATE.md; sourceTree = "<group>"; };
 		31727417218BAEC90039FFCC /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = "<group>"; };
@@ -651,6 +656,7 @@
 				4C43669A1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift */,
 				319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */,
 				4196936122FA1E05001EA5D5 /* Result+Alamofire.swift */,
+				315A4C55241EF28B00D57C7A /* StringEncoding+Alamofire.swift */,
 				4C0CB640220CA89400604EDC /* URLRequest+Alamofire.swift */,
 				31F5085C20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift */,
 			);
@@ -1287,6 +1293,7 @@
 				4CF6270F1BA7CBF60011A099 /* ResponseSerialization.swift in Sources */,
 				4C256A0821EEB69000AD5D87 /* RequestInterceptor.swift in Sources */,
 				4C43669D1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */,
+				315A4C58241EF28B00D57C7A /* StringEncoding+Alamofire.swift in Sources */,
 				4C3D00561C66A63000D1F709 /* NetworkReachabilityManager.swift in Sources */,
 				311B199220B0E3480036823B /* MultipartUpload.swift in Sources */,
 				4C4466ED21F8F5D800AC9703 /* CachedResponseHandler.swift in Sources */,
@@ -1363,6 +1370,7 @@
 				4C0CB642220CA89400604EDC /* URLRequest+Alamofire.swift in Sources */,
 				4C23EB441B327C5B0090E0BC /* MultipartFormData.swift in Sources */,
 				4C256A0721EEB69000AD5D87 /* RequestInterceptor.swift in Sources */,
+				315A4C57241EF28B00D57C7A /* StringEncoding+Alamofire.swift in Sources */,
 				4C43669C1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */,
 				4C811F8E1B51856D00E0F59A /* ServerTrustEvaluation.swift in Sources */,
 				311B199120B0E3470036823B /* MultipartUpload.swift in Sources */,
@@ -1402,6 +1410,7 @@
 				4C0CB644220CA89400604EDC /* URLRequest+Alamofire.swift in Sources */,
 				4C256A0921EEB69000AD5D87 /* RequestInterceptor.swift in Sources */,
 				4C43669E1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */,
+				315A4C59241EF28B00D57C7A /* StringEncoding+Alamofire.swift in Sources */,
 				E4202FD41B667AA100C997FB /* Alamofire.swift in Sources */,
 				311B199320B0E3480036823B /* MultipartUpload.swift in Sources */,
 				4C4466EE21F8F5D800AC9703 /* CachedResponseHandler.swift in Sources */,
@@ -1441,6 +1450,7 @@
 				4C0CB641220CA89400604EDC /* URLRequest+Alamofire.swift in Sources */,
 				4C811F8D1B51856D00E0F59A /* ServerTrustEvaluation.swift in Sources */,
 				4C256A0621EEB69000AD5D87 /* RequestInterceptor.swift in Sources */,
+				315A4C56241EF28B00D57C7A /* StringEncoding+Alamofire.swift in Sources */,
 				4C43669B1D7BB93D00C38AAD /* DispatchQueue+Alamofire.swift in Sources */,
 				4C3D00541C66A63000D1F709 /* NetworkReachabilityManager.swift in Sources */,
 				311B199020B0D3B40036823B /* MultipartUpload.swift in Sources */,

+ 34 - 37
Source/HTTPHeaders.swift

@@ -367,45 +367,42 @@ extension HTTPHeader {
     ///
     /// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0`
     public static let defaultUserAgent: HTTPHeader = {
-        let userAgent: String = {
-            if let info = Bundle.main.infoDictionary {
-                let executable = info[kCFBundleExecutableKey as String] as? String ?? "Unknown"
-                let bundle = info[kCFBundleIdentifierKey as String] as? String ?? "Unknown"
-                let appVersion = info["CFBundleShortVersionString"] as? String ?? "Unknown"
-                let appBuild = info[kCFBundleVersionKey as String] as? String ?? "Unknown"
-
-                let osNameVersion: String = {
-                    let version = ProcessInfo.processInfo.operatingSystemVersion
-                    let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
-                    // swiftformat:disable indent
-                    let osName: String = {
-                    #if os(iOS)
-                        return "iOS"
-                    #elseif os(watchOS)
-                        return "watchOS"
-                    #elseif os(tvOS)
-                        return "tvOS"
-                    #elseif os(macOS)
-                        return "macOS"
-                    #elseif os(Linux)
-                        return "Linux"
-                    #else
-                        return "Unknown"
-                    #endif
-                    }()
-                    // swiftformat:enable indent
-
-                    return "\(osName) \(versionString)"
-                }()
-
-                let alamofireVersion = "Alamofire/\(version)"
-
-                return "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)"
-            }
-
-            return "Alamofire"
+        let info = Bundle.main.infoDictionary
+        let executable = (info?[kCFBundleExecutableKey as String] as? String) ??
+            (ProcessInfo.processInfo.arguments.first?.split(separator: "/").last.map(String.init)) ??
+            "Unknown"
+        let bundle = info?[kCFBundleIdentifierKey as String] as? String ?? "Unknown"
+        let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown"
+        let appBuild = info?[kCFBundleVersionKey as String] as? String ?? "Unknown"
+
+        let osNameVersion: String = {
+            let version = ProcessInfo.processInfo.operatingSystemVersion
+            let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
+            let osName: String = {
+                #if os(iOS)
+                return "iOS"
+                #elseif os(watchOS)
+                return "watchOS"
+                #elseif os(tvOS)
+                return "tvOS"
+                #elseif os(macOS)
+                return "macOS"
+                #elseif os(Linux)
+                return "Linux"
+                #elseif os(Windows)
+                return "Windows"
+                #else
+                return "Unknown"
+                #endif
+            }()
+
+            return "\(osName) \(versionString)"
         }()
 
+        let alamofireVersion = "Alamofire/\(version)"
+
+        let userAgent = "\(executable)/\(appVersion) (\(bundle); build:\(appBuild); \(osNameVersion)) \(alamofireVersion)"
+
         return .userAgent(userAgent)
     }()
 }

+ 4 - 1
Source/MultipartFormData.swift

@@ -55,7 +55,10 @@ open class MultipartFormData {
         }
 
         static func randomBoundary() -> String {
-            String(format: "alamofire.boundary.%08x%08x", arc4random(), arc4random())
+            let first = UInt32.random(in: UInt32.min...UInt32.max)
+            let second = UInt32.random(in: UInt32.min...UInt32.max)
+
+            return String(format: "alamofire.boundary.%08x%08x", first, second)
         }
 
         static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {

+ 5 - 1
Source/ParameterEncoding.swift

@@ -310,5 +310,9 @@ public struct JSONEncoding: ParameterEncoding {
 // MARK: -
 
 extension NSNumber {
-    fileprivate var isBool: Bool { CFBooleanGetTypeID() == CFGetTypeID(self) }
+    fileprivate var isBool: Bool {
+        // Use Obj-C type encoding to check whether the underlying type is a `Bool`, as it's guaranteed as part of
+        // swift-corelibs-foundation, per [this discussion on the Swift forums](https://forums.swift.org/t/alamofire-on-linux-possible-but-not-release-ready/34553/22).
+        String(cString: objCType) == "c"
+    }
 }

+ 66 - 22
Source/Protected.swift

@@ -24,10 +24,67 @@
 
 import Foundation
 
-// MARK: -
+private protocol Lock {
+    func lock()
+    func unlock()
+}
+
+extension Lock {
+    /// Executes a closure returning a value while acquiring the lock.
+    ///
+    /// - Parameter closure: The closure to run.
+    ///
+    /// - Returns:           The value the closure generated.
+    func around<T>(_ closure: () -> T) -> T {
+        lock(); defer { unlock() }
+        return closure()
+    }
+
+    /// Execute a closure while acquiring the lock.
+    ///
+    /// - Parameter closure: The closure to run.
+    func around(_ closure: () -> Void) {
+        lock(); defer { unlock() }
+        closure()
+    }
+}
+
+#if os(Linux)
+/// A `pthread_mutex_t` wrapper.
+final class MutexLock: Lock {
+    private var mutex: UnsafeMutablePointer<pthread_mutex_t>
 
+    init() {
+        mutex = .allocate(capacity: 1)
+
+        var attr = pthread_mutexattr_t()
+        pthread_mutexattr_init(&attr)
+        pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK))
+
+        let error = pthread_mutex_init(mutex, &attr)
+        precondition(error == 0, "Failed to create pthread_mutex")
+    }
+
+    deinit {
+        let error = pthread_mutex_destroy(mutex)
+        precondition(error == 0, "Failed to destroy pthread_mutex")
+    }
+
+    fileprivate func lock() {
+        let error = pthread_mutex_lock(mutex)
+        precondition(error == 0, "Failed to lock pthread_mutex")
+    }
+
+    fileprivate func unlock() {
+        let error = pthread_mutex_unlock(mutex)
+        precondition(error == 0, "Failed to unlock pthread_mutex")
+    }
+}
+#endif
+
+#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
 /// An `os_unfair_lock` wrapper.
-final class UnfairLock {
+final class UnfairLock: Lock {
     private let unfairLock: os_unfair_lock_t
 
     init() {
@@ -40,38 +97,25 @@ final class UnfairLock {
         unfairLock.deallocate()
     }
 
-    private func lock() {
+    fileprivate func lock() {
         os_unfair_lock_lock(unfairLock)
     }
 
-    private func unlock() {
+    fileprivate func unlock() {
         os_unfair_lock_unlock(unfairLock)
     }
-
-    /// Executes a closure returning a value while acquiring the lock.
-    ///
-    /// - Parameter closure: The closure to run.
-    ///
-    /// - Returns:           The value the closure generated.
-    func around<T>(_ closure: () -> T) -> T {
-        lock(); defer { unlock() }
-        return closure()
-    }
-
-    /// Execute a closure while acquiring the lock.
-    ///
-    /// - Parameter closure: The closure to run.
-    func around(_ closure: () -> Void) {
-        lock(); defer { unlock() }
-        return closure()
-    }
 }
+#endif
 
 /// A thread-safe wrapper around a value.
 @propertyWrapper
 @dynamicMemberLookup
 final class Protected<T> {
+    #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
     private let lock = UnfairLock()
+    #elseif os(Linux)
+    private let lock = MutexLock()
+    #endif
     private var value: T
 
     init(_ value: T) {

+ 3 - 1
Source/RequestTaskMap.swift

@@ -26,9 +26,11 @@ import Foundation
 
 /// A type that maintains a two way, one to one map of `URLSessionTask`s to `Request`s.
 struct RequestTaskMap {
+    private typealias Events = (completed: Bool, metricsGathered: Bool)
+
     private var tasksToRequests: [URLSessionTask: Request]
     private var requestsToTasks: [Request: URLSessionTask]
-    private var taskEvents: [URLSessionTask: (completed: Bool, metricsGathered: Bool)]
+    private var taskEvents: [URLSessionTask: Events]
 
     var requests: [Request] {
         Array(tasksToRequests.values)

+ 6 - 8
Source/ResponseSerialization.swift

@@ -214,7 +214,7 @@ extension DataRequest {
         -> Self {
         appendResponseSerializer {
             // Start work that should be on the serialization queue.
-            let start = CFAbsoluteTimeGetCurrent()
+            let start = ProcessInfo.processInfo.systemUptime
             let result: AFResult<Serializer.SerializedObject> = Result {
                 try responseSerializer.serialize(request: self.request,
                                                  response: self.response,
@@ -224,7 +224,7 @@ extension DataRequest {
                 error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error)))
             }
 
-            let end = CFAbsoluteTimeGetCurrent()
+            let end = ProcessInfo.processInfo.systemUptime
             // End work that should be on the serialization queue.
 
             self.underlyingQueue.async {
@@ -329,7 +329,7 @@ extension DownloadRequest {
         -> Self {
         appendResponseSerializer {
             // Start work that should be on the serialization queue.
-            let start = CFAbsoluteTimeGetCurrent()
+            let start = ProcessInfo.processInfo.systemUptime
             let result: AFResult<Serializer.SerializedObject> = Result {
                 try responseSerializer.serializeDownload(request: self.request,
                                                          response: self.response,
@@ -338,7 +338,7 @@ extension DownloadRequest {
             }.mapError { error in
                 error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error)))
             }
-            let end = CFAbsoluteTimeGetCurrent()
+            let end = ProcessInfo.processInfo.systemUptime
             // End work that should be on the serialization queue.
 
             self.underlyingQueue.async {
@@ -516,10 +516,8 @@ public final class StringResponseSerializer: ResponseSerializer {
 
         var convertedEncoding = encoding
 
-        if let encodingName = response?.textEncodingName as CFString?, convertedEncoding == nil {
-            let ianaCharSet = CFStringConvertIANACharSetNameToEncoding(encodingName)
-            let nsStringEncoding = CFStringConvertEncodingToNSStringEncoding(ianaCharSet)
-            convertedEncoding = String.Encoding(rawValue: nsStringEncoding)
+        if let encodingName = response?.textEncodingName, convertedEncoding == nil {
+            convertedEncoding = String.Encoding(ianaCharsetName: encodingName)
         }
 
         let actualEncoding = convertedEncoding ?? .isoLatin1

+ 55 - 0
Source/StringEncoding+Alamofire.swift

@@ -0,0 +1,55 @@
+//
+//  StringEncoding+Alamofire.swift
+//
+//  Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/)
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+//
+
+import Foundation
+
+extension String.Encoding {
+    /// Creates an encoding from the IANA charset name.
+    ///
+    /// - Notes: These mappings match those [provided by CoreFoundation](https://opensource.apple.com/source/CF/CF-476.18/CFStringUtilities.c.auto.html)
+    ///
+    /// - Parameter name: IANA charset name.
+    init?(ianaCharsetName name: String) {
+        switch name.lowercased() {
+        case "utf-8":
+            self = .utf8
+        case "iso-8859-1":
+            self = .isoLatin1
+        case "unicode-1-1", "iso-10646-ucs-2", "utf-16":
+            self = .utf16
+        case "utf-16be":
+            self = .utf16BigEndian
+        case "utf-16le":
+            self = .utf16LittleEndian
+        case "utf-32":
+            self = .utf32
+        case "utf-32be":
+            self = .utf32BigEndian
+        case "utf-32le":
+            self = .utf32LittleEndian
+        default:
+            return nil
+        }
+    }
+}

+ 2 - 9
Tests/SessionTests.swift

@@ -292,18 +292,11 @@ final class SessionTestCase: BaseTestCase {
             return "\(osName) \(versionString)"
         }()
 
-        let alamofireVersion: String = {
-            guard
-                let afInfo = Bundle(for: Session.self).infoDictionary,
-                let build = afInfo["CFBundleShortVersionString"]
-            else { return "Unknown" }
-
-            return "Alamofire/\(build)"
-        }()
+        let alamofireVersion = "Alamofire/\(Alamofire.version)"
 
         XCTAssertTrue(userAgent?.contains(alamofireVersion) == true)
         XCTAssertTrue(userAgent?.contains(osNameVersion) == true)
-        XCTAssertTrue(userAgent?.contains("Unknown/Unknown") == true)
+        XCTAssertTrue(userAgent?.contains("xctest/Unknown") == true)
     }
 
     // MARK: Tests - Supported Accept-Encodings