Skip to content

Commit 6c10ffe

Browse files
authored
Merge pull request #1610 from Anla2z/fix/pac-tun-routing
Fix/pac tun routing
2 parents d7688fd + c2e6d2f commit 6c10ffe

30 files changed

Lines changed: 543 additions & 199 deletions

V2rayU.xcodeproj/project.pbxproj

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@
7979
6632EF3F2EC3720100CED47B /* SubscriptionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6632EF3E2EC371F000CED47B /* SubscriptionStore.swift */; };
8080
6632EF4D2EC4B9E100CED47B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6632EF462EC4B9E100CED47B /* Localizable.strings */; };
8181
6632EF4E2EC4B9E100CED47B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6632EF482EC4B9E100CED47B /* Assets.xcassets */; };
82-
6632EF4F2EC4B9E100CED47B /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6632EF4A2EC4B9E100CED47B /* Info.plist */; };
8382
6632EF502EC4B9E100CED47B /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6632EF492EC4B9E100CED47B /* GoogleService-Info.plist */; };
8483
663814AF2E01938400F5FCF3 /* SubscriptionForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663814AE2E01937700F5FCF3 /* SubscriptionForm.swift */; };
8584
663814B12E0195F700F5FCF3 /* SubscriptionSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663814B02E0195ED00F5FCF3 /* SubscriptionSync.swift */; };
@@ -935,7 +934,6 @@
935934
666A61072F1933A0003654D3 /* install.sh in Resources */,
936935
6632EF4D2EC4B9E100CED47B /* Localizable.strings in Resources */,
937936
6632EF4E2EC4B9E100CED47B /* Assets.xcassets in Resources */,
938-
6632EF4F2EC4B9E100CED47B /* Info.plist in Resources */,
939937
6632EF502EC4B9E100CED47B /* GoogleService-Info.plist in Resources */,
940938
667028E42F0D467E008F27D8 /* pac in Resources */,
941939
);

V2rayU/App/App.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
6767
AppState.shared.appDidLaunch()
6868

6969
// 检查并迁移旧版数据(首次启动时)
70-
await LegacyMigrationHandler.shared.checkAndPromptForMigration()
70+
_ = await LegacyMigrationHandler.shared.checkAndPromptForMigration()
7171
}
7272
}
7373

V2rayU/App/AppMenu.swift

Lines changed: 121 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,72 @@ import SwiftUI
1010
import Combine
1111
import KeyboardShortcuts
1212

13+
final class PingMenuItemView: NSView {
14+
private let titleField = NSTextField(labelWithString: "")
15+
private let clickHandler: () -> Void
16+
private var trackingAreaRef: NSTrackingArea?
17+
18+
init(title: String, clickHandler: @escaping () -> Void) {
19+
self.clickHandler = clickHandler
20+
super.init(frame: NSRect(x: 0, y: 0, width: 240, height: 22))
21+
22+
wantsLayer = true
23+
layer?.cornerRadius = 4
24+
25+
titleField.stringValue = title
26+
titleField.font = .menuFont(ofSize: 0)
27+
titleField.textColor = .labelColor
28+
titleField.backgroundColor = .clear
29+
titleField.isBordered = false
30+
titleField.isEditable = false
31+
titleField.lineBreakMode = .byTruncatingTail
32+
titleField.translatesAutoresizingMaskIntoConstraints = false
33+
34+
addSubview(titleField)
35+
NSLayoutConstraint.activate([
36+
// 对齐系统菜单项文本起始位置,给左侧预留勾选/图标槽位
37+
titleField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24),
38+
titleField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
39+
titleField.centerYAnchor.constraint(equalTo: centerYAnchor),
40+
])
41+
}
42+
43+
@available(*, unavailable)
44+
required init?(coder: NSCoder) {
45+
fatalError("init(coder:) has not been implemented")
46+
}
47+
48+
override func updateTrackingAreas() {
49+
super.updateTrackingAreas()
50+
if let trackingAreaRef {
51+
removeTrackingArea(trackingAreaRef)
52+
}
53+
let options: NSTrackingArea.Options = [.activeAlways, .mouseEnteredAndExited, .inVisibleRect]
54+
let trackingArea = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
55+
addTrackingArea(trackingArea)
56+
trackingAreaRef = trackingArea
57+
}
58+
59+
override func mouseEntered(with event: NSEvent) {
60+
layer?.backgroundColor = NSColor.selectedContentBackgroundColor.withAlphaComponent(0.18).cgColor
61+
}
62+
63+
override func mouseExited(with event: NSEvent) {
64+
layer?.backgroundColor = NSColor.clear.cgColor
65+
}
66+
67+
override func mouseDown(with event: NSEvent) {
68+
clickHandler()
69+
}
70+
71+
func updateTitle(_ title: String) {
72+
titleField.stringValue = title
73+
needsDisplay = true
74+
}
75+
}
76+
1377
@MainActor
14-
final class AppMenuManager: NSObject {
78+
final class AppMenuManager: NSObject, NSMenuDelegate {
1579
static let shared = AppMenuManager()
1680

1781
let versionController = AppVersionController()
@@ -28,6 +92,8 @@ final class AppMenuManager: NSObject {
2892
private var viewErrorLogItem: NSMenuItem!
2993
private var viewLogFilesItem: NSMenuItem!
3094
private var clearLogsItem: NSMenuItem!
95+
private var logsItem: NSMenuItem!
96+
private var logsSubMenu: NSMenu!
3197
private var pacModeItem: NSMenuItem!
3298
private var tunnelModeItem: NSMenuItem!
3399
private var globalModeItem: NSMenuItem!
@@ -51,15 +117,17 @@ final class AppMenuManager: NSObject {
51117
private var helpItem: NSMenuItem!
52118
private var quitItem: NSMenuItem!
53119
private var pingTip: String = ""
120+
private var pingItemView: PingMenuItemView!
54121
private let pingTipSubject = PassthroughSubject<String, Never>()
55122
private var cancellables = Set<AnyCancellable>()
123+
private weak var statusMenu: NSMenu?
56124

57125
override private init() {
58126
super.init()
59127
pingTipSubject // 500毫秒刷新一下,避免很多时一直刷新UI
60128
.throttle(for: .milliseconds(500), scheduler: DispatchQueue.main, latest: true)
61129
.sink { [weak self] tip in
62-
self?.pingItem?.title = String(localized: .LatencyTest) + " \(tip)"
130+
self?.setPingMenuTitle(tip: tip)
63131
}
64132
.store(in: &cancellables)
65133

@@ -194,7 +262,7 @@ final class AppMenuManager: NSObject {
194262
return
195263
}
196264
// coreStatusItem 使用 SwiftUI CoreStatusItemView 自动观察 AppState,无需手动更新 title
197-
pingItem?.title = String(localized: .LatencyTest) + " \(self.pingTip)"
265+
setPingMenuTitle(tip: self.pingTip)
198266
diagnosticsItem?.title = String(localized: .Diagnostics)
199267
toggleCoreItem?.title = AppState.shared.v2rayTurnOn ? String(localized: .TurnCoreOff) : String(localized: .TurnCoreOn)
200268
viewConfigItem?.title = String(localized: .ViewConfigJson)
@@ -203,6 +271,7 @@ final class AppMenuManager: NSObject {
203271
viewErrorLogItem?.title = String(localized: .ViewErrorLog)
204272
viewLogFilesItem?.title = String(localized: .ViewLogFiles)
205273
clearLogsItem?.title = String(localized: .ClearAllLogs)
274+
logsItem?.title = String(localized: .Logs)
206275
pacModeItem?.title = String(localized: .PacMode)
207276
globalModeItem?.title = String(localized: .GlobalMode)
208277
manualModeItem?.title = String(localized: .ManualMode)
@@ -238,31 +307,33 @@ final class AppMenuManager: NSObject {
238307
viewErrorLogItem = NSMenuItem(title: String(localized: .ViewErrorLog), action: #selector(openErrorLogs), keyEquivalent: "")
239308
viewLogFilesItem = NSMenuItem(title: String(localized: .ViewLogFiles), action: #selector(openLogFiles), keyEquivalent: "")
240309
clearLogsItem = NSMenuItem(title: String(localized: .ClearAllLogs), action: #selector(clearLogs), keyEquivalent: "")
310+
logsItem = getLogsItem()
241311
// 配置查看
242312
menu.addItem(toggleCoreItem)
243313
menu.addItem(NSMenuItem.separator())
244314
menu.addItem(viewConfigItem)
245315
menu.addItem(viewPacItem)
246-
menu.addItem(viewLogItem)
247-
menu.addItem(viewErrorLogItem)
248-
menu.addItem(viewLogFilesItem)
249-
menu.addItem(clearLogsItem)
250316
menu.addItem(NSMenuItem.separator())
251317
// 模式切换
252318
pacModeItem = getRunModeItem(mode: .pac, title: String(localized: .PacMode), keyEquivalent: "")
253319
globalModeItem = getRunModeItem(mode: .global, title: String(localized: .GlobalMode), keyEquivalent: "")
254320
manualModeItem = getRunModeItem(mode: .manual, title: String(localized: .ManualMode), keyEquivalent: "")
255321
tunnelModeItem = getRunModeItem(mode: .tun, title: String(localized: .TunnelMode), keyEquivalent: "")
322+
menu.addItem(pacModeItem)
256323
menu.addItem(tunnelModeItem)
257324
menu.addItem(globalModeItem)
258325
menu.addItem(manualModeItem)
259-
menu.addItem(pacModeItem)
260326
menu.addItem(NSMenuItem.separator())
261327
// 路由与服务器
262328
routingItem = getRoutingItem()
263329
serverItem = getServerItem()
264330
// 预先初始化一次
265-
pingItem = NSMenuItem(title: String(localized: .LatencyTest) + "\(self.pingTip)", action: #selector(pingSpeed), keyEquivalent: "")
331+
pingItem = NSMenuItem()
332+
pingItemView = PingMenuItemView(title: String(localized: .LatencyTest)) { [weak self] in
333+
self?.showPingTestingState()
334+
self?.pingSpeedTest()
335+
}
336+
pingItem.view = pingItemView
266337
diagnosticsItem = NSMenuItem(title: String(localized: .Diagnostics), action: #selector(openDiagnostics), keyEquivalent: "")
267338
menu.addItem(NSMenuItem.separator())
268339
menu.addItem(routingItem)
@@ -295,6 +366,7 @@ final class AppMenuManager: NSObject {
295366
// 设置与帮助
296367
checkForUpdatesItem = NSMenuItem(title: String(localized: .CheckForUpdates)+" (V2rayU v\(appVersion))", action: #selector(checkForUpdate), keyEquivalent: "")
297368
helpItem = NSMenuItem(title: String(localized: .Help)+" (Xray-core \(getCoreShortVersion()))", action: #selector(goHelp), keyEquivalent: "")
369+
menu.addItem(logsItem)
298370
menu.addItem(checkForUpdatesItem)
299371
menu.addItem(helpItem)
300372
menu.addItem(NSMenuItem.separator())
@@ -307,9 +379,21 @@ final class AppMenuManager: NSObject {
307379
item.target = self
308380
}
309381

382+
menu.delegate = self
310383
statusItem.menu = menu
384+
statusMenu = menu
311385
self.inited = true
312386
}
387+
388+
private func setPingMenuTitle(tip: String) {
389+
pingTip = tip
390+
guard let pingItem else { return }
391+
let suffix = tip.isEmpty ? "" : " \(tip)"
392+
let title = String(localized: .LatencyTest) + suffix
393+
pingItem.title = title
394+
pingItemView?.updateTitle(title)
395+
statusMenu?.itemChanged(pingItem)
396+
}
313397

314398
func getCoreStatusItem() -> NSMenuItem {
315399
// 使用 SwiftUI 视图替换原始的 title-only 菜单项,CoreStatusItemView 会观察 AppState 自动刷新
@@ -323,6 +407,24 @@ final class AppMenuManager: NSObject {
323407
item.isEnabled = false
324408
return item
325409
}
410+
411+
func getLogsItem() -> NSMenuItem {
412+
viewLogItem.target = self
413+
viewErrorLogItem.target = self
414+
viewLogFilesItem.target = self
415+
clearLogsItem.target = self
416+
417+
logsSubMenu = NSMenu()
418+
logsSubMenu.addItem(viewLogItem)
419+
logsSubMenu.addItem(viewErrorLogItem)
420+
logsSubMenu.addItem(viewLogFilesItem)
421+
logsSubMenu.addItem(NSMenuItem.separator())
422+
logsSubMenu.addItem(clearLogsItem)
423+
424+
let item = NSMenuItem(title: String(localized: .Logs), action: nil, keyEquivalent: "")
425+
item.submenu = logsSubMenu
426+
return item
427+
}
326428

327429
func getRoutingItem() -> NSMenuItem {
328430
// 获取子菜单
@@ -482,6 +584,16 @@ final class AppMenuManager: NSObject {
482584
}
483585
}
484586

587+
func showPingTestingState() {
588+
setPingMenuTitle(tip: " - " + String(localized: .Testing) + "...")
589+
}
590+
591+
func menuNeedsUpdate(_ menu: NSMenu) {
592+
if menu === statusMenu {
593+
setPingMenuTitle(tip: pingTip)
594+
}
595+
}
596+
485597
func importFromPasteboard() {
486598
if let uri = NSPasteboard.general.string(forType: .string), uri.count > 0 {
487599
importUri(url: uri)
@@ -644,11 +756,6 @@ final class AppMenuManager: NSObject {
644756
@objc private func ImportFromPasteboard(_ sender: NSMenuItem) {
645757
importFromPasteboard()
646758
}
647-
648-
@objc private func pingSpeed(_ sender: NSMenuItem) {
649-
pingSpeedTest()
650-
}
651-
652759
@objc private func viewConfig(_ sender: Any) {
653760
openConfigFile()
654761
}

V2rayU/App/AppState.swift

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -197,14 +197,15 @@ final class AppState: ObservableObject {
197197
}
198198

199199
// MARK: - 切换配置
200-
func switchServer(uuid: String) async {
201-
runningProfile = uuid
202-
v2rayTurnOn = true
203-
await setCoreRunning(v2rayTurnOn)
204-
runningServer = ProfileStore.shared.getRunning()
205-
logger.info("switchServer-end: \(self.runningProfile)")
206-
AppMenuManager.shared.refreshServerItems()
207-
}
200+
func switchServer(uuid: String) async {
201+
runningProfile = uuid
202+
v2rayTurnOn = true
203+
await setCoreRunning(v2rayTurnOn)
204+
runningServer = ProfileStore.shared.getRunning()
205+
latency = Double(runningServer?.speed ?? 0)
206+
logger.info("switchServer-end: \(self.runningProfile)")
207+
AppMenuManager.shared.refreshServerItems()
208+
}
208209

209210
// MARK: - App 启动时调用
210211
func appDidLaunch() {
@@ -213,17 +214,19 @@ final class AppState: ObservableObject {
213214
startHttpServer()
214215

215216
// 同步运行中的服务器配置
216-
if let running = ProfileStore.shared.getRunning() {
217-
if runningProfile != running.uuid {
218-
runningProfile = running.uuid
219-
logger.info("appDidLaunch: sync server to \(running.remark)")
220-
}
221-
runningServer = running
222-
} else {
223-
runningProfile = ""
224-
runningServer = nil
225-
logger.info("appDidLaunch: no available server")
226-
}
217+
if let running = ProfileStore.shared.getRunning() {
218+
if runningProfile != running.uuid {
219+
runningProfile = running.uuid
220+
logger.info("appDidLaunch: sync server to \(running.remark)")
221+
}
222+
runningServer = running
223+
latency = Double(running.speed)
224+
} else {
225+
runningProfile = ""
226+
runningServer = nil
227+
latency = 0
228+
logger.info("appDidLaunch: no available server")
229+
}
227230

228231
// 同步运行中的路由配置
229232
let routingManager = RoutingManager()

V2rayU/Core/Database/Protocol.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ extension StoreProtocol {
129129
func fetchAll() -> [Entity] {
130130
do {
131131
return try dbReader.read { db in
132-
try Entity.fetchAll(db)
132+
try Entity.order(Column("sort").asc).fetchAll(db)
133133
}
134134
} catch {
135135
logger.error("fetchAll error: \(error)")

V2rayU/Core/Handlers/CoreConfigHandler.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ class CoreConfigHandler {
3636
extension ProfileEntity {
3737
func AdaptCore() -> CoreType {
3838
var mode: CoreType = .XrayCore
39-
if self.network == .grpc || self.network == .h2 || self.network == .ws {
39+
if self.protocol == .vless && (self.network == .grpc || self.network == .h2 || self.network == .ws) {
4040
mode = .SingBox
4141
}
42-
logger.info("AdaptCore: \(self.network.rawValue) -> \(mode.rawValue)")
42+
logger.info("AdaptCore: \(self.protocol.rawValue)/\(self.network.rawValue) -> \(mode.rawValue)")
4343
return mode
4444
}
4545

V2rayU/Core/Handlers/CoreStatsHandler.swift

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ actor CoreTrafficStatsHandler {
3232
switch coreType {
3333
case .SingBox:
3434
ClashApiStreamHandler.shared.startTask()
35-
await ClashApilatencyHandler.shared.startTask()
3635
case .XrayCore:
3736
await XrayApiStatsHandler.shared.startTask()
3837
}
@@ -42,7 +41,6 @@ actor CoreTrafficStatsHandler {
4241
func stopTask() {
4342
Task {
4443
ClashApiStreamHandler.shared.stopTask()
45-
await ClashApilatencyHandler.shared.stopTask()
4644
await XrayApiStatsHandler.shared.stopTask()
4745
}
4846
}
@@ -138,7 +136,6 @@ actor XrayApiStatsHandler: NSObject {
138136
decoder.dateDecodingStrategy = .iso8601 // 解析日期
139137
// try decode data
140138
let vars: V2rayMetricsVars = try decoder.decode(V2rayMetricsVars.self, from: jsonData)
141-
var latency = 0.0
142139
var directUpLink = 0
143140
var directDownLink = 0
144141
var proxyUpLink = 0
@@ -147,9 +144,6 @@ actor XrayApiStatsHandler: NSObject {
147144
logger.warning("Invalid V2Ray Stats")
148145
return
149146
}
150-
if let latencyValue = vars.observatory?["proxy"] {
151-
latency = latencyValue.delay
152-
}
153147
if let directUpLinkValue = stats.outbound["direct"] {
154148
directUpLink = directUpLinkValue.uplink
155149
directDownLink = directUpLinkValue.downlink
@@ -158,6 +152,7 @@ actor XrayApiStatsHandler: NSObject {
158152
proxyUpLink = proxyUpLinkValue.uplink
159153
proxyDownLink = proxyUpLinkValue.downlink
160154
}
155+
let latency = await AppState.shared.latency
161156
await CoreTrafficStatsHandler.shared.setSpeed(latency: latency, directUpLink: directUpLink, directDownLink: directDownLink, proxyUpLink: proxyUpLink, proxyDownLink: proxyDownLink)
162157
// logger.info("Parsed V2Ray Stats: \(stats)")
163158
} catch {
@@ -206,7 +201,7 @@ actor ClashApilatencyHandler: NSObject {
206201

207202
}
208203

209-
final class ClashApiStreamHandler: NSObject, URLSessionDataDelegate {
204+
final class ClashApiStreamHandler: NSObject, URLSessionDataDelegate, @unchecked Sendable {
210205
static let shared = ClashApiStreamHandler()
211206

212207
private var clashApiSession: URLSession!

0 commit comments

Comments
 (0)