mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-30 18:52:12 +08:00
refactor(widget): resolve all SwiftLint warnings
Split the extension socket client into connect/send/read helpers so exchange() stays under the complexity and body-length limits, rename the file-descriptor parameters, drop redundant nil initialization on the debug preview knobs, and wrap over-length lines. No behavior change.
This commit is contained in:
@@ -1,30 +1,35 @@
|
||||
import Darwin
|
||||
import Foundation
|
||||
import Security
|
||||
import WidgetKit
|
||||
import Darwin
|
||||
|
||||
let widgetKind = "VergeRoutingWidget"
|
||||
let hostID = "io.github.clash-verge-rev.clash-verge-rev"
|
||||
|
||||
func socketPath() -> String? {
|
||||
guard let group = Bundle.main.object(forInfoDictionaryKey: "VergeWidgetGroup") as? String,
|
||||
let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group) else { return nil }
|
||||
guard let group = Bundle.main.object(forInfoDictionaryKey: "VergeWidgetGroup") as? String else { return nil }
|
||||
guard let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group) else {
|
||||
return nil
|
||||
}
|
||||
return container.appendingPathComponent("verge-widget.sock").path
|
||||
}
|
||||
|
||||
func authenticPeer(_ fd: Int32, identifier: String) -> Bool {
|
||||
func authenticPeer(_ descriptor: Int32, identifier: String) -> Bool {
|
||||
var token = audit_token_t()
|
||||
var size = socklen_t(MemoryLayout.size(ofValue: token))
|
||||
guard getsockopt(fd, SOL_LOCAL, LOCAL_PEERTOKEN, &token, &size) == 0,
|
||||
guard getsockopt(descriptor, SOL_LOCAL, LOCAL_PEERTOKEN, &token, &size) == 0,
|
||||
size == MemoryLayout.size(ofValue: token) else { return false }
|
||||
let data = withUnsafeBytes(of: token) { Data($0) }
|
||||
var peer: SecCode?
|
||||
guard SecCodeCopyGuestWithAttributes(nil, [kSecGuestAttributeAudit: data] as CFDictionary, [], &peer) == errSecSuccess,
|
||||
let attributes = [kSecGuestAttributeAudit: data] as CFDictionary
|
||||
guard SecCodeCopyGuestWithAttributes(nil, attributes, [], &peer) == errSecSuccess,
|
||||
let peer,
|
||||
let team = Bundle.main.object(forInfoDictionaryKey: "VergeWidgetTeam") as? String,
|
||||
team.range(of: "^[A-Z0-9]{10}$", options: .regularExpression) != nil else { return false }
|
||||
var requirement: SecRequirement?
|
||||
let expression = "anchor apple generic and certificate leaf[subject.OU] = \"\(team)\" and identifier \"\(identifier)\""
|
||||
let expression = """
|
||||
anchor apple generic and certificate leaf[subject.OU] = "\(team)" and identifier "\(identifier)"
|
||||
"""
|
||||
guard SecRequirementCreateWithString(expression as CFString, [], &requirement) == errSecSuccess,
|
||||
let requirement else { return false }
|
||||
return SecCodeCheckValidity(peer, [], requirement) == errSecSuccess
|
||||
@@ -37,8 +42,8 @@ public func vergeWidgetPath() -> UnsafeMutablePointer<CChar>? {
|
||||
}
|
||||
|
||||
@_cdecl("verge_widget_authenticate")
|
||||
public func vergeWidgetAuthenticate(_ fd: Int32) -> Bool {
|
||||
authenticPeer(fd, identifier: hostID + ".widget")
|
||||
public func vergeWidgetAuthenticate(_ descriptor: Int32) -> Bool {
|
||||
authenticPeer(descriptor, identifier: hostID + ".widget")
|
||||
}
|
||||
|
||||
@_cdecl("verge_widget_reload")
|
||||
|
||||
@@ -6,14 +6,14 @@ struct RoutingView: View {
|
||||
let entry: Entry
|
||||
@Environment(\.widgetFamily) private var widgetFamily
|
||||
#if DEBUG
|
||||
var previewFamily: WidgetFamily? = nil
|
||||
var previewFamily: WidgetFamily?
|
||||
private var family: WidgetFamily { previewFamily ?? widgetFamily }
|
||||
#else
|
||||
private var family: WidgetFamily { widgetFamily }
|
||||
#endif
|
||||
@Environment(\.widgetRenderingMode) private var systemRenderingMode
|
||||
#if DEBUG
|
||||
var previewRenderingMode: WidgetRenderingMode? = nil
|
||||
var previewRenderingMode: WidgetRenderingMode?
|
||||
var previewMaterial = "automatic"
|
||||
var previewIncreaseContrast = false
|
||||
private var renderingMode: WidgetRenderingMode { previewRenderingMode ?? systemRenderingMode }
|
||||
@@ -24,8 +24,8 @@ struct RoutingView: View {
|
||||
@Environment(\.accessibilityReduceMotion) private var systemReduceMotion
|
||||
@Environment(\.accessibilityReduceTransparency) private var systemReduceTransparency
|
||||
#if DEBUG
|
||||
var previewReduceMotion: Bool? = nil
|
||||
var previewReduceTransparency: Bool? = nil
|
||||
var previewReduceMotion: Bool?
|
||||
var previewReduceTransparency: Bool?
|
||||
private var reduceMotion: Bool { previewReduceMotion ?? systemReduceMotion }
|
||||
private var reduceTransparency: Bool { previewReduceTransparency ?? systemReduceTransparency }
|
||||
#else
|
||||
@@ -65,7 +65,9 @@ struct RoutingView: View {
|
||||
return colorScheme == .dark ? .black : .white
|
||||
}
|
||||
private var locale: Locale {
|
||||
Locale(identifier: entry.status.language.map { ["jp": "ja", "zh": "zh-Hans", "zhtw": "zh-Hant"][$0] ?? $0 } ?? Locale.current.identifier)
|
||||
let aliases = ["jp": "ja", "zh": "zh-Hans", "zhtw": "zh-Hant"]
|
||||
let identifier = entry.status.language.map { aliases[$0] ?? $0 } ?? Locale.current.identifier
|
||||
return Locale(identifier: identifier)
|
||||
}
|
||||
private var rtl: Bool { locale.language.characterDirection == .rightToLeft }
|
||||
|
||||
@@ -155,7 +157,10 @@ struct RoutingView: View {
|
||||
ForEach(orderedModes, id: \.self) { mode in
|
||||
let selected = entry.status.mode == mode
|
||||
let label = Text(text("tray." + mode))
|
||||
.font(.system(size: family == .systemSmall ? 12 : 13, weight: selected ? .semibold : .regular))
|
||||
.font(.system(
|
||||
size: family == .systemSmall ? 12 : 13,
|
||||
weight: selected ? .semibold : .regular
|
||||
))
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 30)
|
||||
@@ -177,7 +182,10 @@ struct RoutingView: View {
|
||||
.frame(height: 30)
|
||||
HStack(spacing: 8) {
|
||||
control(text("tray.tooltip.tun"), action: "tun", value: entry.status.tun, icon: "network")
|
||||
control(text("tray.systemProxy"), action: "proxy", value: entry.status.proxy, icon: "shield.lefthalf.filled")
|
||||
control(
|
||||
text("tray.systemProxy"), action: "proxy",
|
||||
value: entry.status.proxy, icon: "shield.lefthalf.filled"
|
||||
)
|
||||
}
|
||||
if let error = entry.status.error {
|
||||
Text(text(error)).font(.caption2).foregroundStyle(ink.opacity(0.8)).lineLimit(2)
|
||||
|
||||
@@ -73,7 +73,7 @@ private struct RoutingStylePreview: View {
|
||||
case "light": Color(white: 0.92)
|
||||
case "dark": Color(white: 0.1)
|
||||
case "pattern":
|
||||
Canvas { context, size in
|
||||
Canvas { context, _ in
|
||||
for row in 0..<8 {
|
||||
for column in 0..<14 {
|
||||
let rect = CGRect(x: CGFloat(column) * 32, y: CGFloat(row) * 32, width: 32, height: 32)
|
||||
@@ -90,9 +90,15 @@ private struct RoutingStylePreview: View {
|
||||
VStack(spacing: 12) {
|
||||
ZStack {
|
||||
background
|
||||
RoutingView(entry: Entry(date: .now, status: sample), previewFamily: medium ? .systemMedium : .systemSmall,
|
||||
previewRenderingMode: renderingOverride, previewMaterial: material, previewIncreaseContrast: increaseContrast,
|
||||
previewReduceMotion: reduceMotion, previewReduceTransparency: reduceTransparency)
|
||||
RoutingView(
|
||||
entry: Entry(date: .now, status: sample),
|
||||
previewFamily: medium ? .systemMedium : .systemSmall,
|
||||
previewRenderingMode: renderingOverride,
|
||||
previewMaterial: material,
|
||||
previewIncreaseContrast: increaseContrast,
|
||||
previewReduceMotion: reduceMotion,
|
||||
previewReduceTransparency: reduceTransparency
|
||||
)
|
||||
.environment(\.colorScheme, dark ? .dark : .light)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
@@ -162,8 +168,11 @@ private struct RoutingStylePreview: View {
|
||||
}
|
||||
Toggle("Reduce transparency", isOn: $reduceTransparency)
|
||||
Toggle("Reduce motion", isOn: $reduceMotion)
|
||||
Text("Style only. Controls change sample data; no proxy requests. Desktop WidgetKit compositing is not simulated.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
Text(
|
||||
"Style only. Controls change sample data; no proxy requests. "
|
||||
+ "Desktop WidgetKit compositing is not simulated."
|
||||
)
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(4)
|
||||
}
|
||||
@@ -187,6 +196,9 @@ private struct RoutingStylePreview: View {
|
||||
}
|
||||
|
||||
#Preview("中文 · Unknown · Offline") {
|
||||
RoutingStylePreview(medium: true, dark: true, language: "zh", mode: "unknown", tun: "unknown", proxy: "unknown", error: "unavailable")
|
||||
RoutingStylePreview(
|
||||
medium: true, dark: true, language: "zh", mode: "unknown",
|
||||
tun: "unknown", proxy: "unknown", error: "unavailable"
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -46,7 +46,8 @@ struct Provider: TimelineProvider {
|
||||
let status = exchange()
|
||||
let now = Date()
|
||||
let unavailable = status.mode == nil || status.tun == nil || status.proxy == nil
|
||||
completion(Timeline(entries: [Entry(date: now, status: status)], policy: .after(now.addingTimeInterval(unavailable ? 60 : 900))))
|
||||
let refresh = now.addingTimeInterval(unavailable ? 60 : 900)
|
||||
completion(Timeline(entries: [Entry(date: now, status: status)], policy: .after(refresh)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
import Darwin
|
||||
import Foundation
|
||||
|
||||
struct Status: Codable {
|
||||
var mode: String?
|
||||
@@ -11,10 +11,71 @@ struct Status: Codable {
|
||||
|
||||
func localized(_ key: String, language: String? = nil) -> String {
|
||||
let code = language.map { ["jp": "ja", "zh": "zh-Hans", "zhtw": "zh-Hant"][$0] ?? $0 }
|
||||
let bundle = code.flatMap { Bundle.main.path(forResource: $0, ofType: "lproj") }.flatMap(Bundle.init(path:)) ?? .main
|
||||
let path = code.flatMap { Bundle.main.path(forResource: $0, ofType: "lproj") }
|
||||
let bundle = path.flatMap(Bundle.init(path:)) ?? .main
|
||||
return bundle.localizedString(forKey: key, value: nil, table: nil)
|
||||
}
|
||||
|
||||
private func connectSocket(_ path: String) -> Int32? {
|
||||
let descriptor = socket(AF_UNIX, SOCK_STREAM, 0)
|
||||
guard descriptor >= 0 else { return nil }
|
||||
var timeout = timeval(tv_sec: 8, tv_usec: 0)
|
||||
var noSignal: Int32 = 1
|
||||
setsockopt(descriptor, SOL_SOCKET, SO_RCVTIMEO, &timeout, socklen_t(MemoryLayout.size(ofValue: timeout)))
|
||||
setsockopt(descriptor, SOL_SOCKET, SO_SNDTIMEO, &timeout, socklen_t(MemoryLayout.size(ofValue: timeout)))
|
||||
setsockopt(descriptor, SOL_SOCKET, SO_NOSIGPIPE, &noSignal, socklen_t(MemoryLayout.size(ofValue: noSignal)))
|
||||
var address = sockaddr_un()
|
||||
address.sun_family = sa_family_t(AF_UNIX)
|
||||
address.sun_len = UInt8(MemoryLayout<sockaddr_un>.size)
|
||||
path.withCString { source in
|
||||
withUnsafeMutableBytes(of: &address.sun_path) { bytes in
|
||||
bytes.copyMemory(from: UnsafeRawBufferPointer(start: source, count: path.utf8.count + 1))
|
||||
}
|
||||
}
|
||||
let connected = withUnsafePointer(to: &address) { pointer in
|
||||
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
||||
connect(descriptor, $0, socklen_t(MemoryLayout<sockaddr_un>.size))
|
||||
}
|
||||
}
|
||||
guard connected == 0 else {
|
||||
close(descriptor)
|
||||
return nil
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
|
||||
private func sendRequest(_ data: Data, descriptor: Int32) -> Bool {
|
||||
data.withUnsafeBytes { bytes in
|
||||
var offset = 0
|
||||
while offset < bytes.count {
|
||||
let count = write(descriptor, bytes.baseAddress!.advanced(by: offset), bytes.count - offset)
|
||||
if count < 0 && errno == EINTR { continue }
|
||||
guard count > 0 else { return false }
|
||||
offset += count
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func readResponse(descriptor: Int32) -> Status? {
|
||||
var response = Data(count: 4096)
|
||||
var received = 0
|
||||
while received < 4096 {
|
||||
let count = response.withUnsafeMutableBytes { bytes in
|
||||
read(descriptor, bytes.baseAddress!.advanced(by: received), bytes.count - received)
|
||||
}
|
||||
if count < 0 && errno == EINTR { continue }
|
||||
guard count > 0 else { return nil }
|
||||
let end = received + count
|
||||
if let newline = response[received..<end].firstIndex(of: 10) {
|
||||
response.count = newline
|
||||
return try? JSONDecoder().decode(Status.self, from: response)
|
||||
}
|
||||
received = end
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func exchange(action: String = "status", value: String? = nil) -> Status {
|
||||
#if PREVIEW_HOST
|
||||
return Status(mode: "rule", tun: true, proxy: false, language: "en")
|
||||
@@ -28,59 +89,16 @@ func exchange(action: String = "status", value: String? = nil) -> Status {
|
||||
guard let path = socketPath(), path.utf8.count < 104 else {
|
||||
return unavailable("widget.unavailable")
|
||||
}
|
||||
let fd = socket(AF_UNIX, SOCK_STREAM, 0)
|
||||
guard fd >= 0 else { return unavailable("widget.unavailable") }
|
||||
defer { close(fd) }
|
||||
var timeout = timeval(tv_sec: 8, tv_usec: 0)
|
||||
var noSignal: Int32 = 1
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, socklen_t(MemoryLayout.size(ofValue: timeout)))
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, socklen_t(MemoryLayout.size(ofValue: timeout)))
|
||||
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &noSignal, socklen_t(MemoryLayout.size(ofValue: noSignal)))
|
||||
var address = sockaddr_un()
|
||||
address.sun_family = sa_family_t(AF_UNIX)
|
||||
address.sun_len = UInt8(MemoryLayout<sockaddr_un>.size)
|
||||
path.withCString { source in
|
||||
withUnsafeMutableBytes(of: &address.sun_path) { bytes in
|
||||
bytes.copyMemory(from: UnsafeRawBufferPointer(start: source, count: path.utf8.count + 1))
|
||||
}
|
||||
}
|
||||
let connected = withUnsafePointer(to: &address) { pointer in
|
||||
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
||||
connect(fd, $0, socklen_t(MemoryLayout<sockaddr_un>.size))
|
||||
}
|
||||
}
|
||||
guard connected == 0 else { return unavailable("widget.unavailable") }
|
||||
guard authenticPeer(fd, identifier: hostID) else { return unavailable("widget.unavailable") }
|
||||
guard let descriptor = connectSocket(path) else { return unavailable("widget.unavailable") }
|
||||
defer { close(descriptor) }
|
||||
guard authenticPeer(descriptor, identifier: hostID) else { return unavailable("widget.unavailable") }
|
||||
var request = ["action": action]
|
||||
if let value { request["value"] = value }
|
||||
guard var data = try? JSONSerialization.data(withJSONObject: request) else { return unavailable("widget.failed") }
|
||||
guard var data = try? JSONSerialization.data(withJSONObject: request) else {
|
||||
return unavailable("widget.failed")
|
||||
}
|
||||
data.append(10)
|
||||
let sent = data.withUnsafeBytes { bytes in
|
||||
var offset = 0
|
||||
while offset < bytes.count {
|
||||
let count = write(fd, bytes.baseAddress!.advanced(by: offset), bytes.count - offset)
|
||||
if count < 0 && errno == EINTR { continue }
|
||||
guard count > 0 else { return false }
|
||||
offset += count
|
||||
}
|
||||
return true
|
||||
}
|
||||
guard sent else { return unavailable("widget.failed") }
|
||||
var response = Data(count: 4096)
|
||||
var received = 0
|
||||
while received < 4096 {
|
||||
let count = response.withUnsafeMutableBytes { bytes in
|
||||
read(fd, bytes.baseAddress!.advanced(by: received), bytes.count - received)
|
||||
}
|
||||
if count < 0 && errno == EINTR { continue }
|
||||
guard count > 0 else { return unavailable("widget.failed") }
|
||||
let end = received + count
|
||||
if let newline = response[received..<end].firstIndex(of: 10) {
|
||||
response.count = newline
|
||||
return (try? JSONDecoder().decode(Status.self, from: response)) ?? unavailable("widget.failed")
|
||||
}
|
||||
received = end
|
||||
}
|
||||
return unavailable("widget.failed")
|
||||
guard sendRequest(data, descriptor: descriptor) else { return unavailable("widget.failed") }
|
||||
return readResponse(descriptor: descriptor) ?? unavailable("widget.failed")
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user