QuickDataTask.swift (10741B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 * @author codex gpt5.6-sol 8 */ 9 import Foundation 10 //import "Foundation/NSURLError.h" 11 import os.log 12 13 import FTalerWalletcore 14 15 // will be called from wallet-core for networking 16 func request_create(userdata: Optional<UnsafeMutableRawPointer>, 17 requestInfo: Optional<UnsafeMutablePointer<JSHttpRequestInfo>>) -> Int32 { 18 let quickjs = Unmanaged<Quickjs>.fromOpaque(userdata!).takeUnretainedValue() 19 20 if let requestInfo { 21 if let url = URL(string: String(cString: requestInfo.pointee.url)) { 22 let responseCb = requestInfo.pointee.response_cb 23 let responseCbCls = requestInfo.pointee.response_cb_cls 24 let method = String(cString: requestInfo.pointee.method) 25 let requestHeaders = requestInfo.pointee.request_headers 26 let redirect = requestInfo.pointee.redirect // TODO: redirect 27 let timeoutMs = requestInfo.pointee.timeout_ms 28 let debug = requestInfo.pointee.debug // TODO: debug 29 let reqBody = requestInfo.pointee.req_body 30 let bodyLen = requestInfo.pointee.req_body_len 31 32 var request = URLRequest(url: url) 33 request.httpMethod = method 34 switch timeoutMs { 35 case 0: // leave URLSession's default 36 break 37 case ..<0: // "no timeout" 38 request.timeoutInterval = 60 * 60 * 24 39 default: 40 request.timeoutInterval = TimeInterval(timeoutMs) / 1000.0 41 } 42 if let reqBody, bodyLen > 0 { // qtart frees req_body as soon as request_create returns, 43 request.httpBody = Data(bytes: reqBody, count: Int(bodyLen)) // thus we must copy it 44 } 45 if var ptr = requestHeaders { 46 while let cString = ptr.pointee { 47 let string = String(cString: cString) 48 if let index = string.firstIndex(of: ":") { 49 let headerField = string.prefix(upTo: index) 50 let nextIndex = string.index(index, offsetBy: 1) // skip the ":" 51 let value = string.suffix(from: nextIndex) 52 request.addValue(String(value), forHTTPHeaderField: String(headerField)) 53 } 54 ptr += 1 55 } 56 } 57 return quickjs.reqCreate(request, responseCb, responseCbCls) 58 } 59 } 60 let logger = Logger(subsystem: "net.taler.gnu", category: "Networking Failure") 61 logger.error("Failed to create URLRequest for >>\(String(describing: requestInfo?.pointee.url))<<") 62 return -1 63 } 64 65 func request_cancel(userdata: Optional<UnsafeMutableRawPointer>, 66 requestID: Int32) -> Int32 { 67 let quickjs = Unmanaged<Quickjs>.fromOpaque(userdata!).takeUnretainedValue() 68 return quickjs.reqCancel(requestID) 69 } 70 // MARK: - 71 extension Error { 72 var errorCode:Int? { 73 return (self as NSError).code 74 } 75 } 76 // MARK: - 77 final class QuickDataTask: NSObject, @unchecked Sendable { 78 let urlSession: URLSession 79 let request: URLRequest 80 let requestID: Int32 81 let responseCb: JSHttpResponseCb? 82 let responseCbCls: Optional<UnsafeMutableRawPointer> 83 84 private let state = NSCondition() 85 private let onFinish: (Int32) -> Void 86 private var dataTask: URLSessionDataTask? 87 private var isCancelled = false 88 private var isCallbackActive = false 89 private var isFinished = false 90 #if DEBUG || TALER_NIGHTLY 91 private let logger: Logger 92 #endif 93 94 init(urlSession: URLSession, 95 request: URLRequest, 96 requestID: Int32, 97 responseCb: JSHttpResponseCb?, 98 responseCbCls: Optional<UnsafeMutableRawPointer>, 99 onFinish: @escaping (Int32) -> Void 100 ) { 101 #if DEBUG || TALER_NIGHTLY 102 self.logger = Logger(subsystem: "net.taler.gnu", category: "Networking") 103 #endif 104 self.urlSession = urlSession 105 self.request = request 106 self.requestID = requestID 107 self.responseCb = responseCb 108 self.responseCbCls = responseCbCls 109 self.onFinish = onFinish 110 } 111 112 func run() { 113 guard responseCb != nil, responseCbCls != nil else { 114 finish() 115 return 116 } 117 118 let method = request.httpMethod ?? "Unknown" 119 let url = request.url?.trimmedString ?? EMPTYSTRING 120 #if DEBUG || TALER_NIGHTLY 121 logger.trace("❓\(self.requestID, privacy: .public) \(method, privacy: .public) \(url, privacy: .public)") 122 #endif 123 let task = urlSession.dataTask(with: request) { [self] data, response, error in 124 handleCompletion(data: data, 125 response: response, 126 error: error, 127 method: method, 128 url: url) 129 } 130 131 state.lock() 132 guard !isFinished else { 133 state.unlock() 134 task.cancel() 135 return 136 } 137 dataTask = task 138 let shouldStart = !isCancelled 139 state.unlock() 140 141 if shouldStart { 142 task.resume() 143 } else { 144 task.cancel() 145 finish() 146 } 147 } 148 149 /// Cancels the request and does not return until a native callback that had 150 /// already started has completed. A URLSession completion may still arrive 151 /// later, but `isCancelled` prevents it from entering the native callback. 152 func cancel() { 153 state.lock() 154 guard !isFinished else { 155 state.unlock() 156 return 157 } 158 isCancelled = true 159 let task = dataTask 160 state.unlock() 161 162 task?.cancel() 163 164 var shouldNotify = false 165 state.lock() 166 while isCallbackActive { 167 state.wait() 168 } 169 if !isFinished { 170 isFinished = true 171 dataTask = nil 172 shouldNotify = true 173 } 174 state.unlock() 175 176 if shouldNotify { 177 onFinish(requestID) 178 } 179 } 180 181 private func handleCompletion(data: Data?, 182 response: URLResponse?, 183 error: Error?, 184 method: String, 185 url: String) { 186 guard beginCallback() else { 187 finish() 188 return 189 } 190 defer { endCallback() } 191 192 if let response = response as? HTTPURLResponse { 193 deliver(response: response, data: data ?? Data(), method: method, url: url) 194 } else { 195 #if DEBUG || TALER_NIGHTLY 196 logger.error("⁉️ \(self.requestID, privacy: .public) \(method, privacy: .public) \(error, privacy: .public)") 197 #endif 198 Task.detached { 199 Controller.shared.checkInternetConnection() 200 } 201 deliver(error: error) 202 } 203 } 204 205 private func deliver(response: HTTPURLResponse, 206 data: Data, 207 method: String, 208 url: String) { 209 guard let responseCb, let responseCbCls else { 210 return 211 } 212 213 let status = Int32(response.statusCode) 214 let errmsg = HTTPURLResponse.localizedString(forStatusCode: response.statusCode) 215 let headerArray = response.allHeaderFields.map { "\($0.key): \($0.value)" } 216 let cHeaders = CStringArray(headerArray) 217 218 #if DEBUG || TALER_NIGHTLY 219 if data.isEmpty { 220 logger.error("‼️\(self.requestID, privacy: .public) \(method, privacy: .public) \(response.statusCode, privacy: .public) \(errmsg, privacy: .public)") 221 } else { 222 logger.trace("❗️ \(self.requestID, privacy: .public) \(url, privacy: .public)") 223 } 224 #endif 225 226 errmsg.withCString { errmsgPtr in 227 data.withUnsafeBytes { bodyBytes in 228 var responseInfo = JSHttpResponseInfo( 229 request_id: requestID, 230 status: status, 231 errmsg: UnsafeMutablePointer(mutating: errmsgPtr), 232 response_headers: cHeaders.pointer, 233 num_response_headers: Int32(headerArray.count), 234 body: UnsafeMutableRawPointer(mutating: bodyBytes.baseAddress), 235 body_len: numericCast(data.count) 236 ) 237 withUnsafeMutablePointer(to: &responseInfo) { responseInfoPtr in 238 // cHeaders owns the C strings that response_headers points to, and ARC 239 // only keeps it alive up to its last use - which is reading .pointer 240 // above, before the callback that dereferences it has run 241 withExtendedLifetime(cHeaders) { 242 responseCb(responseCbCls, responseInfoPtr) 243 } 244 } 245 } 246 } 247 } 248 249 private func deliver(error: Error?) { 250 guard let responseCb, let responseCbCls else { 251 return 252 } 253 254 let errmsg = error?.localizedDescription ?? "Network request failed" 255 errmsg.withCString { errmsgPtr in 256 var responseInfo = JSHttpResponseInfo( 257 request_id: requestID, 258 status: 0, 259 errmsg: UnsafeMutablePointer(mutating: errmsgPtr), 260 response_headers: nil, 261 num_response_headers: 0, 262 body: nil, 263 body_len: 0 264 ) 265 withUnsafeMutablePointer(to: &responseInfo) { responseInfoPtr in 266 responseCb(responseCbCls, responseInfoPtr) 267 } 268 } 269 } 270 271 private func beginCallback() -> Bool { 272 state.lock() 273 defer { state.unlock() } 274 guard !isCancelled, !isFinished else { 275 return false 276 } 277 isCallbackActive = true 278 return true 279 } 280 281 private func endCallback() { 282 var shouldNotify = false 283 state.lock() 284 isCallbackActive = false 285 if !isFinished { 286 isFinished = true 287 dataTask = nil 288 shouldNotify = true 289 } 290 state.broadcast() 291 state.unlock() 292 293 if shouldNotify { 294 onFinish(requestID) 295 } 296 } 297 298 private func finish() { 299 var shouldNotify = false 300 state.lock() 301 if !isFinished { 302 isFinished = true 303 dataTask = nil 304 shouldNotify = true 305 } 306 state.broadcast() 307 state.unlock() 308 309 if shouldNotify { 310 onFinish(requestID) 311 } 312 } 313 }