diff --git a/V2rayU/AppDelegate.swift b/V2rayU/AppDelegate.swift index ce884d7..784e485 100644 --- a/V2rayU/AppDelegate.swift +++ b/V2rayU/AppDelegate.swift @@ -42,22 +42,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } - AppCenter.start(withAppSecret: "d52dd1a1-7a3a-4143-b159-a30434f87713", services:[ - Analytics.self, - Crashes.self - ]) - - // auto check updates - if UserDefaults.getBool(forKey: .autoCheckVersion) { - menuController.checkV2rayUVersion() - // check version - V2rayUpdater.checkForUpdatesInBackground() - } - - _ = GeneratePACFile(rewrite: true) - // start http server for pac - V2rayLaunch.startHttpServer() - // wake and sleep NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(onSleepNote(note:)), name: NSWorkspace.willSleepNotification, object: nil) NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(onWakeNote(note:)), name: NSWorkspace.didWakeNotification, object: nil) @@ -78,6 +62,23 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Register global hotkey ShortcutsController.bindShortcuts() + + // appcenter init + AppCenter.start(withAppSecret: "d52dd1a1-7a3a-4143-b159-a30434f87713", services:[ + Analytics.self, + Crashes.self + ]) + + // auto check updates + if UserDefaults.getBool(forKey: .autoCheckVersion) { + menuController.checkV2rayUVersion() + // check version + V2rayUpdater.checkForUpdatesInBackground() + } + + _ = GeneratePACFile(rewrite: true) + // start http server for pac + V2rayLaunch.startHttpServer() } func checkDefault() { @@ -94,6 +95,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { if UserDefaults.get(forKey: .runMode) == nil { UserDefaults.set(forKey: .runMode, value: RunMode.pac.rawValue) } + V2rayServer.loadConfig() if V2rayServer.count() == 0 { // add default V2rayServer.add(remark: "default", json: "", isValid: false) diff --git a/V2rayU/MainMenu.swift b/V2rayU/MainMenu.swift index 7df8e17..d3f61f4 100644 --- a/V2rayU/MainMenu.swift +++ b/V2rayU/MainMenu.swift @@ -312,8 +312,10 @@ class MenuController: NSObject, NSMenuDelegate { // set URLSessionDataDelegate let metric = PingMetrics() metric.ping = ping + let config = getProxyUrlSessionConfigure() + config.timeoutIntervalForRequest = 2 // url request - let session = URLSession(configuration: getProxyUrlSessionConfigure(), delegate: metric, delegateQueue: nil) + let session = URLSession(configuration: config, delegate: metric, delegateQueue: nil) let url = URL(string: "http://www.google.com/generate_204")! let task = session.dataTask(with: URLRequest(url: url)){(data: Data?, response: URLResponse?, error: Error?) in NSLog("ping current end") @@ -431,7 +433,9 @@ class MenuController: NSObject, NSMenuDelegate { func showServers() { // reomve old items - serverItems.submenu?.removeAllItems() + if serverItems.submenu != nil { + serverItems.submenu?.removeAllItems() + } let curSer = UserDefaults.get(forKey: .v2rayCurrentServerName) // reload servers V2rayServer.loadConfig() diff --git a/V2rayU/Ping.swift b/V2rayU/Ping.swift index 44d2e24..8a596e7 100644 --- a/V2rayU/Ping.swift +++ b/V2rayU/Ping.swift @@ -15,11 +15,13 @@ var fastV2rayName = "" var fastV2raySpeed = 5 var ping = PingSpeed() let second: Double = 1000000 - +let pingURL = URL(string: "http://www.google.com/generate_204")! class PingSpeed: NSObject { - var pingServers: [V2rayItem] = [] - var serverLen: Int = 0 + var unpingServers: Dictionary = [String: Bool]() + + let lock = NSLock() + let semaphore = DispatchSemaphore(value: 10) // work pool func pingAll() { NSLog("ping start") @@ -28,7 +30,7 @@ class PingSpeed: NSObject { return } fastV2rayName = "" - pingServers = [] + unpingServers = [String: Bool]() let itemList = V2rayServer.list() if itemList.count == 0 { return @@ -51,69 +53,77 @@ class PingSpeed: NSObject { menuController.statusMenu.item(withTag: 1)?.title = pingTip // in ping inPing = true - let pingQueue = DispatchQueue.init(label: "pingQueue") // 串行队列 - // use thread - let thread = Thread { - // ping - self.serverLen = itemList.count - - for item in itemList { - pingQueue.async { - // run ping by async queue - self.pingEachServer(item: item) - } + let pingQueue = DispatchQueue(label: "pingQueue", attributes: .concurrent) // 串行队列 + for item in itemList { + unpingServers[item.name] = true + pingQueue.async { + self.semaphore.wait() + // run ping by async queue + self.pingEachServer(item: item) } - // refresh menu - self.refreshStatusMenu() - NSLog("ping done") } - thread.name = "pingThread" - thread.threadPriority = 1 /// 优先级 - thread.start() } func pingEachServer(item: V2rayItem) { NSLog("ping \(item.name) - \(item.remark)") - - if !item.isValid { - pingServers.append(item) - return - } // ping - doPing(item: item) - // print("finished",message) - pingServers.append(item) - // refresh menu - refreshStatusMenu() + PingServer(item: item).doPing() } - func refreshStatusMenu() { - if pingServers.count == serverLen { + func refreshStatusMenu(item: V2rayItem) { + lock.lock() + defer { lock.unlock() } + + semaphore.signal() + + unpingServers.removeValue(forKey: item.name) + + if unpingServers.count == 0 { inPing = false - menuController.statusMenu.item(withTag: 1)?.title = "Ping Speed..." - menuController.showServers() - // reload config - if menuController.configWindow != nil { - // fix: must be used from main thread only + usleep(useconds_t(1 * second)) + do { DispatchQueue.main.async { - menuController.configWindow.serversTableView.reloadData() + menuController.statusMenu.item(withTag: 1)?.title = "Ping Speed..." + menuController.showServers() + // reload config + if menuController.configWindow != nil { + // fix: must be used from main thread only + menuController.configWindow.serversTableView.reloadData() + } } } } } +} + +class PingServer: NSObject, URLSessionDataDelegate { + var item: V2rayItem + + var bindPort: UInt16 = 0 + var jsonFile: String = "" + var process: Process = Process() + + init(item: V2rayItem) { + self.item = item + super.init() // can actually be omitted in this example because will happen automatically. + } - func doPing(item: V2rayItem) { - let randomInt = Int.random(in: 9000...36500) - let (_, bindPort) = getUsablePort(port: uint16(randomInt)) + func doPing() { + if !item.isValid { + // refresh servers + ping.refreshStatusMenu(item: item) + return + } + let (_, _bindPort) = getUsablePort(port: uint16(Int.random(in: 9000 ... 36500))) - NSLog("doPing: \(item.name)-\(item.remark) - \(bindPort)") - let jsonFile = AppHomePath + "/.config_ping.\(item.name).json" + NSLog("doPing: \(item.name)-\(item.remark) - \(_bindPort)") + bindPort = _bindPort + jsonFile = AppHomePath + "/.config_ping.\(item.name).json" // create v2ray config file - createV2rayJsonFileForPing(item: item, bindPort: bindPort, jsonFile: jsonFile) + createV2rayJsonFileForPing() // Create a Process instance with async launch - let process = Process() // can't use `/bin/bash -c cmd...` otherwize v2ray process will become a ghost process process.launchPath = v2rayCoreFile process.arguments = ["-config", jsonFile] @@ -126,48 +136,18 @@ class PingSpeed: NSObject { } // async launch and can't waitUntilExit process.launch() - + // sleep for wait v2ray process instanse usleep(useconds_t(1 * second)) - // async process - // set URLSessionDataDelegate - let metric = PingMetrics() - metric.ping = item - // url request - let session = URLSession(configuration: getProxyUrlSessionConfigure(httpProxyPort: bindPort), delegate: metric, delegateQueue: nil) - let url = URL(string: "http://www.google.com/generate_204")! - let task = session.dataTask(with: URLRequest(url: url)){(data: Data?, response: URLResponse?, error: Error?) in - self.pingEnd(process: process, jsonFile: jsonFile, bindPort: bindPort) - } + let session = URLSession(configuration: getProxyUrlSessionConfigure(httpProxyPort: bindPort), delegate: self, delegateQueue: nil) + let task = session.dataTask(with: URLRequest(url: pingURL)) task.resume() } - - func pingEnd(process: Process, jsonFile:String, bindPort: UInt16) { - // exit process - if process.isRunning { - // terminate v2ray process - process.terminate() - process.waitUntilExit() - } - - // close port - closePort(port: bindPort) - - // delete config - do { - let jsonFilePath = URL(fileURLWithPath: jsonFile) - try? FileManager.default.removeItem(at: jsonFilePath) - } - - // refresh server - self.refreshStatusMenu() - } - func createV2rayJsonFileForPing(item: V2rayItem, bindPort: UInt16, jsonFile: String) { + func createV2rayJsonFileForPing() { var jsonText = item.json - NSLog("bindPort: \(item.name)-\(item.remark) - \(bindPort)") // parse old let vCfg = V2rayConfig() vCfg.enableSocks = false // just can use one tcp port @@ -181,18 +161,61 @@ class PingSpeed: NSObject { do { let jsonFilePath = URL(fileURLWithPath: jsonFile) - // delete before config if FileManager.default.fileExists(atPath: jsonFile) { try? FileManager.default.removeItem(at: jsonFilePath) } - + // write try jsonText.write(to: jsonFilePath, atomically: true, encoding: String.Encoding.utf8) } catch let error { // failed to write file – bad permissions, bad filename, missing permissions, or more likely it can't be converted to the encoding NSLog("save json file fail: \(error)") } } + + // MARK: - URLSessionDataDelegate + + func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + item.speed = "-1ms" + if let transactionMetrics = metrics.transactionMetrics.first { + let fetchStartDate = transactionMetrics.fetchStartDate + let responseEndDate = transactionMetrics.responseEndDate + // check + if responseEndDate != nil && fetchStartDate != nil { + let requestDuration = responseEndDate!.timeIntervalSince(fetchStartDate!) + let pingTs = Int(requestDuration * 100) + print("PingResult: fetchStartDate=\(fetchStartDate!),responseEndDate=\(responseEndDate!),requestDuration=\(requestDuration),pingTs=\(pingTs)") + // update ping speed + item.speed = String(format: "%d", pingTs) + "ms" + } + } + // save + item.store() + pingEnd() + } + + func pingEnd() { + NSLog("ping end: \(item.remark) - \(item.speed)") + + // refresh servers + ping.refreshStatusMenu(item: item) + + // exit process + if process.isRunning { + // terminate v2ray process + process.terminate() + process.waitUntilExit() + } + + // close port + closePort(port: bindPort) + + // delete config + do { + let jsonFilePath = URL(fileURLWithPath: jsonFile) + try? FileManager.default.removeItem(at: jsonFilePath) + } + } } class PingMetrics: NSObject, URLSessionDataDelegate { diff --git a/V2rayU/Util.swift b/V2rayU/Util.swift index 3135082..42d2c0c 100644 --- a/V2rayU/Util.swift +++ b/V2rayU/Util.swift @@ -491,6 +491,8 @@ func getProxyUrlSessionConfigure() -> URLSessionConfiguration { kCFNetworkProxiesHTTPSPort as AnyHashable: proxyPort, ] } + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + configuration.urlCache = nil configuration.timeoutIntervalForRequest = 30 // Set your desired timeout interval in seconds return configuration @@ -510,7 +512,9 @@ func getProxyUrlSessionConfigure(httpProxyPort: uint16) -> URLSessionConfigurati kCFNetworkProxiesHTTPSProxy as AnyHashable: proxyHost, kCFNetworkProxiesHTTPSPort as AnyHashable: proxyPort, ] - configuration.timeoutIntervalForRequest = 30 // Set your desired timeout interval in seconds + configuration.requestCachePolicy = .reloadIgnoringLocalCacheData + configuration.urlCache = nil + configuration.timeoutIntervalForRequest = 2 // Set your desired timeout interval in seconds return configuration } diff --git a/V2rayU/V2rayLaunch.swift b/V2rayU/V2rayLaunch.swift index bd7422a..aac7ba3 100644 --- a/V2rayU/V2rayLaunch.swift +++ b/V2rayU/V2rayLaunch.swift @@ -119,6 +119,8 @@ class V2rayLaunch: NSObject { v2rayProcess = Process() v2rayProcess.launchPath = v2rayCoreFile v2rayProcess.arguments = ["-config", JsonConfigFilePath] + v2rayProcess.standardError = nil + v2rayProcess.standardOutput = nil v2rayProcess.terminationHandler = { process in if process.terminationStatus != EXIT_SUCCESS { NSLog("process is not kill \(process.description) - \(process.processIdentifier) - \(process.terminationStatus)") diff --git a/V2rayU/V2raySubscribe.swift b/V2rayU/V2raySubscribe.swift index 5150fe2..0b8c1ab 100644 --- a/V2rayU/V2raySubscribe.swift +++ b/V2rayU/V2raySubscribe.swift @@ -314,7 +314,7 @@ class V2raySubSync: NSObject { func importByYaml(strTmp: String, subscribe: String) -> Bool { // parse clash yaml do { - var oldList = getOld(subscribe: subscribe) + let oldList = getOld(subscribe: subscribe) var exists: Dictionary = [String: Bool]() let decoder = YAMLDecoder() @@ -332,7 +332,7 @@ class V2raySubSync: NSObject { // remove not exist for name in oldList { - if exists[name] ?? false { + if !(exists[name] ?? false) { // delete from v2ray UserDefaults V2rayItem.remove(name: name) logTip(title: "remove: ", informativeText: name) @@ -349,7 +349,7 @@ class V2raySubSync: NSObject { } func importByNormal(strTmp: String, subscribe: String) { - var oldList = getOld(subscribe: subscribe) + let oldList = getOld(subscribe: subscribe) var exists: Dictionary = [String: Bool]() let list = strTmp.trimmingCharacters(in: .newlines).components(separatedBy: CharacterSet.newlines) @@ -371,10 +371,10 @@ class V2raySubSync: NSObject { } logTip(title: "need remove?: ", informativeText: "old=\(oldList.count) - new=\(exists.count)") - + // remove not exist for name in oldList { - if exists[name] ?? false { + if !(exists[name] ?? false) { // delete from v2ray UserDefaults V2rayItem.remove(name: name) logTip(title: "remove: ", informativeText: name) @@ -411,7 +411,7 @@ class V2raySubSync: NSObject { if let v2rayOld = V2rayServer.exist(url: newUri) { v2rayOld.json = importUri.json v2rayOld.isValid = importUri.isValid - v2rayOld.name = importUri.remark + v2rayOld.remark = importUri.remark v2rayOld.store() logTip(title: "success update: ", informativeText: importUri.remark) return v2rayOld