Fix main-thread blocking I/O
This commit is contained in:
@@ -2,6 +2,9 @@ import Foundation
|
||||
|
||||
public extension Profile {
|
||||
func read() throws -> String {
|
||||
#if DEBUG
|
||||
precondition(!Thread.isMainThread, "Profile.read() must not be called on the main thread")
|
||||
#endif
|
||||
switch type {
|
||||
case .local, .remote:
|
||||
return try String(contentsOfFile: path)
|
||||
@@ -12,6 +15,9 @@ public extension Profile {
|
||||
}
|
||||
|
||||
func write(_ content: String) throws {
|
||||
#if DEBUG
|
||||
precondition(!Thread.isMainThread, "Profile.write(...) must not be called on the main thread")
|
||||
#endif
|
||||
switch type {
|
||||
case .local, .remote:
|
||||
try content.write(toFile: path, atomically: true, encoding: .utf8)
|
||||
@@ -20,4 +26,33 @@ public extension Profile {
|
||||
try content.write(to: saveURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
func readAsync() async throws -> String {
|
||||
let type = type
|
||||
let path = path
|
||||
return try await BlockingIO.run {
|
||||
switch type {
|
||||
case .local, .remote:
|
||||
return try String(contentsOfFile: path)
|
||||
case .icloud:
|
||||
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(path)
|
||||
return try String(contentsOf: saveURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeAsync(_ content: String) async throws {
|
||||
let type = type
|
||||
let path = path
|
||||
let content = content
|
||||
try await BlockingIO.run {
|
||||
switch type {
|
||||
case .local, .remote:
|
||||
try content.write(toFile: path, atomically: true, encoding: .utf8)
|
||||
case .icloud:
|
||||
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(path)
|
||||
try content.write(to: saveURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,79 @@ public extension Profile {
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func encodedContentDataAsync() async throws -> Data {
|
||||
let name = name
|
||||
let type = type
|
||||
let remoteURL = remoteURL
|
||||
let autoUpdate = autoUpdate
|
||||
let autoUpdateInterval = autoUpdateInterval
|
||||
let lastUpdated = lastUpdated
|
||||
|
||||
let config = try await readAsync()
|
||||
return try await BlockingIO.run {
|
||||
let content = LibboxProfileContent()
|
||||
content.name = name
|
||||
switch type {
|
||||
case .local, .icloud:
|
||||
content.type = LibboxProfileTypeLocal
|
||||
case .remote:
|
||||
content.type = LibboxProfileTypeRemote
|
||||
}
|
||||
content.config = config
|
||||
if type == .remote {
|
||||
content.remotePath = remoteURL!
|
||||
content.autoUpdate = autoUpdate
|
||||
content.autoUpdateInterval = autoUpdateInterval
|
||||
if let lastUpdated {
|
||||
content.lastUpdated = Int64(lastUpdated.timeIntervalSince1970 * 1000)
|
||||
}
|
||||
}
|
||||
guard let encoded = content.encode() else {
|
||||
throw NSError(domain: "Profile", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to encode profile"])
|
||||
}
|
||||
return encoded
|
||||
}
|
||||
}
|
||||
|
||||
func generateShareFileAsync() async throws -> URL {
|
||||
let name = name
|
||||
let type = type
|
||||
let remoteURL = remoteURL
|
||||
let autoUpdate = autoUpdate
|
||||
let autoUpdateInterval = autoUpdateInterval
|
||||
let lastUpdated = lastUpdated
|
||||
|
||||
let config = try await readAsync()
|
||||
return try await BlockingIO.run {
|
||||
let content = LibboxProfileContent()
|
||||
content.name = name
|
||||
switch type {
|
||||
case .local, .icloud:
|
||||
content.type = LibboxProfileTypeLocal
|
||||
case .remote:
|
||||
content.type = LibboxProfileTypeRemote
|
||||
}
|
||||
content.config = config
|
||||
if type == .remote {
|
||||
content.remotePath = remoteURL!
|
||||
content.autoUpdate = autoUpdate
|
||||
content.autoUpdateInterval = autoUpdateInterval
|
||||
if let lastUpdated {
|
||||
content.lastUpdated = Int64(lastUpdated.timeIntervalSince1970 * 1000)
|
||||
}
|
||||
}
|
||||
return try content.generateShareFile()
|
||||
}
|
||||
}
|
||||
|
||||
func generateJSONShareFileAsync(name: String) async throws -> URL {
|
||||
let filename = name
|
||||
let config = try await readAsync()
|
||||
return try await BlockingIO.run {
|
||||
try config.generateShareFile(name: filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func dateFromTimestamp(_ timestamp: Int64) -> Date {
|
||||
@@ -57,17 +130,34 @@ public extension LibboxProfileContent {
|
||||
|
||||
@discardableResult
|
||||
func importProfile() async throws -> Profile {
|
||||
let name = name
|
||||
let type = type
|
||||
let config = config
|
||||
let remotePath = remotePath
|
||||
let autoUpdate = autoUpdate
|
||||
let autoUpdateInterval = autoUpdateInterval
|
||||
let lastUpdated = lastUpdated
|
||||
let nextProfileID = try await ProfileManager.nextID()
|
||||
let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
|
||||
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
|
||||
try config.write(to: profileConfig, atomically: true, encoding: .utf8)
|
||||
var lastUpdatedAt: Date?
|
||||
if lastUpdated > 0 {
|
||||
lastUpdatedAt = dateFromTimestamp(lastUpdated)
|
||||
}
|
||||
try await BlockingIO.run {
|
||||
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
|
||||
try config.write(to: profileConfig, atomically: true, encoding: .utf8)
|
||||
}
|
||||
let uniqueProfileName = try await ProfileManager.uniqueName(name)
|
||||
let profile = Profile(name: uniqueProfileName, type: ProfileType(rawValue: Int(type))!, path: profileConfig.relativePath, remoteURL: remotePath, autoUpdate: autoUpdate, autoUpdateInterval: autoUpdateInterval, lastUpdated: lastUpdatedAt)
|
||||
let profile = Profile(
|
||||
name: uniqueProfileName,
|
||||
type: ProfileType(rawValue: Int(type))!,
|
||||
path: profileConfig.relativePath,
|
||||
remoteURL: remotePath,
|
||||
autoUpdate: autoUpdate,
|
||||
autoUpdateInterval: autoUpdateInterval,
|
||||
lastUpdated: lastUpdatedAt
|
||||
)
|
||||
try await ProfileManager.create(profile)
|
||||
await SharedPreferences.selectedProfileID.set(profile.mustID)
|
||||
return profile
|
||||
@@ -190,6 +280,12 @@ public extension UTType {
|
||||
public let filename: String
|
||||
public let contentType: UTType
|
||||
|
||||
public init(data: Data, filename: String, contentType: UTType) {
|
||||
self.data = data
|
||||
self.filename = filename
|
||||
self.contentType = contentType
|
||||
}
|
||||
|
||||
public init(profile: ProfileExportDocument) {
|
||||
data = profile.data
|
||||
filename = profile.filename
|
||||
|
||||
@@ -7,21 +7,23 @@ public extension Profile {
|
||||
if type != .remote {
|
||||
return
|
||||
}
|
||||
let remoteContent = try HTTPClient().getString(remoteURL)
|
||||
var error: NSError?
|
||||
LibboxCheckConfig(remoteContent, &error)
|
||||
if let error {
|
||||
throw error
|
||||
let remoteContent = try await HTTPClient.getStringAsync(remoteURL)
|
||||
try await BlockingIO.run {
|
||||
var error: NSError?
|
||||
LibboxCheckConfig(remoteContent, &error)
|
||||
if let error {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
lastUpdated = Date()
|
||||
try await ProfileManager.update(self)
|
||||
do {
|
||||
let oldContent = try read()
|
||||
let oldContent = try await readAsync()
|
||||
if oldContent == remoteContent {
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
try write(remoteContent)
|
||||
try await writeAsync(remoteContent)
|
||||
try await onProfileUpdated()
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ public class ExtensionProfile: ObservableObject {
|
||||
])
|
||||
}
|
||||
|
||||
let configContent = try profile.read()
|
||||
let configContent = try await profile.readAsync()
|
||||
options["configContent"] = NSString(string: configContent)
|
||||
|
||||
options["ignoreMemoryLimit"] = await NSNumber(value: SharedPreferences.ignoreMemoryLimit.get())
|
||||
@@ -215,10 +215,9 @@ public class ExtensionProfile: ObservableObject {
|
||||
}
|
||||
|
||||
public func fetchProfile() async throws {
|
||||
if let profile = try await ProfileManager.get(Int64(SharedPreferences.selectedProfileID.get())) {
|
||||
if profile.type == .icloud {
|
||||
_ = try profile.read()
|
||||
}
|
||||
let profileID = await SharedPreferences.selectedProfileID.get()
|
||||
if let profile = try await ProfileManager.get(profileID), profile.type == .icloud {
|
||||
_ = try await profile.readAsync()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ public class HTTPClient {
|
||||
}
|
||||
|
||||
public func getString(_ url: String?) throws -> String {
|
||||
#if DEBUG
|
||||
precondition(!Thread.isMainThread, "HTTPClient.getString(...) must not be called on the main thread")
|
||||
#endif
|
||||
let request = client.newRequest()!
|
||||
request.setUserAgent(HTTPClient.userAgent)
|
||||
try request.setURL(url)
|
||||
@@ -32,6 +35,16 @@ public class HTTPClient {
|
||||
return content.value
|
||||
}
|
||||
|
||||
public func getStringAsync(_ url: String?) async throws -> String {
|
||||
try await Self.getStringAsync(url)
|
||||
}
|
||||
|
||||
public static func getStringAsync(_ url: String?) async throws -> String {
|
||||
try await BlockingIO.run {
|
||||
try HTTPClient().getString(url)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
client.close()
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
public class RootHelperClient {
|
||||
public class RootHelperClient: @unchecked Sendable {
|
||||
public static let shared = RootHelperClient()
|
||||
|
||||
private var connection: NSXPCConnection?
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import Foundation
|
||||
|
||||
public enum BlockingIO {
|
||||
private static let queue = DispatchQueue(
|
||||
label: "io.nekohasekai.sing-box.blocking-io",
|
||||
qos: .userInitiated,
|
||||
attributes: .concurrent
|
||||
)
|
||||
|
||||
public static func run<T: Sendable>(_ operation: @escaping @Sendable () throws -> T) async throws -> T {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
queue.async {
|
||||
#if DEBUG
|
||||
precondition(!Thread.isMainThread, "BlockingIO operation must not run on the main thread")
|
||||
#endif
|
||||
do {
|
||||
try continuation.resume(returning: operation())
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func run<T: Sendable>(_ operation: @escaping @Sendable () -> T) async -> T {
|
||||
await withCheckedContinuation { continuation in
|
||||
queue.async {
|
||||
#if DEBUG
|
||||
precondition(!Thread.isMainThread, "BlockingIO operation must not run on the main thread")
|
||||
#endif
|
||||
continuation.resume(returning: operation())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user