| // Foundation/URLSession/URLSessionTask.swift - URLSession API |
| // |
| // This source file is part of the Swift.org open source project |
| // |
| // Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors |
| // Licensed under Apache License v2.0 with Runtime Library Exception |
| // |
| // See http://swift.org/LICENSE.txt for license information |
| // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| // |
| // ----------------------------------------------------------------------------- |
| /// |
| /// URLSession API code. |
| /// - SeeAlso: URLSession.swift |
| /// |
| // ----------------------------------------------------------------------------- |
| |
| |
| |
| |
| import CoreFoundation |
| import Dispatch |
| |
| |
| /// A cancelable object that refers to the lifetime |
| /// of processing a given request. |
| open class URLSessionTask : NSObject, NSCopying { |
| /// How many times the task has been suspended, 0 indicating a running task. |
| internal var suspendCount = 1 |
| internal var session: URLSessionProtocol! //change to nil when task completes |
| internal let body: _Body |
| fileprivate var _protocol: URLProtocol? = nil |
| |
| /// All operations must run on this queue. |
| internal let workQueue: DispatchQueue |
| /// Using dispatch semaphore to make public attributes thread safe. |
| /// A semaphore is a simpler option against the usage of concurrent queue |
| /// as the critical sections are very short. |
| fileprivate let semaphore = DispatchSemaphore(value: 1) |
| |
| public override init() { |
| // Darwin Foundation oddly allows calling this initializer, even though |
| // such a task is quite broken -- it doesn't have a session. And calling |
| // e.g. `taskIdentifier` will crash. |
| // |
| // We set up the bare minimum for init to work, but don't care too much |
| // about things crashing later. |
| session = _MissingURLSession() |
| taskIdentifier = 0 |
| originalRequest = nil |
| body = .none |
| workQueue = DispatchQueue(label: "URLSessionTask.notused.0") |
| super.init() |
| } |
| /// Create a data task. If there is a httpBody in the URLRequest, use that as a parameter |
| internal convenience init(session: URLSession, request: URLRequest, taskIdentifier: Int) { |
| if let bodyData = request.httpBody { |
| self.init(session: session, request: request, taskIdentifier: taskIdentifier, body: _Body.data(createDispatchData(bodyData))) |
| } else { |
| self.init(session: session, request: request, taskIdentifier: taskIdentifier, body: .none) |
| } |
| } |
| internal init(session: URLSession, request: URLRequest, taskIdentifier: Int, body: _Body) { |
| self.session = session |
| self.workQueue = session.workQueue |
| self.taskIdentifier = taskIdentifier |
| self.originalRequest = request |
| self.body = body |
| super.init() |
| if session.configuration.protocolClasses != nil { |
| guard let protocolClasses = session.configuration.protocolClasses else { fatalError() } |
| if let urlProtocolClass = URLProtocol.getProtocolClass(protocols: protocolClasses, request: request) { |
| guard let urlProtocol = urlProtocolClass as? URLProtocol.Type else { fatalError() } |
| self._protocol = urlProtocol.init(task: self, cachedResponse: nil, client: nil) |
| } else { |
| guard let protocolClasses = URLProtocol.getProtocols() else { fatalError() } |
| if let urlProtocolClass = URLProtocol.getProtocolClass(protocols: protocolClasses, request: request) { |
| guard let urlProtocol = urlProtocolClass as? URLProtocol.Type else { fatalError() } |
| self._protocol = urlProtocol.init(task: self, cachedResponse: nil, client: nil) |
| } |
| } |
| } else { |
| guard let protocolClasses = URLProtocol.getProtocols() else { fatalError() } |
| if let urlProtocolClass = URLProtocol.getProtocolClass(protocols: protocolClasses, request: request) { |
| guard let urlProtocol = urlProtocolClass as? URLProtocol.Type else { fatalError() } |
| self._protocol = urlProtocol.init(task: self, cachedResponse: nil, client: nil) |
| } |
| } |
| } |
| deinit { |
| //TODO: Do we remove the EasyHandle from the session here? This might run on the wrong thread / queue. |
| } |
| |
| open override func copy() -> Any { |
| return copy(with: nil) |
| } |
| |
| open func copy(with zone: NSZone?) -> Any { |
| return self |
| } |
| |
| /// An identifier for this task, assigned by and unique to the owning session |
| open let taskIdentifier: Int |
| |
| /// May be nil if this is a stream task |
| /*@NSCopying*/ open let originalRequest: URLRequest? |
| |
| /// May differ from originalRequest due to http server redirection |
| /*@NSCopying*/ open internal(set) var currentRequest: URLRequest? { |
| get { |
| semaphore.wait() |
| defer { |
| semaphore.signal() |
| } |
| return self._currentRequest |
| } |
| set { |
| semaphore.wait() |
| self._currentRequest = newValue |
| semaphore.signal() |
| } |
| } |
| fileprivate var _currentRequest: URLRequest? = nil |
| /*@NSCopying*/ open internal(set) var response: URLResponse? { |
| get { |
| semaphore.wait() |
| defer { |
| semaphore.signal() |
| } |
| return self._response |
| } |
| set { |
| semaphore.wait() |
| self._response = newValue |
| semaphore.signal() |
| } |
| } |
| fileprivate var _response: URLResponse? = nil |
| |
| /* Byte count properties may be zero if no body is expected, |
| * or URLSessionTransferSizeUnknown if it is not possible |
| * to know how many bytes will be transferred. |
| */ |
| |
| /// Number of body bytes already received |
| open internal(set) var countOfBytesReceived: Int64 { |
| get { |
| semaphore.wait() |
| defer { |
| semaphore.signal() |
| } |
| return self._countOfBytesReceived |
| } |
| set { |
| semaphore.wait() |
| self._countOfBytesReceived = newValue |
| semaphore.signal() |
| } |
| } |
| fileprivate var _countOfBytesReceived: Int64 = 0 |
| |
| /// Number of body bytes already sent */ |
| open internal(set) var countOfBytesSent: Int64 { |
| get { |
| semaphore.wait() |
| defer { |
| semaphore.signal() |
| } |
| return self._countOfBytesSent |
| } |
| set { |
| semaphore.wait() |
| self._countOfBytesSent = newValue |
| semaphore.signal() |
| } |
| } |
| |
| fileprivate var _countOfBytesSent: Int64 = 0 |
| |
| /// Number of body bytes we expect to send, derived from the Content-Length of the HTTP request */ |
| open internal(set) var countOfBytesExpectedToSend: Int64 = 0 |
| |
| /// Number of byte bytes we expect to receive, usually derived from the Content-Length header of an HTTP response. */ |
| open internal(set) var countOfBytesExpectedToReceive: Int64 = 0 |
| |
| /// The taskDescription property is available for the developer to |
| /// provide a descriptive label for the task. |
| open var taskDescription: String? |
| |
| /* -cancel returns immediately, but marks a task as being canceled. |
| * The task will signal -URLSession:task:didCompleteWithError: with an |
| * error value of { NSURLErrorDomain, NSURLErrorCancelled }. In some |
| * cases, the task may signal other work before it acknowledges the |
| * cancelation. -cancel may be sent to a task that has been suspended. |
| */ |
| open func cancel() { |
| workQueue.sync { |
| guard self.state == .running || self.state == .suspended else { return } |
| self.state = .canceling |
| self.workQueue.async { |
| let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled, userInfo: nil)) |
| self.error = urlError |
| self._protocol?.stopLoading() |
| self._protocol?.client?.urlProtocol(self._protocol!, didFailWithError: urlError) |
| } |
| } |
| } |
| |
| /* |
| * The current state of the task within the session. |
| */ |
| open var state: URLSessionTask.State { |
| get { |
| semaphore.wait() |
| defer { |
| semaphore.signal() |
| } |
| return self._state |
| } |
| set { |
| semaphore.wait() |
| self._state = newValue |
| semaphore.signal() |
| } |
| } |
| fileprivate var _state: URLSessionTask.State = .suspended |
| |
| /* |
| * The error, if any, delivered via -URLSession:task:didCompleteWithError: |
| * This property will be nil in the event that no error occured. |
| */ |
| /*@NSCopying*/ open internal(set) var error: Error? |
| |
| /// Suspend the task. |
| /// |
| /// Suspending a task will prevent the URLSession from continuing to |
| /// load data. There may still be delegate calls made on behalf of |
| /// this task (for instance, to report data received while suspending) |
| /// but no further transmissions will be made on behalf of the task |
| /// until -resume is sent. The timeout timer associated with the task |
| /// will be disabled while a task is suspended. -suspend and -resume are |
| /// nestable. |
| open func suspend() { |
| // suspend / resume is implemented simply by adding / removing the task's |
| // easy handle fromt he session's multi-handle. |
| // |
| // This might result in slightly different behaviour than the Darwin Foundation |
| // implementation, but it'll be difficult to get complete parity anyhow. |
| // Too many things depend on timeout on the wire etc. |
| // |
| // TODO: It may be worth looking into starting over a task that gets |
| // resumed. The Darwin Foundation documentation states that that's what |
| // it does for anything but download tasks. |
| |
| // We perform the increment and call to `updateTaskState()` |
| // synchronous, to make sure the `state` is updated when this method |
| // returns, but the actual suspend will be done asynchronous to avoid |
| // dead-locks. |
| workQueue.sync { |
| self.suspendCount += 1 |
| guard self.suspendCount < Int.max else { fatalError("Task suspended too many times \(Int.max).") } |
| self.updateTaskState() |
| |
| if self.suspendCount == 1 { |
| self.workQueue.async { |
| self._protocol?.stopLoading() |
| } |
| } |
| } |
| } |
| /// Resume the task. |
| /// |
| /// - SeeAlso: `suspend()` |
| open func resume() { |
| workQueue.sync { |
| self.suspendCount -= 1 |
| guard 0 <= self.suspendCount else { fatalError("Resuming a task that's not suspended. Calls to resume() / suspend() need to be matched.") } |
| self.updateTaskState() |
| if self.suspendCount == 0 { |
| self.workQueue.async { |
| if let _protocol = self._protocol { |
| _protocol.startLoading() |
| } |
| else if self.error == nil { |
| var userInfo: [String: Any] = [NSLocalizedDescriptionKey: "unsupported URL"] |
| if let url = self.originalRequest?.url { |
| userInfo[NSURLErrorFailingURLErrorKey] = url |
| userInfo[NSURLErrorFailingURLStringErrorKey] = url.absoluteString |
| } |
| let urlError = URLError(_nsError: NSError(domain: NSURLErrorDomain, |
| code: NSURLErrorUnsupportedURL, |
| userInfo: userInfo)) |
| self.error = urlError |
| _ProtocolClient().urlProtocol(task: self, didFailWithError: urlError) |
| } |
| } |
| } |
| } |
| } |
| |
| /// The priority of the task. |
| /// |
| /// Sets a scaling factor for the priority of the task. The scaling factor is a |
| /// value between 0.0 and 1.0 (inclusive), where 0.0 is considered the lowest |
| /// priority and 1.0 is considered the highest. |
| /// |
| /// The priority is a hint and not a hard requirement of task performance. The |
| /// priority of a task may be changed using this API at any time, but not all |
| /// protocols support this; in these cases, the last priority that took effect |
| /// will be used. |
| /// |
| /// If no priority is specified, the task will operate with the default priority |
| /// as defined by the constant URLSessionTask.defaultPriority. Two additional |
| /// priority levels are provided: URLSessionTask.lowPriority and |
| /// URLSessionTask.highPriority, but use is not restricted to these. |
| open var priority: Float { |
| get { |
| semaphore.wait() |
| defer { |
| semaphore.signal() |
| } |
| return self._priority |
| } |
| set { |
| semaphore.wait() |
| self._priority = newValue |
| semaphore.signal() |
| } |
| } |
| fileprivate var _priority: Float = URLSessionTask.defaultPriority |
| } |
| |
| extension URLSessionTask { |
| public enum State : Int { |
| /// The task is currently being serviced by the session |
| case running |
| case suspended |
| /// The task has been told to cancel. The session will receive a URLSession:task:didCompleteWithError: message. |
| case canceling |
| /// The task has completed and the session will receive no more delegate notifications |
| case completed |
| } |
| } |
| |
| internal extension URLSessionTask { |
| /// Updates the (public) state based on private / internal state. |
| /// |
| /// - Note: This must be called on the `workQueue`. |
| internal func updateTaskState() { |
| func calculateState() -> URLSessionTask.State { |
| if suspendCount == 0 { |
| return .running |
| } else { |
| return .suspended |
| } |
| } |
| state = calculateState() |
| } |
| } |
| |
| internal extension URLSessionTask { |
| enum _Body { |
| case none |
| case data(DispatchData) |
| /// Body data is read from the given file URL |
| case file(URL) |
| case stream(InputStream) |
| } |
| } |
| internal extension URLSessionTask._Body { |
| enum _Error : Error { |
| case fileForBodyDataNotFound |
| } |
| /// - Returns: The body length, or `nil` for no body (e.g. `GET` request). |
| func getBodyLength() throws -> UInt64? { |
| switch self { |
| case .none: |
| return 0 |
| case .data(let d): |
| return UInt64(d.count) |
| /// Body data is read from the given file URL |
| case .file(let fileURL): |
| guard let s = try FileManager.default.attributesOfItem(atPath: fileURL.path)[.size] as? NSNumber else { |
| throw _Error.fileForBodyDataNotFound |
| } |
| return s.uint64Value |
| case .stream: |
| return nil |
| } |
| } |
| } |
| |
| |
| fileprivate func errorCode(fileSystemError error: Error) -> Int { |
| func fromCocoaErrorCode(_ code: Int) -> Int { |
| switch code { |
| case CocoaError.fileReadNoSuchFile.rawValue: |
| return NSURLErrorFileDoesNotExist |
| case CocoaError.fileReadNoPermission.rawValue: |
| return NSURLErrorNoPermissionsToReadFile |
| default: |
| return NSURLErrorUnknown |
| } |
| } |
| switch error { |
| case let e as NSError where e.domain == NSCocoaErrorDomain: |
| return fromCocoaErrorCode(e.code) |
| default: |
| return NSURLErrorUnknown |
| } |
| } |
| |
| public extension URLSessionTask { |
| /// The default URL session task priority, used implicitly for any task you |
| /// have not prioritized. The floating point value of this constant is 0.5. |
| public static let defaultPriority: Float = 0.5 |
| |
| /// A low URL session task priority, with a floating point value above the |
| /// minimum of 0 and below the default value. |
| public static let lowPriority: Float = 0.25 |
| |
| /// A high URL session task priority, with a floating point value above the |
| /// default value and below the maximum of 1.0. |
| public static let highPriority: Float = 0.75 |
| } |
| |
| /* |
| * An URLSessionDataTask does not provide any additional |
| * functionality over an URLSessionTask and its presence is merely |
| * to provide lexical differentiation from download and upload tasks. |
| */ |
| open class URLSessionDataTask : URLSessionTask { |
| } |
| |
| /* |
| * An URLSessionUploadTask does not currently provide any additional |
| * functionality over an URLSessionDataTask. All delegate messages |
| * that may be sent referencing an URLSessionDataTask equally apply |
| * to URLSessionUploadTasks. |
| */ |
| open class URLSessionUploadTask : URLSessionDataTask { |
| } |
| |
| /* |
| * URLSessionDownloadTask is a task that represents a download to |
| * local storage. |
| */ |
| open class URLSessionDownloadTask : URLSessionTask { |
| |
| internal var fileLength = -1.0 |
| |
| /* Cancel the download (and calls the superclass -cancel). If |
| * conditions will allow for resuming the download in the future, the |
| * callback will be called with an opaque data blob, which may be used |
| * with -downloadTaskWithResumeData: to attempt to resume the download. |
| * If resume data cannot be created, the completion handler will be |
| * called with nil resumeData. |
| */ |
| open func cancel(byProducingResumeData completionHandler: @escaping (Data?) -> Void) { NSUnimplemented() } |
| } |
| |
| /* |
| * An URLSessionStreamTask provides an interface to perform reads |
| * and writes to a TCP/IP stream created via URLSession. This task |
| * may be explicitly created from an URLSession, or created as a |
| * result of the appropriate disposition response to a |
| * -URLSession:dataTask:didReceiveResponse: delegate message. |
| * |
| * URLSessionStreamTask can be used to perform asynchronous reads |
| * and writes. Reads and writes are enquened and executed serially, |
| * with the completion handler being invoked on the sessions delegate |
| * queuee. If an error occurs, or the task is canceled, all |
| * outstanding read and write calls will have their completion |
| * handlers invoked with an appropriate error. |
| * |
| * It is also possible to create InputStream and OutputStream |
| * instances from an URLSessionTask by sending |
| * -captureStreams to the task. All outstanding read and writess are |
| * completed before the streams are created. Once the streams are |
| * delivered to the session delegate, the task is considered complete |
| * and will receive no more messsages. These streams are |
| * disassociated from the underlying session. |
| */ |
| |
| open class URLSessionStreamTask : URLSessionTask { |
| |
| /* Read minBytes, or at most maxBytes bytes and invoke the completion |
| * handler on the sessions delegate queue with the data or an error. |
| * If an error occurs, any outstanding reads will also fail, and new |
| * read requests will error out immediately. |
| */ |
| open func readData(ofMinLength minBytes: Int, maxLength maxBytes: Int, timeout: TimeInterval, completionHandler: @escaping (Data?, Bool, Error?) -> Void) { NSUnimplemented() } |
| |
| /* Write the data completely to the underlying socket. If all the |
| * bytes have not been written by the timeout, a timeout error will |
| * occur. Note that invocation of the completion handler does not |
| * guarantee that the remote side has received all the bytes, only |
| * that they have been written to the kernel. */ |
| open func write(_ data: Data, timeout: TimeInterval, completionHandler: @escaping (Error?) -> Void) { NSUnimplemented() } |
| |
| /* -captureStreams completes any already enqueued reads |
| * and writes, and then invokes the |
| * URLSession:streamTask:didBecomeInputStream:outputStream: delegate |
| * message. When that message is received, the task object is |
| * considered completed and will not receive any more delegate |
| * messages. */ |
| open func captureStreams() { NSUnimplemented() } |
| |
| /* Enqueue a request to close the write end of the underlying socket. |
| * All outstanding IO will complete before the write side of the |
| * socket is closed. The server, however, may continue to write bytes |
| * back to the client, so best practice is to continue reading from |
| * the server until you receive EOF. |
| */ |
| open func closeWrite() { NSUnimplemented() } |
| |
| /* Enqueue a request to close the read side of the underlying socket. |
| * All outstanding IO will complete before the read side is closed. |
| * You may continue writing to the server. |
| */ |
| open func closeRead() { NSUnimplemented() } |
| |
| /* |
| * Begin encrypted handshake. The hanshake begins after all pending |
| * IO has completed. TLS authentication callbacks are sent to the |
| * session's -URLSession:task:didReceiveChallenge:completionHandler: |
| */ |
| open func startSecureConnection() { NSUnimplemented() } |
| |
| /* |
| * Cleanly close a secure connection after all pending secure IO has |
| * completed. |
| */ |
| open func stopSecureConnection() { NSUnimplemented() } |
| } |
| |
| /* Key in the userInfo dictionary of an NSError received during a failed download. */ |
| public let URLSessionDownloadTaskResumeData: String = "NSURLSessionDownloadTaskResumeData" |
| |
| extension _ProtocolClient : URLProtocolClient { |
| |
| func urlProtocol(_ protocol: URLProtocol, didReceive response: URLResponse, cacheStoragePolicy policy: URLCache.StoragePolicy) { |
| guard let task = `protocol`.task else { fatalError("Received response, but there's no task.") } |
| task.response = response |
| let session = task.session as! URLSession |
| guard let dataTask = task as? URLSessionDataTask else { return } |
| switch session.behaviour(for: task) { |
| case .taskDelegate(let delegate as URLSessionDataDelegate): |
| session.delegateQueue.addOperation { |
| delegate.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: { _ in |
| URLSession.printDebug("warning: Ignoring disposition from completion handler.") |
| }) |
| } |
| case .noDelegate, .taskDelegate, .dataCompletionHandler, .downloadCompletionHandler: |
| break |
| } |
| } |
| |
| func urlProtocolDidFinishLoading(_ protocol: URLProtocol) { |
| guard let task = `protocol`.task else { fatalError() } |
| guard let session = task.session as? URLSession else { fatalError() } |
| switch session.behaviour(for: task) { |
| case .taskDelegate(let delegate): |
| if let downloadDelegate = delegate as? URLSessionDownloadDelegate, let downloadTask = task as? URLSessionDownloadTask { |
| session.delegateQueue.addOperation { |
| downloadDelegate.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: `protocol`.properties[URLProtocol._PropertyKey.temporaryFileURL] as! URL) |
| } |
| } |
| session.delegateQueue.addOperation { |
| delegate.urlSession(session, task: task, didCompleteWithError: nil) |
| task.state = .completed |
| session.taskRegistry.remove(task) |
| } |
| case .noDelegate: |
| task.state = .completed |
| session.taskRegistry.remove(task) |
| case .dataCompletionHandler(let completion): |
| session.delegateQueue.addOperation { |
| completion(`protocol`.properties[URLProtocol._PropertyKey.responseData] as? Data ?? Data(), task.response, nil) |
| task.state = .completed |
| session.taskRegistry.remove(task) |
| } |
| case .downloadCompletionHandler(let completion): |
| session.delegateQueue.addOperation { |
| completion(`protocol`.properties[URLProtocol._PropertyKey.temporaryFileURL] as? URL, task.response, nil) |
| task.state = .completed |
| session.taskRegistry.remove(task) |
| } |
| } |
| task._protocol = nil |
| } |
| |
| func urlProtocol(_ protocol: URLProtocol, didCancel challenge: URLAuthenticationChallenge) { |
| NSUnimplemented() |
| } |
| |
| func urlProtocol(_ protocol: URLProtocol, didReceive challenge: URLAuthenticationChallenge) { |
| NSUnimplemented() |
| } |
| |
| func urlProtocol(_ protocol: URLProtocol, didLoad data: Data) { |
| `protocol`.properties[.responseData] = data |
| guard let task = `protocol`.task else { fatalError() } |
| guard let session = task.session as? URLSession else { fatalError() } |
| switch session.behaviour(for: task) { |
| case .taskDelegate(let delegate): |
| let dataDelegate = delegate as? URLSessionDataDelegate |
| let dataTask = task as? URLSessionDataTask |
| session.delegateQueue.addOperation { |
| dataDelegate?.urlSession(session, dataTask: dataTask!, didReceive: data) |
| } |
| default: return |
| } |
| } |
| |
| func urlProtocol(_ protocol: URLProtocol, didFailWithError error: Error) { |
| guard let task = `protocol`.task else { fatalError() } |
| urlProtocol(task: task, didFailWithError: error) |
| } |
| |
| func urlProtocol(task: URLSessionTask, didFailWithError error: Error) { |
| guard let session = task.session as? URLSession else { fatalError() } |
| switch session.behaviour(for: task) { |
| case .taskDelegate(let delegate): |
| session.delegateQueue.addOperation { |
| delegate.urlSession(session, task: task, didCompleteWithError: error as Error) |
| task.state = .completed |
| session.taskRegistry.remove(task) |
| } |
| case .noDelegate: |
| task.state = .completed |
| session.taskRegistry.remove(task) |
| case .dataCompletionHandler(let completion): |
| session.delegateQueue.addOperation { |
| completion(nil, nil, error) |
| task.state = .completed |
| session.taskRegistry.remove(task) |
| } |
| case .downloadCompletionHandler(let completion): |
| session.delegateQueue.addOperation { |
| completion(nil, nil, error) |
| task.state = .completed |
| session.taskRegistry.remove(task) |
| } |
| } |
| task._protocol = nil |
| } |
| |
| func urlProtocol(_ protocol: URLProtocol, cachedResponseIsValid cachedResponse: CachedURLResponse) { |
| NSUnimplemented() |
| } |
| |
| func urlProtocol(_ protocol: URLProtocol, wasRedirectedTo request: URLRequest, redirectResponse: URLResponse) { |
| NSUnimplemented() |
| } |
| } |
| |
| extension URLProtocol { |
| enum _PropertyKey: String { |
| case responseData |
| case temporaryFileURL |
| } |
| } |