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:
Tunglies
2026-09-14 13:59:51 +08:00
parent 143ed48aa9
commit e35a239a06
5 changed files with 121 additions and 77 deletions
+14 -9
View File
@@ -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
}