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

RDMR-100 #7460

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion ios/Capacitor/Capacitor/KeyValueStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,16 @@ private class InMemoryStore: KeyValueStoreBackend {
}
}

private class ConcurrentDictionary<Value> {
internal class ConcurrentDictionary<Value> {
private var storage: [String: Value]
private let lock = NSLock()

internal var keys: [String: Value].Keys {
lock.withLock {
storage.keys
}
}

init(_ initial: [String: Value] = [:]) {
storage = initial
}
Expand All @@ -293,4 +299,13 @@ private class ConcurrentDictionary<Value> {
}
}
}

/// Only runs ``action`` if ``key`` is present
func with(_ key: String, action: (Value) -> Void) {
lock.withLock {
if let value = storage[key] {
action(value)
}
}
}
}
224 changes: 122 additions & 102 deletions ios/Capacitor/Capacitor/WebViewAssetHandler.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import Foundation
import MobileCoreServices
import Combine

@objc(CAPWebViewAssetHandler)
// swiftlint:disable type_body_length
open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
private var router: Router
private var serverUrl: URL?
private let tasks = ConcurrentDictionary<Task<Void, any Error>>()

public init(router: Router) {
self.router = router
Expand All @@ -25,89 +27,97 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
}

open func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
let startPath: String
let url = urlSchemeTask.request.url!
let stringToLoad = url.path
let localUrl = URL.init(string: url.absoluteString)!
tasks[urlSchemeTask.hashString] = Task(priority: .userInitiated) {
defer { tasks[urlSchemeTask.hashString] = nil }
let startPath: String
let url = urlSchemeTask.request.url!
let stringToLoad = url.path
let localUrl = URL.init(string: url.absoluteString)!

if url.path.starts(with: CapacitorBridge.httpInterceptorStartIdentifier) {
handleCapacitorHttpRequest(urlSchemeTask, localUrl, false)
return
}

if url.path.starts(with: CapacitorBridge.httpsInterceptorStartIdentifier) {
handleCapacitorHttpRequest(urlSchemeTask, localUrl, true)
return
}
if url.path.starts(with: CapacitorBridge.httpInterceptorStartIdentifier) {
await handleCapacitorHttpRequest(urlSchemeTask, localUrl, false)
return
}

if stringToLoad.starts(with: CapacitorBridge.fileStartIdentifier) {
startPath = stringToLoad.replacingOccurrences(of: CapacitorBridge.fileStartIdentifier, with: "")
} else {
startPath = router.route(for: stringToLoad)
}
if url.path.starts(with: CapacitorBridge.httpsInterceptorStartIdentifier) {
await handleCapacitorHttpRequest(urlSchemeTask, localUrl, true)
return
}

let fileUrl = URL.init(fileURLWithPath: startPath)
if stringToLoad.starts(with: CapacitorBridge.fileStartIdentifier) {
startPath = stringToLoad.replacingOccurrences(of: CapacitorBridge.fileStartIdentifier, with: "")
} else {
startPath = router.route(for: stringToLoad)
}

do {
var data = Data()
let mimeType = mimeTypeForExtension(pathExtension: url.pathExtension)
var headers = [
"Content-Type": mimeType,
"Cache-Control": "no-cache"
]
let fileUrl = URL.init(fileURLWithPath: startPath)

// if using live reload, then set CORS headers
if isUsingLiveReload(localUrl) {
headers["Access-Control-Allow-Origin"] = self.serverUrl?.absoluteString
headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS, TRACE"
}
do {
var data = Data()
let mimeType = mimeTypeForExtension(pathExtension: url.pathExtension)
var headers = [
"Content-Type": mimeType,
"Cache-Control": "no-cache"
]

if let rangeString = urlSchemeTask.request.value(forHTTPHeaderField: "Range"),
let totalSize = try fileUrl.resourceValues(forKeys: [.fileSizeKey]).fileSize,
isMediaExtension(pathExtension: url.pathExtension) {
let fileHandle = try FileHandle(forReadingFrom: fileUrl)
let parts = rangeString.components(separatedBy: "=")
let streamParts = parts[1].components(separatedBy: "-")
let fromRange = Int(streamParts[0]) ?? 0
var toRange = totalSize - 1
if streamParts.count > 1 {
toRange = Int(streamParts[1]) ?? toRange
// if using live reload, then set CORS headers
if isUsingLiveReload(localUrl) {
headers["Access-Control-Allow-Origin"] = self.serverUrl?.absoluteString
headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS, TRACE"
}
let rangeLength = toRange - fromRange + 1
try fileHandle.seek(toOffset: UInt64(fromRange))
data = fileHandle.readData(ofLength: rangeLength)
headers["Accept-Ranges"] = "bytes"
headers["Content-Range"] = "bytes \(fromRange)-\(toRange)/\(totalSize)"
headers["Content-Length"] = String(data.count)
let response = HTTPURLResponse(url: localUrl, statusCode: 206, httpVersion: nil, headerFields: headers)
urlSchemeTask.didReceive(response!)
try fileHandle.close()
} else {
if !stringToLoad.contains("cordova.js") {
if isMediaExtension(pathExtension: url.pathExtension) {
data = try Data(contentsOf: fileUrl, options: Data.ReadingOptions.mappedIfSafe)
} else {
data = try Data(contentsOf: fileUrl)

if let rangeString = urlSchemeTask.request.value(forHTTPHeaderField: "Range"),
let totalSize = try fileUrl.resourceValues(forKeys: [.fileSizeKey]).fileSize,
isMediaExtension(pathExtension: url.pathExtension) {
let fileHandle = try FileHandle(forReadingFrom: fileUrl)
let parts = rangeString.components(separatedBy: "=")
let streamParts = parts[1].components(separatedBy: "-")
let fromRange = Int(streamParts[0]) ?? 0
var toRange = totalSize - 1
if streamParts.count > 1 {
toRange = Int(streamParts[1]) ?? toRange
}
}
let urlResponse = URLResponse(url: localUrl, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil)
let httpResponse = HTTPURLResponse(url: localUrl, statusCode: 200, httpVersion: nil, headerFields: headers)
if isMediaExtension(pathExtension: url.pathExtension) {
urlSchemeTask.didReceive(urlResponse)
let rangeLength = toRange - fromRange + 1
try fileHandle.seek(toOffset: UInt64(fromRange))
data = fileHandle.readData(ofLength: rangeLength)
headers["Accept-Ranges"] = "bytes"
headers["Content-Range"] = "bytes \(fromRange)-\(toRange)/\(totalSize)"
headers["Content-Length"] = String(data.count)
let response = HTTPURLResponse(url: localUrl, statusCode: 206, httpVersion: nil, headerFields: headers)
tasks.with(urlSchemeTask) { $0.didReceive(response!) }
try fileHandle.close()
} else {
urlSchemeTask.didReceive(httpResponse!)
if !stringToLoad.contains("cordova.js") {
if isMediaExtension(pathExtension: url.pathExtension) {
data = try Data(contentsOf: fileUrl, options: Data.ReadingOptions.mappedIfSafe)
} else {
data = try Data(contentsOf: fileUrl)
}
}
let urlResponse = URLResponse(url: localUrl, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil)
let httpResponse = HTTPURLResponse(url: localUrl, statusCode: 200, httpVersion: nil, headerFields: headers)
tasks.with(urlSchemeTask) {
if isMediaExtension(pathExtension: url.pathExtension) {
$0.didReceive(urlResponse)
} else {
$0.didReceive(httpResponse!)
}
$0.didReceive(data)
}
}

} catch let error as NSError {
tasks.with(urlSchemeTask) { $0.didFailWithError(error) }
return
}
urlSchemeTask.didReceive(data)
} catch let error as NSError {
urlSchemeTask.didFailWithError(error)
return
tasks.with(urlSchemeTask) { $0.didFinish() }
}
urlSchemeTask.didFinish()
}

open func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
CAPLog.print("scheme stop")
print("cancelling task \(urlSchemeTask.hashString)")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
print("cancelling task \(urlSchemeTask.hashString)")

remove print

with latest changes the app sometimes doesn't start, I just get a white screen and no messages after Loading app at capacitor://localhost...

tasks[urlSchemeTask.hashString]?.cancel()
tasks[urlSchemeTask.hashString] = nil
}

open func mimeTypeForExtension(pathExtension: String) -> String {
Expand Down Expand Up @@ -135,7 +145,7 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
return false
}

func handleCapacitorHttpRequest(_ urlSchemeTask: WKURLSchemeTask, _ localUrl: URL, _ isHttpsRequest: Bool) {
func handleCapacitorHttpRequest(_ urlSchemeTask: WKURLSchemeTask, _ localUrl: URL, _ isHttpsRequest: Bool) async {
var urlRequest = urlSchemeTask.request
guard let url = urlRequest.url else { return }
var targetUrl = url.absoluteString
Expand All @@ -154,47 +164,45 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
urlRequest.url = URL(string: targetUrl.removingPercentEncoding ?? targetUrl)

let urlSession = URLSession.shared
let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
urlSchemeTask.didFailWithError(error)
return
}
let data: Data
let response: URLResponse
do {
(data, response) = try await urlSession.data(for: urlRequest)
} catch {
tasks.with(urlSchemeTask) { $0.didFailWithError(error) }
return
}

if let response = response as? HTTPURLResponse {
let existingHeaders = response.allHeaderFields
var newHeaders: [AnyHashable: Any] = [:]
if let response = response as? HTTPURLResponse {
let existingHeaders = response.allHeaderFields
var newHeaders: [AnyHashable: Any] = [:]

// if using live reload, then set CORS headers
if self.isUsingLiveReload(url) {
newHeaders = [
"Access-Control-Allow-Origin": self.serverUrl?.absoluteString ?? "",
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS, TRACE"
]
}
// if using live reload, then set CORS headers
if self.isUsingLiveReload(url) {
newHeaders = [
"Access-Control-Allow-Origin": self.serverUrl?.absoluteString ?? "",
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS, TRACE"
]
}

if let mergedHeaders = existingHeaders.merging(newHeaders, uniquingKeysWith: { (_, newHeaders) in newHeaders }) as? [String: String] {
if let mergedHeaders = existingHeaders.merging(newHeaders, uniquingKeysWith: { (_, newHeaders) in newHeaders }) as? [String: String] {

if let responseUrl = response.url {
if let modifiedResponse = HTTPURLResponse(
url: responseUrl,
statusCode: response.statusCode,
httpVersion: nil,
headerFields: mergedHeaders
) {
urlSchemeTask.didReceive(modifiedResponse)
}
}
if let responseUrl = response.url,
let modifiedResponse = HTTPURLResponse(
url: responseUrl,
statusCode: response.statusCode,
httpVersion: nil,
headerFields: mergedHeaders
) {
tasks.with(urlSchemeTask) { $0.didReceive(modifiedResponse) }
}

if let data = data {
urlSchemeTask.didReceive(data)
}
tasks.with(urlSchemeTask) {
$0.didReceive(data)
$0.didFinish()
}
}
urlSchemeTask.didFinish()
return
}

task.resume()
}

public let mimeTypes = [
Expand Down Expand Up @@ -546,3 +554,15 @@ open class WebViewAssetHandler: NSObject, WKURLSchemeHandler {
"zip": "application/x-zip-compressed"
]
}

private extension WKURLSchemeTask {
var hashString: String { String(hash) }
}

private extension ConcurrentDictionary {
func with(_ schemeTask: WKURLSchemeTask, action: (WKURLSchemeTask) -> Void) {
with(schemeTask.hashString) { _ in
action(schemeTask)
}
}
}