ClashX.Meta/ClashX/AppDelegate.swift

1552 lines
56 KiB
Swift
Raw Normal View History

2018-06-13 10:44:30 +08:00
//
// AppDelegate.swift
// ClashX
//
2018-08-08 13:47:38 +08:00
// Created by CYC on 2018/6/10.
// Copyright © 2018 yichengchen. All rights reserved.
2018-06-13 10:44:30 +08:00
//
2019-10-20 13:40:50 +08:00
import Alamofire
2018-06-13 10:44:30 +08:00
import Cocoa
2018-08-04 14:33:47 +08:00
import RxCocoa
import RxSwift
2022-07-03 23:03:15 +08:00
import SwiftyJSON
2022-07-27 11:32:08 +08:00
import Yams
import PromiseKit
2023-03-06 15:00:36 +08:00
let statusItemLengthWithSpeed: CGFloat = 72
2022-07-31 10:12:38 +08:00
private let MetaCoreMd5 = "WOSHIZIDONGSHENGCHENGDEA"
2018-06-13 10:44:30 +08:00
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem!
@IBOutlet weak var checkForUpdateMenuItem: NSMenuItem!
2019-10-20 13:40:50 +08:00
@IBOutlet var statusMenu: NSMenu!
@IBOutlet var proxySettingMenuItem: NSMenuItem!
@IBOutlet var autoStartMenuItem: NSMenuItem!
@IBOutlet var proxyModeGlobalMenuItem: NSMenuItem!
@IBOutlet var proxyModeDirectMenuItem: NSMenuItem!
@IBOutlet var proxyModeRuleMenuItem: NSMenuItem!
@IBOutlet var allowFromLanMenuItem: NSMenuItem!
@IBOutlet var proxyModeMenuItem: NSMenuItem!
@IBOutlet var showNetSpeedIndicatorMenuItem: NSMenuItem!
@IBOutlet var dashboardMenuItem: NSMenuItem!
@IBOutlet var separatorLineTop: NSMenuItem!
@IBOutlet var sepatatorLineEndProxySelect: NSMenuItem!
@IBOutlet var configSeparatorLine: NSMenuItem!
@IBOutlet var logLevelMenuItem: NSMenuItem!
@IBOutlet var httpPortMenuItem: NSMenuItem!
@IBOutlet var socksPortMenuItem: NSMenuItem!
@IBOutlet var apiPortMenuItem: NSMenuItem!
2019-11-30 19:29:57 +08:00
@IBOutlet var ipMenuItem: NSMenuItem!
2019-10-20 13:40:50 +08:00
@IBOutlet var remoteConfigAutoupdateMenuItem: NSMenuItem!
@IBOutlet var showProxyGroupCurrentMenuItem: NSMenuItem!
@IBOutlet var copyExportCommandMenuItem: NSMenuItem!
@IBOutlet var copyExportCommandExternalMenuItem: NSMenuItem!
2019-10-28 10:48:22 +08:00
@IBOutlet var experimentalMenu: NSMenu!
2020-07-10 20:59:00 +08:00
@IBOutlet var externalControlSeparator: NSMenuItem!
2022-07-26 09:14:58 +08:00
2022-07-28 11:37:16 +08:00
@IBOutlet var tunModeMenuItem: NSMenuItem!
2022-06-25 18:05:40 +08:00
@IBOutlet var hideUnselecableMenuItem: NSMenuItem!
@IBOutlet var useYacdDashboardMenuItem: NSMenuItem!
2022-07-05 14:38:39 +08:00
@IBOutlet var proxyProvidersMenu: NSMenu!
2022-07-12 12:35:21 +08:00
@IBOutlet var ruleProvidersMenu: NSMenu!
2022-07-12 12:59:32 +08:00
@IBOutlet var proxyProvidersMenuItem: NSMenuItem!
@IBOutlet var ruleProvidersMenuItem: NSMenuItem!
2022-07-05 21:24:28 +08:00
@IBOutlet var snifferMenuItem: NSMenuItem!
2022-07-12 19:02:13 +08:00
@IBOutlet var flushFakeipCacheMenuItem: NSMenuItem!
2022-07-26 09:14:58 +08:00
2022-07-14 00:32:20 +08:00
@IBOutlet var useAlphaMetaMenuItem: NSMenuItem!
@IBOutlet var alphaMetaVersionMenuItem: NSMenuItem!
2022-07-15 10:49:39 +08:00
@IBOutlet var updateAlphaMetaMenuItem: NSMenuItem!
2020-07-10 20:59:00 +08:00
var disposeBag = DisposeBag()
2023-03-01 11:43:48 +08:00
var statusItemView: StatusItemViewProtocol!
2019-02-19 09:45:23 +08:00
var isSpeedTesting = false
2019-10-20 13:40:50 +08:00
2020-05-10 12:17:51 +08:00
var runAfterConfigReload: (() -> Void)?
2023-04-28 22:54:15 +08:00
var helperStatusTimer: Timer?
2022-07-26 09:14:58 +08:00
2020-04-07 23:58:30 +08:00
func applicationWillFinishLaunching(_ notification: Notification) {
Logger.log("applicationWillFinishLaunching")
signal(SIGPIPE, SIG_IGN)
2020-04-07 23:58:30 +08:00
failLaunchProtect()
NSAppleEventManager.shared()
.setEventHandler(self,
andSelector: #selector(handleURL(event:reply:)),
forEventClass: AEEventClass(kInternetEventClass),
andEventID: AEEventID(kAEGetURL))
2020-04-07 23:58:30 +08:00
}
2019-10-20 13:40:50 +08:00
2020-04-07 23:58:30 +08:00
func applicationDidFinishLaunching(_ notification: Notification) {
2022-11-30 13:00:09 +08:00
Logger.log("———————————————————————————————————————————————————————————")
Logger.log("———————————————applicationDidFinishLaunching———————————————")
Logger.log("———————————————————————————————————————————————————————————")
Logger.log("Appversion: \(AppVersionUtil.currentVersion) \(AppVersionUtil.currentBuild)")
ProcessInfo.processInfo.disableSuddenTermination()
2018-12-09 00:06:43 +08:00
// setup menu item first
2019-10-20 13:40:50 +08:00
statusItem = NSStatusBar.system.statusItem(withLength: statusItemLengthWithSpeed)
2023-03-06 15:11:36 +08:00
2023-03-06 15:00:36 +08:00
/*
2023-03-01 11:43:48 +08:00
if #available(macOS 11, *), let button = statusItem.button {
statusItemView = NewStatusMenuView.create(on: button)
} else {
statusItemView = StatusItemView.create(statusItem: statusItem)
}
2023-03-06 15:00:36 +08:00
*/
statusItemView = StatusItemView.create(statusItem: statusItem)
2023-03-06 15:11:36 +08:00
2023-03-01 11:43:48 +08:00
statusItemView.updateSize(width: statusItemLengthWithSpeed)
2018-12-09 00:06:43 +08:00
statusMenu.delegate = self
2020-04-07 23:58:30 +08:00
DispatchQueue.main.async {
self.postFinishLaunching()
}
}
2019-10-20 13:40:50 +08:00
2020-04-07 23:58:30 +08:00
func postFinishLaunching() {
Logger.log("postFinishLaunching")
2020-04-07 23:58:30 +08:00
defer {
statusItem.menu = statusMenu
2023-06-15 14:24:00 +08:00
DispatchQueue.main.asyncAfter(deadline: .now()+5) {
self.checkMenuIconVisable()
}
2020-04-07 23:58:30 +08:00
}
if #unavailable(macOS 10.15) {
// dashboard is not support in macOS 10.15 below
self.dashboardMenuItem.isHidden = true
}
setupStatusMenuItemData()
AppVersionUtil.showUpgradeAlert()
ICloudManager.shared.setup()
setupExperimentalMenuItem()
// install proxy helper
_ = ClashResourceManager.check()
2020-04-21 23:57:06 +08:00
PrivilegedHelperManager.shared.checkInstall()
ConfigFileManager.copySampleConfigIfNeed()
2019-10-20 13:40:50 +08:00
// claer not existed selected model
removeUnExistProxyGroups()
2020-03-04 18:30:53 +08:00
setupData()
2020-05-10 12:17:51 +08:00
runAfterConfigReload = { [weak self] in
2022-06-24 21:17:19 +08:00
self?.selectAllowLanWithMenory()
2020-05-10 12:17:51 +08:00
}
2022-07-26 09:14:58 +08:00
updateLoggingLevel()
2019-10-20 13:40:50 +08:00
// start watch config file change
2020-06-02 19:05:32 +08:00
ConfigManager.watchCurrentConfigFile()
2019-10-20 13:40:50 +08:00
RemoteConfigManager.shared.autoUpdateCheck()
2019-10-20 13:40:50 +08:00
2019-10-15 22:40:02 +08:00
setupNetworkNotifier()
registCrashLogger()
2023-05-26 15:45:33 +08:00
KeyboardShortCutManager.setup()
2023-06-25 09:28:55 +08:00
Hotfixs.applyMacOS14Hotfix(modeItem: proxyModeMenuItem)
2018-06-13 10:44:30 +08:00
}
2019-10-20 13:40:50 +08:00
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
let group = DispatchGroup()
var shouldWait = false
2022-07-26 09:14:58 +08:00
2022-07-16 12:49:21 +08:00
PrivilegedHelperManager.shared.helper()?.stopMeta()
2022-11-27 11:59:38 +08:00
PrivilegedHelperManager.shared.helper()?.updateTun(with: false)
2022-11-10 22:48:35 +08:00
2020-10-24 19:54:01 +08:00
if ConfigManager.shared.proxyPortAutoSet && !ConfigManager.shared.isProxySetByOtherVariable.value || NetworkChangeNotifier.isCurrentSystemSetToClash(looser: true) ||
NetworkChangeNotifier.hasInterfaceProxySetToClash() {
Logger.log("ClashX quit need clean proxy setting")
shouldWait = true
group.enter()
SystemProxyManager.shared.disableProxy(forceDisable: ConfigManager.shared.isProxySetByOtherVariable.value) {
group.leave()
}
}
if !shouldWait {
Logger.log("ClashX quit without clean waiting")
return .terminateNow
}
if statusItem != nil, statusItem.menu != nil {
statusItem.menu = nil
}
disposeBag = DisposeBag()
DispatchQueue.global(qos: .default).async {
let res = group.wait(timeout: .now() + 5)
switch res {
case .success:
Logger.log("ClashX quit after clean up finish")
2020-10-24 19:54:01 +08:00
DispatchQueue.main.asyncAfter(deadline: .now()+0.2) {
NSApp.reply(toApplicationShouldTerminate: true)
}
2020-12-06 14:02:45 +08:00
DispatchQueue.global().asyncAfter(deadline: .now()+1) {
NSApp.reply(toApplicationShouldTerminate: true)
}
case .timedOut:
Logger.log("ClashX quit after clean up timeout")
2020-10-24 19:54:01 +08:00
DispatchQueue.main.async {
NSApp.reply(toApplicationShouldTerminate: true)
}
2020-12-06 14:02:45 +08:00
DispatchQueue.global().asyncAfter(deadline: .now()+1) {
NSApp.reply(toApplicationShouldTerminate: true)
}
}
2018-06-13 10:44:30 +08:00
}
Logger.log("ClashX quit wait for clean up")
return .terminateLater
}
func applicationWillTerminate(_ aNotification: Notification) {
2019-11-09 11:50:51 +08:00
UserDefaults.standard.set(0, forKey: "launch_fail_times")
2020-10-24 19:54:01 +08:00
Logger.log("ClashX will terminate")
if NetworkChangeNotifier.isCurrentSystemSetToClash(looser: true) ||
NetworkChangeNotifier.hasInterfaceProxySetToClash() {
Logger.log("Need Reset Proxy Setting again", level: .error)
2020-10-24 19:54:01 +08:00
SystemProxyManager.shared.disableProxy()
}
2018-06-13 10:44:30 +08:00
}
2023-02-19 19:51:33 +08:00
func checkMenuIconVisable() {
guard let button = statusItem.button else {assertionFailure(); return }
guard let window = button.window else {assertionFailure(); return }
let buttonRect = button.convert(button.bounds, to: nil)
let onScreenRect = window.convertToScreen(buttonRect)
var leftScreenX: CGFloat = 0
2023-06-14 11:17:27 +08:00
for screen in NSScreen.screens where screen.frame.origin.x < leftScreenX {
leftScreenX = screen.frame.origin.x
}
let isMenuIconHidden = onScreenRect.midX < leftScreenX
2023-02-19 19:51:33 +08:00
var isCoverdByNotch = false
if #available(macOS 12, *), NSScreen.screens.count == 1, let screen = NSScreen.screens.first, let leftArea = screen.auxiliaryTopLeftArea, let rightArea = screen.auxiliaryTopRightArea {
if onScreenRect.minX > leftArea.maxX, onScreenRect.maxX<rightArea.minX {
isCoverdByNotch = true
}
}
2023-02-19 19:51:33 +08:00
Logger.log("checkMenuIconVisable: \(onScreenRect) \(leftScreenX), hidden: \(isMenuIconHidden), coverd by notch:\(isCoverdByNotch)")
if (isMenuIconHidden || isCoverdByNotch), !Settings.disableMenubarNotice {
let alert = NSAlert()
alert.messageText = NSLocalizedString("The status icon is coverd or hide by other app.", comment: "")
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Never show again", comment: ""))
if alert.runModal() == .alertSecondButtonReturn {
Settings.disableMenubarNotice = true
}
}
}
2019-10-20 13:40:50 +08:00
2020-04-07 23:58:30 +08:00
func setupStatusMenuItemData() {
ConfigManager.shared
.showNetSpeedIndicatorObservable
2019-10-20 13:40:50 +08:00
.bind { [weak self] show in
guard let self = self else { return }
self.showNetSpeedIndicatorMenuItem.state = (show ?? true) ? .on : .off
2019-10-20 13:40:50 +08:00
let statusItemLength: CGFloat = (show ?? true) ? statusItemLengthWithSpeed : 25
self.statusItem.length = statusItemLength
2023-03-01 11:43:48 +08:00
self.statusItemView.updateSize(width: statusItemLength)
2019-10-20 13:40:50 +08:00
self.statusItemView.showSpeedContainer(show: show ?? true)
}.disposed(by: disposeBag)
2020-04-07 23:58:30 +08:00
statusItemView.updateViewStatus(enableProxy: ConfigManager.shared.proxyPortAutoSet)
LaunchAtLogin.shared
.isEnableVirable
.asObservable()
.subscribe(onNext: { [weak self] enable in
guard let self = self else { return }
self.autoStartMenuItem.state = enable ? .on : .off
}).disposed(by: disposeBag)
remoteConfigAutoupdateMenuItem.state = RemoteConfigManager.autoUpdateEnable ? .on : .off
2022-07-26 09:14:58 +08:00
2022-06-25 18:05:40 +08:00
hideUnselecableMenuItem.state = .init(rawValue: MenuItemFactory.hideUnselectable)
useYacdDashboardMenuItem.state = MenuItemFactory.useYacdDashboard ? .on : .off
useYacdDashboardMenuItem.isHidden = !DashboardManager.shared.enableSwiftUI
2022-07-14 00:32:20 +08:00
useAlphaMetaMenuItem.state = MenuItemFactory.useAlphaCore ? .on : .off
2020-04-07 23:58:30 +08:00
}
2023-04-28 22:54:15 +08:00
2020-04-07 23:58:30 +08:00
func setupData() {
2023-05-24 11:07:26 +08:00
SSIDSuspendTool.shared.setup()
ConfigManager.shared
.showNetSpeedIndicatorObservable.skip(1)
.bind {
_ in
ApiRequest.shared.resetTrafficStreamApi()
2020-04-11 11:30:48 +08:00
}.disposed(by: disposeBag)
2019-10-15 22:40:02 +08:00
Observable
.merge([ConfigManager.shared.proxyPortAutoSetObservable,
2023-05-24 11:07:26 +08:00
ConfigManager.shared.isProxySetByOtherVariable.asObservable(),
ConfigManager.shared.proxyShouldPaused.asObservable()])
.observe(on: MainScheduler.instance)
2019-10-15 22:40:02 +08:00
.map { _ -> NSControl.StateValue in
2023-05-24 11:07:26 +08:00
if (ConfigManager.shared.isProxySetByOtherVariable.value || ConfigManager.shared.proxyShouldPaused.value) && ConfigManager.shared.proxyPortAutoSet {
2019-10-15 22:40:02 +08:00
return .mixed
}
return ConfigManager.shared.proxyPortAutoSet ? .on : .off
2019-10-20 13:40:50 +08:00
}.distinctUntilChanged()
2019-10-15 22:40:02 +08:00
.bind { [weak self] status in
2019-10-20 13:40:50 +08:00
guard let self = self else { return }
2019-10-15 22:40:02 +08:00
self.proxySettingMenuItem.state = status
2022-07-28 13:01:58 +08:00
}.disposed(by: disposeBag)
Observable
.merge([ConfigManager.shared.proxyPortAutoSetObservable,
ConfigManager.shared.isTunModeVariable.asObservable(),
ConfigManager.shared.isProxySetByOtherVariable.asObservable()])
.map { _ -> Bool in
var status = NSControl.StateValue.mixed
if ConfigManager.shared.isProxySetByOtherVariable.value && ConfigManager.shared.proxyPortAutoSet {
} else {
status = ConfigManager.shared.proxyPortAutoSet ? .on : .off
}
return status == .on || ConfigManager.shared.isTunModeVariable.value
}.distinctUntilChanged()
.bind { [weak self] enable in
guard let self = self else { return }
self.statusItemView.updateViewStatus(enableProxy: enable)
2019-10-20 13:40:50 +08:00
}.disposed(by: disposeBag)
let configObservable = ConfigManager.shared
.currentConfigVariable
.asObservable()
2019-10-20 13:40:50 +08:00
Observable.zip(configObservable, configObservable.skip(1))
.filter { _, new in return new != nil }
.bind { [weak self] old, config in
guard let self = self, let config = config else { return }
self.proxyModeDirectMenuItem.state = .off
self.proxyModeGlobalMenuItem.state = .off
self.proxyModeRuleMenuItem.state = .off
2019-10-20 13:40:50 +08:00
switch config.mode {
2019-10-20 13:40:50 +08:00
case .direct: self.proxyModeDirectMenuItem.state = .on
case .global: self.proxyModeGlobalMenuItem.state = .on
case .rule: self.proxyModeRuleMenuItem.state = .on
}
self.allowFromLanMenuItem.state = config.allowLan ? .on : .off
2019-10-20 13:40:50 +08:00
2019-07-28 12:39:49 +08:00
self.proxyModeMenuItem.title = "\(NSLocalizedString("Proxy Mode", comment: "")) (\(config.mode.name))"
2019-10-20 13:40:50 +08:00
2020-06-16 14:30:32 +08:00
if old?.usedHttpPort != config.usedHttpPort || old?.usedSocksPort != config.usedSocksPort {
Logger.log("port config updated,new: \(config.usedHttpPort),\(config.usedSocksPort)")
2020-05-10 12:17:51 +08:00
if ConfigManager.shared.proxyPortAutoSet {
2020-06-16 14:30:32 +08:00
SystemProxyManager.shared.enableProxy(port: config.usedHttpPort, socksPort: config.usedSocksPort)
2020-05-10 12:17:51 +08:00
}
}
2019-10-20 13:40:50 +08:00
2020-06-16 14:30:32 +08:00
self.httpPortMenuItem.title = "Http Port: \(config.usedHttpPort)"
self.socksPortMenuItem.title = "Socks Port: \(config.usedSocksPort)"
2019-11-30 19:29:57 +08:00
self.apiPortMenuItem.title = "Api Port: \(ConfigManager.shared.apiPort)"
self.ipMenuItem.title = "IP: \(NetworkChangeNotifier.getPrimaryIPAddress() ?? "")"
if RemoteControlManager.selectConfig == nil {
ClashStatusTool.checkPortConfig(cfg: config)
}
2019-10-20 13:40:50 +08:00
2022-07-05 21:24:28 +08:00
self.snifferMenuItem.state = config.sniffing ? .on : .off
2022-07-28 11:37:16 +08:00
self.tunModeMenuItem.state = config.tun.enable ? .on : .off
2022-07-28 13:01:58 +08:00
ConfigManager.shared.isTunModeVariable.accept(config.tun.enable)
2019-10-20 13:40:50 +08:00
}.disposed(by: disposeBag)
// start proxy
2023-04-28 22:54:15 +08:00
PrivilegedHelperManager.shared.isHelperReady
.filter({$0})
.take(1)
.observe(on: MainScheduler.instance)
.bind(onNext: { _ in
2023-06-13 14:46:41 +08:00
Logger.log("HelperReady")
2023-04-28 22:54:15 +08:00
self.initMetaCore()
self.startProxy()
}).disposed(by: disposeBag)
2023-06-09 19:47:26 +08:00
helperStatusTimer = Timer.scheduledTimer(withTimeInterval: 300, repeats: true) { timer in
2023-04-28 22:54:15 +08:00
timer.fireDate = .init(timeIntervalSinceNow: 3600)
PrivilegedHelperManager.shared.helper {
Logger.log("Check helper status Error, will try again")
2023-06-09 19:47:26 +08:00
timer.fireDate = .init(timeIntervalSinceNow: 0.3)
2023-04-28 22:54:15 +08:00
}?.getVersion {
Logger.log("Check helper status success \($0 ?? "")")
timer.invalidate()
PrivilegedHelperManager.shared.isHelperReady.accept(true)
}
}
2023-08-07 00:20:25 +08:00
if !PrivilegedHelperManager.shared.isHelperCheckFinished.value {
proxySettingMenuItem.target = nil
tunModeMenuItem.target = nil
PrivilegedHelperManager.shared.isHelperCheckFinished
.filter({$0})
.take(1)
.observe(on: MainScheduler.instance)
.subscribe { [weak self] _ in
guard let self = self else { return }
self.proxySettingMenuItem.target = self
self.tunModeMenuItem.target = self
self.helperStatusTimer?.fire()
}.disposed(by: disposeBag)
}
2023-06-13 14:46:41 +08:00
Logger.log("Fire helperStatusTimer")
2019-10-15 22:40:02 +08:00
}
2022-07-26 09:14:58 +08:00
2022-07-17 14:06:25 +08:00
func setupSystemData() {
if !PrivilegedHelperManager.shared.isHelperCheckFinished.value &&
ConfigManager.shared.proxyPortAutoSet {
PrivilegedHelperManager.shared.isHelperCheckFinished
.filter({$0})
.take(1)
.take(while: {_ in ConfigManager.shared.proxyPortAutoSet})
.observe(on: MainScheduler.instance)
.bind(onNext: { _ in
SystemProxyManager.shared.enableProxy()
}).disposed(by: disposeBag)
} else if ConfigManager.shared.proxyPortAutoSet {
SystemProxyManager.shared.enableProxy()
}
}
2019-10-20 13:40:50 +08:00
2019-10-15 22:40:02 +08:00
func setupNetworkNotifier() {
2021-06-15 20:07:21 +08:00
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
NetworkChangeNotifier.start()
}
2019-10-20 13:40:50 +08:00
2019-10-15 22:40:02 +08:00
NotificationCenter
.default
.rx
.notification(.systemNetworkStatusDidChange)
.observe(on: MainScheduler.instance)
2019-12-10 17:19:07 +08:00
.delay(.milliseconds(200), scheduler: MainScheduler.instance)
2019-10-20 13:40:50 +08:00
.bind { _ in
guard NetworkChangeNotifier.getPrimaryInterface() != nil else { return }
let proxySetted = NetworkChangeNotifier.isCurrentSystemSetToClash()
2019-10-15 22:40:02 +08:00
ConfigManager.shared.isProxySetByOtherVariable.accept(!proxySetted)
if !proxySetted && ConfigManager.shared.proxyPortAutoSet {
let proxiesSetting = NetworkChangeNotifier.getRawProxySetting()
2020-10-24 19:54:01 +08:00
Logger.log("Proxy changed by other process!, current:\(proxiesSetting), is Interface Set: \(NetworkChangeNotifier.hasInterfaceProxySetToClash())", level: .warning)
}
2019-10-20 13:40:50 +08:00
}.disposed(by: disposeBag)
NSWorkspace.shared.notificationCenter.addObserver(
self, selector: #selector(resetProxySettingOnWakeupFromSleep),
name: NSWorkspace.didWakeNotification, object: nil
)
NotificationCenter
.default
.rx
.notification(.systemNetworkStatusIPUpdate).map({ _ in
NetworkChangeNotifier.getPrimaryIPAddress(allowIPV6: false)
})
.startWith(NetworkChangeNotifier.getPrimaryIPAddress(allowIPV6: false))
.distinctUntilChanged()
.skip(1)
.filter { $0 != nil }
.observe(on: MainScheduler.instance)
.debounce(.seconds(5), scheduler: MainScheduler.instance).bind { [weak self] _ in
self?.healthCheckOnNetworkChange()
}.disposed(by: disposeBag)
ConfigManager.shared
.isProxySetByOtherVariable
.asObservable()
.filter { _ in ConfigManager.shared.proxyPortAutoSet }
.distinctUntilChanged()
2023-05-24 11:07:26 +08:00
.filter { $0 }
.filter { _ in !ConfigManager.shared.proxyShouldPaused.value }
.bind { _ in
let rawProxy = NetworkChangeNotifier.getRawProxySetting()
Logger.log("proxy changed to no clashX setting: \(rawProxy)", level: .warning)
NSUserNotificationCenter.default.postProxyChangeByOtherAppNotice()
}.disposed(by: disposeBag)
NotificationCenter
.default
.rx
.notification(.systemNetworkStatusIPUpdate).map({ _ in
NetworkChangeNotifier.getPrimaryIPAddress(allowIPV6: false)
}).bind { [weak self] _ in
if RemoteControlManager.selectConfig != nil {
self?.resetStreamApi()
}
}.disposed(by: disposeBag)
2018-06-13 10:44:30 +08:00
}
2019-10-20 13:40:50 +08:00
func updateProxyList(withMenus menus: [NSMenuItem]) {
let startIndex = statusMenu.items.firstIndex(of: separatorLineTop)! + 1
let endIndex = statusMenu.items.firstIndex(of: sepatatorLineEndProxySelect)!
2019-10-20 00:10:27 +08:00
sepatatorLineEndProxySelect.isHidden = menus.count == 0
2019-10-20 13:40:50 +08:00
for _ in 0..<endIndex - startIndex {
2019-10-20 00:10:27 +08:00
statusMenu.removeItem(at: startIndex)
2019-03-24 13:59:12 +08:00
}
2019-10-20 00:10:27 +08:00
for each in menus {
statusMenu.insertItem(each, at: startIndex)
2019-03-24 13:59:12 +08:00
}
}
2019-10-20 13:40:50 +08:00
2018-11-30 22:14:20 +08:00
func updateConfigFiles() {
2019-10-20 13:40:50 +08:00
guard let menu = configSeparatorLine.menu else { return }
2020-05-10 23:36:43 +08:00
MenuItemFactory.generateSwitchConfigMenuItems {
items in
let lineIndex = menu.items.firstIndex(of: self.configSeparatorLine)!
for _ in 0..<lineIndex {
menu.removeItem(at: 0)
}
for item in items.reversed() {
menu.insertItem(item, at: 0)
}
2019-10-20 00:10:27 +08:00
}
2018-08-04 21:49:32 +08:00
}
2019-10-20 13:40:50 +08:00
2018-08-12 11:29:51 +08:00
func updateLoggingLevel() {
2019-10-20 13:28:40 +08:00
ApiRequest.updateLogLevel(level: ConfigManager.selectLoggingApiLevel)
2019-10-20 13:40:50 +08:00
for item in logLevelMenuItem.submenu?.items ?? [] {
2018-08-12 11:29:51 +08:00
item.state = item.title.lowercased() == ConfigManager.selectLoggingApiLevel.rawValue ? .on : .off
}
NotificationCenter.default.post(name: .reloadDashboard, object: nil)
2018-08-12 11:29:51 +08:00
}
2022-07-26 09:14:58 +08:00
func initMetaCore() {
Logger.log("initClashCore")
2022-07-26 09:14:58 +08:00
2023-03-19 15:13:22 +08:00
let corePath: (String?, String?) = {
2023-03-31 14:41:46 +08:00
guard let alphaCorePath = Paths.alphaCorePath(),
let corePath = Paths.defaultCorePath() else {
return (nil, "Paths error")
}
2023-03-31 14:51:11 +08:00
2023-03-31 14:41:46 +08:00
// alpha core
if let v = testMetaCore(alphaCorePath.path) {
updateAlphaVersion(v.version)
if MenuItemFactory.useAlphaCore {
return (alphaCorePath.path, nil)
}
} else {
updateAlphaVersion(nil)
2023-03-19 15:58:30 +08:00
}
2022-07-31 11:50:33 +08:00
2023-03-31 15:49:03 +08:00
let fm = FileManager.default
2023-03-31 14:41:46 +08:00
// unzip internal core
2023-03-31 15:49:03 +08:00
if !fm.fileExists(atPath: corePath.path) {
if let msg = unzipMetaCore() {
return (nil, msg)
}
} else if !validateDefaultCore() {
try? fm.removeItem(at: corePath)
2023-03-31 14:41:46 +08:00
if let msg = unzipMetaCore() {
return (nil, msg)
2023-03-19 15:13:22 +08:00
}
2023-03-31 14:41:46 +08:00
}
2023-03-31 14:51:11 +08:00
2023-03-31 15:08:56 +08:00
if let msg = testMetaCore(corePath.path) {
Logger.log("version: \(msg.version)")
}
2023-03-31 14:41:46 +08:00
// validate md5
if validateDefaultCore() {
2023-03-31 14:51:11 +08:00
return (corePath.path, nil)
2023-03-31 14:41:46 +08:00
} else {
Logger.log("Failure to verify the internal Meta Core.")
2023-03-31 14:51:11 +08:00
Logger.log(corePath.path)
2023-03-31 14:41:46 +08:00
return (nil, "Failure to verify the internal Meta Core.\nDo NOT replace core file in the resources folder.")
}
2022-07-14 00:09:22 +08:00
}()
2023-03-31 14:41:46 +08:00
2023-03-19 15:13:22 +08:00
if let path = corePath.0 {
RemoteConfigManager.shared.verifyConfigTask.setLaunchPath(path)
PrivilegedHelperManager.shared.helper()?.initMetaCore(withPath: path)
Logger.log("initClashCore finish")
} else {
let msg = corePath.1 ?? "Load internal Meta Core failed."
2023-03-31 14:41:46 +08:00
2023-03-19 15:13:22 +08:00
let alert = NSAlert()
alert.messageText = msg
alert.alertStyle = .warning
alert.addButton(withTitle: NSLocalizedString("Quit", comment: ""))
alert.runModal()
DispatchQueue.main.async {
NSApplication.shared.terminate(nil)
}
}
}
2022-07-26 09:14:58 +08:00
2022-11-06 15:24:42 +08:00
func unzipMetaCore() -> String? {
2023-03-31 14:41:46 +08:00
guard let corePath = Paths.defaultCorePath(),
let gzPath = Paths.defaultCoreGzPath() else { return "Paths error" }
let fm = FileManager.default
2022-11-06 15:24:42 +08:00
do {
2023-03-31 14:41:46 +08:00
let data = try Data(contentsOf: .init(fileURLWithPath: gzPath)).gunzipped()
2022-11-27 12:25:43 +08:00
2023-03-31 14:41:46 +08:00
if !fm.fileExists(atPath: corePath.deletingLastPathComponent().path) {
try fm.createDirectory(at: corePath.deletingLastPathComponent(), withIntermediateDirectories: true)
}
2022-11-27 12:25:43 +08:00
2023-03-31 14:41:46 +08:00
try data.write(to: corePath)
return nil
2022-11-06 15:24:42 +08:00
} catch let error {
2023-03-19 15:58:30 +08:00
let msg = "Unzip Meta failed: \(error)"
Logger.log(msg, level: .error)
2023-03-31 14:41:46 +08:00
return msg
2022-11-06 15:24:42 +08:00
}
}
2022-07-14 00:09:22 +08:00
func testMetaCore(_ path: String) -> (version: String, date: Date?)? {
guard chmodX(path) else { return nil }
2022-07-26 09:14:58 +08:00
2022-07-14 00:09:22 +08:00
let proc = Process()
proc.executableURL = .init(fileURLWithPath: path)
proc.arguments = ["-v"]
let pipe = Pipe()
proc.standardOutput = pipe
do {
try proc.run()
} catch let error {
Logger.log(error.localizedDescription)
return nil
}
proc.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
2022-07-26 09:14:58 +08:00
2022-07-14 00:09:22 +08:00
guard proc.terminationStatus == 0,
let out = String(data: data, encoding: .utf8) else {
return nil
}
2022-07-26 09:14:58 +08:00
2023-04-01 22:13:59 +08:00
Logger.log("test core path: \(path)")
Logger.log("-v out: \(out)")
2023-04-01 13:52:11 +08:00
let outs = out
.split(separator: "\n")
.first {
$0.starts(with: "Clash Meta")
}?.split(separator: " ")
.map(String.init)
guard let outs,
outs.count == 13,
2022-07-14 00:09:22 +08:00
outs[0] == "Clash",
outs[1] == "Meta",
outs[3] == "darwin" else {
return nil
}
2022-07-26 09:14:58 +08:00
2022-07-14 00:09:22 +08:00
let version = outs[2]
2022-07-26 09:14:58 +08:00
2023-04-01 13:52:11 +08:00
let dateString = [outs[7], outs[8], outs[9], outs[10], outs[12]].joined(separator: "-")
let f = DateFormatter()
f.dateFormat = "E-MMM-d-HH:mm:ss-yyyy"
f.timeZone = .init(abbreviation: outs[11])
let date = f.date(from: dateString)
return (version: version, date: date)
2022-07-14 00:09:22 +08:00
}
2022-07-26 09:14:58 +08:00
2022-07-31 10:12:38 +08:00
func validateDefaultCore() -> Bool {
guard let path = Paths.defaultCorePath()?.path,
chmodX(path) else { return false }
2022-07-31 10:12:38 +08:00
#if DEBUG
return true
#endif
let proc = Process()
proc.executableURL = .init(fileURLWithPath: "/sbin/md5")
2023-03-31 14:41:46 +08:00
proc.arguments = ["-q", path]
2022-07-31 10:12:38 +08:00
let pipe = Pipe()
proc.standardOutput = pipe
try? proc.run()
proc.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard proc.terminationStatus == 0,
let out = String(data: data, encoding: .utf8) else {
return false
}
let md5 = out.replacingOccurrences(of: "\n", with: "")
return md5 == MetaCoreMd5
}
2022-07-14 00:09:22 +08:00
func chmodX(_ path: String) -> Bool {
let proc = Process()
proc.executableURL = .init(fileURLWithPath: "/bin/chmod")
proc.arguments = ["+x", path]
do {
try proc.run()
} catch let error {
Logger.log("chmod +x failed. \(error.localizedDescription)")
return false
}
proc.waitUntilExit()
return proc.terminationStatus == 0
}
2019-10-20 13:40:50 +08:00
func syncConfig(completeHandler: (() -> Void)? = nil) {
ApiRequest.requestConfig { config in
ConfigManager.shared.currentConfig = config
completeHandler?()
2018-06-23 21:43:33 +08:00
}
}
2019-10-20 13:40:50 +08:00
2022-11-27 13:41:12 +08:00
func syncConfigWithTun(_ isInit: Bool = false,
_ completeHandler: (() -> Void)? = nil) {
syncConfig {
defer {
completeHandler?()
}
guard let config = ConfigManager.shared.currentConfig else { return }
let enable = config.tun.enable
if isInit, !enable {
Logger.log("tun didn't set")
return
}
PrivilegedHelperManager.shared.helper()?.updateTun(with: enable)
Logger.log("tun state updated, new: \(enable)")
}
}
2018-08-07 15:09:25 +08:00
func resetStreamApi() {
ApiRequest.shared.delegate = self
ApiRequest.shared.resetStreamApis()
2018-06-13 10:44:30 +08:00
}
2019-10-20 13:40:50 +08:00
func updateConfig(configName: String? = nil, showNotification: Bool = true, completeHandler: ((ErrorString?) -> Void)? = nil) {
startProxy()
2019-10-20 13:40:50 +08:00
guard ConfigManager.shared.isRunning else { return }
let config = configName ?? ConfigManager.selectConfigName
2020-02-26 16:10:21 +08:00
ClashProxy.cleanCache()
ApiRequest.requestConfigUpdate(configName: config) {
[weak self] err in
2019-10-20 13:40:50 +08:00
guard let self = self else { return }
defer {
completeHandler?(err)
}
if let error = err {
NSUserNotificationCenter.default
.postNotificationAlert(title: NSLocalizedString("Reload Config Fail", comment: ""),
info: error)
} else {
2022-11-28 20:27:08 +08:00
self.syncConfigWithTun()
self.resetStreamApi()
2020-05-10 12:17:51 +08:00
self.runAfterConfigReload?()
self.runAfterConfigReload = nil
if showNotification {
NSUserNotificationCenter.default
.post(title: NSLocalizedString("Reload Config Succeed", comment: ""),
2020-03-31 20:20:20 +08:00
info: NSLocalizedString("Success", comment: ""))
}
if let newConfigName = configName {
ConfigManager.selectConfigName = newConfigName
}
self.selectProxyGroupWithMemory()
self.selectOutBoundModeWithMenory()
MenuItemFactory.recreateProxyMenuItems()
NotificationCenter.default.post(name: .reloadDashboard, object: nil)
}
}
}
2019-10-20 13:40:50 +08:00
2019-10-28 10:48:22 +08:00
func setupExperimentalMenuItem() {
ConnectionManager.addCloseOptionMenuItem(&experimentalMenu)
2022-07-05 23:26:29 +08:00
// ClashResourceManager.addUpdateMMDBMenuItem(&experimentalMenu)
SystemProxyManager.shared.addDisableRestoreProxyMenuItem(&experimentalMenu)
MenuItemFactory.addExperimentalMenuItem(&experimentalMenu)
2019-12-08 13:38:18 +08:00
if WebPortalManager.hasWebProtal {
WebPortalManager.shared.addWebProtalMenuItem(&statusMenu)
}
2022-11-27 12:25:43 +08:00
2022-07-05 21:47:35 +08:00
// AutoUpgardeManager.shared.setup()
// AutoUpgardeManager.shared.addChanelMenuItem(&experimentalMenu)
2019-10-28 10:48:22 +08:00
updateExperimentalFeatureStatus()
RemoteControlManager.setupMenuItem(separator: externalControlSeparator)
2019-10-28 10:48:22 +08:00
}
func updateExperimentalFeatureStatus() {
showProxyGroupCurrentMenuItem.state = ConfigManager.shared.disableShowCurrentProxyInMenu ? .off : .on
}
@objc func resetProxySettingOnWakeupFromSleep() {
guard !ConfigManager.shared.isProxySetByOtherVariable.value,
ConfigManager.shared.proxyPortAutoSet else { return }
guard NetworkChangeNotifier.getPrimaryInterface() != nil else { return }
if !NetworkChangeNotifier.isCurrentSystemSetToClash() {
let rawProxy = NetworkChangeNotifier.getRawProxySetting()
Logger.log("Resting proxy setting, current:\(rawProxy)", level: .warning)
SystemProxyManager.shared.disableProxy()
SystemProxyManager.shared.enableProxy()
}
if RemoteControlManager.selectConfig != nil {
resetStreamApi()
}
}
@objc func healthCheckOnNetworkChange() {
ApiRequest.getMergedProxyData {
proxyResp in
guard let proxyResp = proxyResp else {return}
var providers = Set<ClashProxyName>()
let groups = proxyResp.proxyGroups.filter({$0.type.isAutoGroup})
for group in groups {
group.all?.compactMap {
proxyResp.proxiesMap[$0]?.enclosingProvider?.name
}.forEach {
providers.insert($0)
}
}
for group in groups {
Logger.log("Start auto health check for group \(group.name)")
ApiRequest.healthCheck(proxy: group.name)
}
for provider in providers {
Logger.log("Start auto health check for provider \(provider)")
ApiRequest.healthCheck(proxy: provider)
2021-07-27 22:22:04 +08:00
}
}
}
}
2022-07-31 23:39:07 +08:00
// MARK: Meta Core
extension AppDelegate {
enum StartMetaError: Error {
case configMissing
case remoteConfigMissing
case startMetaFailed(String)
case helperNotFound
case pushConfigFailed(String)
}
struct StartProxyResp: Codable {
let externalController: String
let secret: String
let log: String?
}
func startProxy() {
if ConfigManager.shared.isRunning { return }
Logger.log("Trying start meta core")
prepareConfigFile().then {
self.generateInitConfig()
}.then {
self.startMeta($0)
}.get { res in
if let log = res.log {
Logger.log("""
\n######## Clash Meta Start Log #########
\(log)
######## END #########
""", level: .info)
}
let port = res.externalController.components(separatedBy: ":").last ?? "9090"
ConfigManager.shared.apiPort = port
ConfigManager.shared.apiSecret = res.secret
ConfigManager.shared.isRunning = true
self.proxyModeMenuItem.isEnabled = true
self.dashboardMenuItem.isEnabled = true
}.then { _ in
self.pushInitConfig()
}.done {
Logger.log("Init config file success.")
2023-06-30 13:36:45 +08:00
self.showUpdateNotification("ClashX_Meta_1.3.0_UpdateTips", info: "Config Floder migrated from\n~/.config/clash to\n~/.config/clash.meta")
2023-06-30 13:36:45 +08:00
2022-07-31 23:39:07 +08:00
}.catch { error in
ConfigManager.shared.isRunning = false
self.proxyModeMenuItem.isEnabled = false
Logger.log("\(error)", level: .error)
let unc = NSUserNotificationCenter.default
switch error {
case StartMetaError.configMissing:
unc.postConfigErrorNotice(msg: "Can't find config.")
case StartMetaError.remoteConfigMissing:
unc.postConfigErrorNotice(msg: "Can't find remote config.")
case StartMetaError.helperNotFound:
unc.postMetaErrorNotice(msg: "Can't connect to helper.")
case StartMetaError.startMetaFailed(let s):
2022-08-01 23:45:37 +08:00
unc.postMetaErrorNotice(msg: s)
2022-07-31 23:39:07 +08:00
case StartMetaError.pushConfigFailed(let s):
unc.postConfigErrorNotice(msg: s)
default:
unc.postMetaErrorNotice(msg: "Unknown Error.")
}
}
}
func prepareConfigFile() -> Promise<()> {
.init { resolver in
let configName = ConfigManager.selectConfigName
ApiRequest.findConfigPath(configName: configName) { path in
guard let path = path else {
resolver.reject(StartMetaError.configMissing)
return
}
if !FileManager.default.fileExists(atPath: path) {
2022-08-01 23:45:37 +08:00
Logger.log("\(configName) not exists")
2022-07-31 23:39:07 +08:00
if let config = RemoteConfigManager.shared.configs.first(where: { $0.name == configName }) {
2022-08-01 23:45:37 +08:00
Logger.log("Try to download remote config \(configName)")
2022-07-31 23:39:07 +08:00
RemoteConfigManager.updateConfig(config: config) {
if let error = $0 {
2022-08-01 23:45:37 +08:00
Logger.log("Download remote config failed, \(error)")
2022-07-31 23:39:07 +08:00
resolver.reject(StartMetaError.remoteConfigMissing)
} else {
2022-08-01 23:45:37 +08:00
Logger.log("Download remote config success")
2022-07-31 23:39:07 +08:00
resolver.fulfill_()
}
}
} else {
2022-08-01 23:45:37 +08:00
if configName != "config" {
ConfigManager.selectConfigName = "config"
}
Logger.log("Try to copy default config")
2022-07-31 23:39:07 +08:00
ICloudManager.shared.setup()
ConfigFileManager.copySampleConfigIfNeed()
resolver.fulfill_()
}
} else {
resolver.fulfill_()
}
}
}
}
func generateInitConfig() -> Promise<ClashMetaConfig.Config> {
Promise { resolver in
ClashMetaConfig.generateInitConfig {
var config = $0
PrivilegedHelperManager.shared.helper {
2023-06-25 12:15:43 +08:00
// resolver.reject(StartMetaError.helperNotFound)
Logger.log("helperNotFound, getUsedPorts failed", level: .error)
resolver.fulfill(config)
2022-07-31 23:39:07 +08:00
}?.getUsedPorts {
config.updatePorts($0 ?? "")
resolver.fulfill(config)
}
}
}
}
func startMeta(_ config: ClashMetaConfig.Config) -> Promise<StartProxyResp> {
.init { resolver in
PrivilegedHelperManager.shared.helper {
2023-06-25 12:15:43 +08:00
Logger.log("helperNotFound, startMeta failed", level: .error)
2022-07-31 23:39:07 +08:00
resolver.reject(StartMetaError.helperNotFound)
}?.startMeta(withConfPath: kConfigFolderPath,
confFilePath: config.path) {
if let string = $0 {
guard let jsonData = string.data(using: .utf8),
let res = try? JSONDecoder().decode(StartProxyResp.self, from: jsonData) else {
resolver.reject(StartMetaError.startMetaFailed(string))
return
}
resolver.fulfill(res)
} else {
resolver.reject(StartMetaError.startMetaFailed($0 ?? "unknown error"))
}
}
}
}
func pushInitConfig() -> Promise<()> {
.init { resolver in
ClashProxy.cleanCache()
let configName = ConfigManager.selectConfigName
Logger.log("Push init config file: \(configName)")
ApiRequest.requestConfigUpdate(configName: configName) { err in
if let error = err {
resolver.reject(StartMetaError.pushConfigFailed(error))
} else {
2022-11-27 13:41:12 +08:00
self.syncConfigWithTun(true)
2022-07-31 23:39:07 +08:00
self.resetStreamApi()
self.runAfterConfigReload?()
self.runAfterConfigReload = nil
self.selectProxyGroupWithMemory()
MenuItemFactory.recreateProxyMenuItems()
NotificationCenter.default.post(name: .reloadDashboard, object: nil)
resolver.fulfill_()
}
}
}
}
}
// MARK: Main actions
extension AppDelegate {
@IBAction func actionDashboard(_ sender: NSMenuItem?) {
2023-06-05 21:53:53 +08:00
DashboardManager.shared.show(sender)
2020-02-05 21:45:11 +08:00
}
@IBAction func actionAllowFromLan(_ sender: NSMenuItem) {
ApiRequest.updateAllowLan(allow: !ConfigManager.allowConnectFromLan) {
[weak self] in
2019-10-20 13:40:50 +08:00
guard let self = self else { return }
self.syncConfig()
ConfigManager.allowConnectFromLan = !ConfigManager.allowConnectFromLan
}
}
2019-10-20 13:40:50 +08:00
@IBAction func actionStartAtLogin(_ sender: NSMenuItem) {
LaunchAtLogin.shared.isEnabled = !LaunchAtLogin.shared.isEnabled
2018-06-13 10:44:30 +08:00
}
2019-10-20 13:40:50 +08:00
@IBAction func actionSwitchProxyMode(_ sender: NSMenuItem) {
2019-10-20 13:40:50 +08:00
let mode: ClashProxyMode
switch sender {
case proxyModeGlobalMenuItem:
mode = .global
case proxyModeDirectMenuItem:
mode = .direct
case proxyModeRuleMenuItem:
mode = .rule
default:
return
}
2023-05-26 15:45:33 +08:00
switchProxyMode(mode: mode)
}
2023-06-14 11:17:27 +08:00
2023-05-26 15:45:33 +08:00
func switchProxyMode(mode: ClashProxyMode) {
let config = ConfigManager.shared.currentConfig?.copy()
config?.mode = mode
ApiRequest.updateOutBoundMode(mode: mode) { _ in
ConfigManager.shared.currentConfig = config
ConfigManager.selectOutBoundMode = mode
MenuItemFactory.recreateProxyMenuItems()
}
}
2019-10-20 13:40:50 +08:00
@IBAction func actionShowNetSpeedIndicator(_ sender: NSMenuItem) {
ConfigManager.shared.showNetSpeedIndicator = !(sender.state == .on)
}
2019-10-20 13:40:50 +08:00
2023-05-26 15:45:33 +08:00
@IBAction func actionSetSystemProxy(_ sender: Any?) {
var canSaveProxy = true
2023-05-24 11:07:26 +08:00
if ConfigManager.shared.proxyPortAutoSet && ConfigManager.shared.proxyShouldPaused.value {
ConfigManager.shared.proxyPortAutoSet = false
} else if ConfigManager.shared.isProxySetByOtherVariable.value {
2019-10-15 22:40:02 +08:00
// should reset proxy to clashx
ConfigManager.shared.isProxySetByOtherVariable.accept(false)
ConfigManager.shared.proxyPortAutoSet = true
// clear then reset.
canSaveProxy = false
SystemProxyManager.shared.disableProxy(port: 0, socksPort: 0, forceDisable: true)
2019-10-15 22:40:02 +08:00
} else {
ConfigManager.shared.proxyPortAutoSet = !ConfigManager.shared.proxyPortAutoSet
}
2019-10-20 13:40:50 +08:00
if ConfigManager.shared.proxyPortAutoSet {
if canSaveProxy {
SystemProxyManager.shared.saveProxy()
}
2020-06-16 14:30:32 +08:00
SystemProxyManager.shared.enableProxy()
2018-06-13 10:44:30 +08:00
} else {
2020-06-16 14:30:32 +08:00
SystemProxyManager.shared.disableProxy()
2018-06-13 10:44:30 +08:00
}
}
2019-10-20 13:40:50 +08:00
2019-12-17 21:04:11 +08:00
@IBAction func actionCopyExportCommand(_ sender: NSMenuItem) {
2018-06-13 10:44:30 +08:00
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
2020-06-16 14:30:32 +08:00
let port = ConfigManager.shared.currentConfig?.usedHttpPort ?? 0
let socksport = ConfigManager.shared.currentConfig?.usedSocksPort ?? 0
let localhost = "127.0.0.1"
2019-12-17 21:04:11 +08:00
let isLocalhostCopy = sender == copyExportCommandMenuItem
let ip = isLocalhostCopy ? localhost :
NetworkChangeNotifier.getPrimaryIPAddress() ?? localhost
2019-11-06 19:25:29 +08:00
pasteboard.setString("export https_proxy=http://\(ip):\(port) http_proxy=http://\(ip):\(port) all_proxy=socks5://\(ip):\(socksport)", forType: .string)
2018-06-13 10:44:30 +08:00
}
2019-10-20 13:40:50 +08:00
2018-09-23 21:37:11 +08:00
@IBAction func actionSpeedTest(_ sender: Any) {
2019-03-01 17:39:17 +08:00
if isSpeedTesting {
NSUserNotificationCenter.default.postSpeedTestingNotice()
return
}
NSUserNotificationCenter.default.postSpeedTestBeginNotice()
2019-10-20 13:40:50 +08:00
2019-02-19 09:45:23 +08:00
isSpeedTesting = true
2019-10-20 13:40:50 +08:00
ApiRequest.getMergedProxyData { [weak self] resp in
let group = DispatchGroup()
for (name, _) in resp?.enclosingProviderResp?.providers ?? [:] {
group.enter()
ApiRequest.healthCheck(proxy: name) {
group.leave()
}
}
for p in resp?.proxiesMap["GLOBAL"]?.all ?? [] {
group.enter()
ApiRequest.getProxyDelay(proxyName: p) { _ in
group.leave()
2019-02-19 09:45:23 +08:00
}
}
group.notify(queue: DispatchQueue.main) {
2019-02-19 09:45:23 +08:00
NSUserNotificationCenter.default.postSpeedTestFinishNotice()
2019-03-01 17:39:17 +08:00
self?.isSpeedTesting = false
}
2019-02-19 09:45:23 +08:00
}
}
2019-10-20 13:40:50 +08:00
@IBAction func actionQuit(_ sender: Any) {
NSApplication.shared.terminate(self)
}
}
// MARK: Streaming Info
2019-10-20 13:40:50 +08:00
extension AppDelegate: ApiRequestStreamDelegate {
func didUpdateTraffic(up: Int, down: Int) {
2019-11-21 22:39:10 +08:00
statusItemView.updateSpeedLabel(up: up, down: down)
}
2019-10-20 13:40:50 +08:00
func didGetLog(log: String, level: String) {
Logger.log(log, level: ClashLogLevel(rawValue: level) ?? .unknow)
}
}
// MARK: Help actions
2019-10-20 13:40:50 +08:00
extension AppDelegate {
2023-05-26 15:45:33 +08:00
@IBAction func actionShowLog(_ sender: Any?) {
NSWorkspace.shared.openFile(Logger.shared.logFilePath())
}
}
// MARK: Config actions
extension AppDelegate {
2018-06-13 10:44:30 +08:00
@IBAction func openConfigFolder(_ sender: Any) {
2022-11-20 12:08:46 +08:00
if ICloudManager.shared.useiCloud.value {
ICloudManager.shared.getUrl {
url in
if let url = url {
NSWorkspace.shared.open(url)
}
}
} else {
NSWorkspace.shared.openFile(kConfigFolderPath)
}
2018-06-13 10:44:30 +08:00
}
2019-10-20 13:40:50 +08:00
@IBAction func actionUpdateConfig(_ sender: AnyObject) {
updateConfig()
2018-06-13 10:44:30 +08:00
}
2019-10-20 13:40:50 +08:00
2018-08-12 11:29:51 +08:00
@IBAction func actionSetLogLevel(_ sender: NSMenuItem) {
let level = ClashLogLevel(rawValue: sender.title.lowercased()) ?? .unknow
ConfigManager.selectLoggingApiLevel = level
updateLoggingLevel()
resetStreamApi()
}
2019-10-20 13:40:50 +08:00
@IBAction func actionAutoUpdateRemoteConfig(_ sender: Any) {
RemoteConfigManager.autoUpdateEnable = !RemoteConfigManager.autoUpdateEnable
remoteConfigAutoupdateMenuItem.state = RemoteConfigManager.autoUpdateEnable ? .on : .off
}
2019-10-20 13:40:50 +08:00
@IBAction func actionUpdateRemoteConfig(_ sender: Any) {
RemoteConfigManager.shared.updateCheck(ignoreTimeLimit: true, showNotification: true)
}
@IBAction func actionSetUpdateInterval(_ sender: Any) {
RemoteConfigManager.showAdd()
}
2019-10-20 13:40:50 +08:00
@IBAction func actionUpdateProxyGroupMenu(_ sender: Any) {
ConfigManager.shared.disableShowCurrentProxyInMenu = !ConfigManager.shared.disableShowCurrentProxyInMenu
updateExperimentalFeatureStatus()
2020-11-14 15:01:28 +08:00
MenuItemFactory.recreateProxyMenuItems()
}
2019-10-20 13:40:50 +08:00
2019-10-17 19:38:04 +08:00
@IBAction func actionSetBenchmarkUrl(_ sender: Any) {
let alert = NSAlert()
let textfiled = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 20))
textfiled.stringValue = ConfigManager.shared.benchMarkUrl
alert.messageText = NSLocalizedString("Benchmark", comment: "")
alert.accessoryView = textfiled
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
2019-10-20 13:40:50 +08:00
2019-10-17 19:38:04 +08:00
if alert.runModal() == .alertFirstButtonReturn {
if textfiled.stringValue.isUrlVaild() {
ConfigManager.shared.benchMarkUrl = textfiled.stringValue
} else {
let err = NSAlert()
err.messageText = NSLocalizedString("URL is not valid", comment: "")
err.runModal()
}
}
}
2018-12-09 00:06:43 +08:00
}
2023-06-30 13:36:45 +08:00
// MARK: Meta Update Notification
extension AppDelegate {
func showUpdateNotification(_ udString: String, info: String) {
guard !UserDefaults.standard.bool(forKey: udString) else { return }
UserDefaults.standard.set(true, forKey: udString)
NSUserNotificationCenter.default
.postNotificationAlert(title: "Update Tips", info: info)
}
}
2022-06-25 18:05:40 +08:00
// MARK: Meta Menu
extension AppDelegate {
@IBAction func actionSetTunMode(_ sender: NSMenuItem?) {
let enable = tunModeMenuItem.state != .on
tunModeMenuItem.isEnabled = false
2022-11-27 13:41:12 +08:00
ApiRequest.updateTun(enable: enable) {
self.syncConfigWithTun {
self.tunModeMenuItem.state = enable ? .on : .off
self.tunModeMenuItem.isEnabled = true
2022-11-27 13:41:12 +08:00
}
2022-07-28 11:37:16 +08:00
}
}
2022-06-25 18:05:40 +08:00
@IBAction func hideUnselectable(_ sender: NSMenuItem) {
var newState = NSControl.StateValue.off
switch sender.state {
case .off:
newState = .mixed
case .mixed:
newState = .on
case .on:
newState = .off
default:
return
}
2022-07-03 22:08:38 +08:00
2022-06-25 18:05:40 +08:00
sender.state = newState
MenuItemFactory.hideUnselectable = newState.rawValue
}
@IBAction func useYacdDashboard(_ sender: NSMenuItem) {
guard DashboardManager.shared.enableSwiftUI else { return }
let useYacd = sender.state == .off
sender.state = useYacd ? .on : .off
MenuItemFactory.useYacdDashboard = useYacd
}
2022-07-26 09:14:58 +08:00
2022-07-03 23:03:15 +08:00
@IBAction func checkForUpdate(_ sender: NSMenuItem) {
let unc = NSUserNotificationCenter.default
AF.request("https://api.github.com/repos/MetaCubeX/ClashX.Meta/releases/latest").responseString {
guard $0.error == nil,
let data = $0.data,
let tagName = try? JSON(data: data)["tag_name"].string else {
2022-08-07 12:11:03 +08:00
unc.postUpdateNotice(msg: NSLocalizedString("Some thing failed.", comment: ""))
2022-07-03 23:03:15 +08:00
return
}
2022-07-26 09:14:58 +08:00
2022-07-03 23:03:15 +08:00
if tagName != AppVersionUtil.currentVersion {
let alert = NSAlert()
2022-08-07 12:11:03 +08:00
alert.messageText = NSLocalizedString("Open github release page to download ", comment: "") + "\(tagName)"
2022-07-03 23:03:15 +08:00
alert.addButton(withTitle: NSLocalizedString("OK", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
if alert.runModal() == .alertFirstButtonReturn {
NSWorkspace.shared.open(.init(string: "https://github.com/MetaCubeX/ClashX.Meta/releases/latest")!)
}
} else {
2022-08-07 12:11:03 +08:00
unc.postUpdateNotice(msg: NSLocalizedString("No new release found.", comment: ""))
2022-07-03 23:03:15 +08:00
}
}
}
2022-07-26 09:14:58 +08:00
2022-07-05 21:06:17 +08:00
@IBAction func updateGEO(_ sender: NSMenuItem) {
2022-07-26 09:14:58 +08:00
ApiRequest.updateGEO { _ in
2022-08-07 12:11:03 +08:00
NSUserNotificationCenter.default.post(title: NSLocalizedString("Updating GEO Databases...", comment: ""), info: NSLocalizedString("Good luck to you 🙃", comment: ""))
2022-07-05 21:06:17 +08:00
}
}
2022-07-26 09:14:58 +08:00
2022-07-12 19:02:13 +08:00
@IBAction func flushFakeipCache(_ sender: NSMenuItem) {
2022-07-26 09:14:58 +08:00
ApiRequest.flushFakeipCache {
2022-08-07 12:11:03 +08:00
NSUserNotificationCenter.default.post(title: NSLocalizedString("Flush fake-ip cache", comment: ""), info: $0 ? "Success" : "Failed")
2022-07-12 19:02:13 +08:00
}
}
2022-07-26 09:14:58 +08:00
2022-07-05 21:24:28 +08:00
@IBAction func updateSniffing(_ sender: NSMenuItem) {
let enable = sender.state != .on
ApiRequest.updateSniffing(enable: enable) {
sender.state = enable ? .on : .off
}
}
2022-07-26 09:14:58 +08:00
2022-07-14 00:32:20 +08:00
@IBAction func useAlphaMeta(_ sender: NSMenuItem) {
2022-07-31 10:31:33 +08:00
if UserDefaults.standard.object(forKey: "useAlphaCore") as? Bool == nil {
let alert = NSAlert()
2022-08-05 15:59:56 +08:00
alert.messageText = NSLocalizedString("Alpha Meta core Warning", comment: "")
2022-07-31 10:31:33 +08:00
alert.alertStyle = .warning
alert.addButton(withTitle: NSLocalizedString("Continue", comment: ""))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: ""))
if alert.runModal() != .alertFirstButtonReturn {
return
}
}
2022-07-14 00:32:20 +08:00
let use = sender.state != .on
MenuItemFactory.useAlphaCore = use
sender.state = use ? .on : .off
}
2022-07-26 09:14:58 +08:00
2022-07-14 00:32:20 +08:00
@IBAction func showAlphaInFinder(_ sender: NSMenuItem) {
guard let u = Paths.alphaCorePath(),
FileManager.default.fileExists(atPath: u.path) else { return }
NSWorkspace.shared.activateFileViewerSelecting([u])
}
2022-07-26 09:14:58 +08:00
2022-07-14 00:32:20 +08:00
@IBAction func updateAlphaMeta(_ sender: NSMenuItem) {
2022-07-14 20:08:00 +08:00
sender.isEnabled = false
2022-07-26 09:14:58 +08:00
2023-04-01 21:13:42 +08:00
AlphaMetaDownloader.alphaAsset().then {
2023-04-01 15:23:13 +08:00
AlphaMetaDownloader.checkVersion($0)
}.then {
AlphaMetaDownloader.downloadCore($0)
}.then {
AlphaMetaDownloader.replaceCore($0)
}.done {
self.updateAlphaVersion($0)
let msg = NSLocalizedString("Version: ", comment: "") + $0
NSUserNotificationCenter.default.post(title: "Clash Meta Core", info: msg)
}.ensure {
sender.isEnabled = true
}.catch {
let error = $0 as? AlphaMetaDownloader.errors
NSUserNotificationCenter.default.post(title: "Clash Meta Core", info: error?.des() ?? "")
}
2022-07-14 00:32:20 +08:00
}
2022-07-26 09:14:58 +08:00
2022-07-14 00:32:20 +08:00
func updateAlphaVersion(_ version: String?) {
let enable = version != nil
useAlphaMetaMenuItem.isEnabled = enable
alphaMetaVersionMenuItem.isEnabled = enable
if let v = version {
2022-08-07 12:11:03 +08:00
let info = NSLocalizedString("Version: ", comment: "") + v
2022-07-14 00:32:20 +08:00
alphaMetaVersionMenuItem.title = info
2022-08-07 12:11:03 +08:00
updateAlphaMetaMenuItem.title = NSLocalizedString("Update Alpha Meta core", comment: "")
2022-07-14 00:32:20 +08:00
} else {
2022-08-05 15:59:56 +08:00
alphaMetaVersionMenuItem.title = NSLocalizedString("Version: ", comment: "") + "none"
updateAlphaMetaMenuItem.title = NSLocalizedString("Download Meta core", comment: "")
2022-07-14 00:32:20 +08:00
}
}
2022-06-25 18:05:40 +08:00
}
// MARK: crash hanlder
2019-10-20 13:40:50 +08:00
2018-12-09 00:06:43 +08:00
extension AppDelegate {
func registCrashLogger() {
2022-06-25 12:09:27 +08:00
/*
#if DEBUG
return
#else
2020-03-13 18:17:34 +08:00
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
2020-11-14 15:06:46 +08:00
AppCenter.start(withAppSecret: "dce6e9a3-b6e3-4fd2-9f2d-35c767a99663", services: [
Analytics.self,
Crashes.self
2020-03-13 18:17:34 +08:00
])
}
#endif
2022-06-25 12:09:27 +08:00
*/
2018-12-09 00:06:43 +08:00
}
2019-10-20 13:40:50 +08:00
func failLaunchProtect() {
#if DEBUG
return
#else
UserDefaults.standard.register(defaults: ["NSApplicationCrashOnExceptions": true])
let x = UserDefaults.standard
var launch_fail_times: Int = 0
if let xx = x.object(forKey: "launch_fail_times") as? Int { launch_fail_times = xx }
launch_fail_times += 1
x.set(launch_fail_times, forKey: "launch_fail_times")
if launch_fail_times > 3 {
//
ConfigFileManager.backupAndRemoveConfigFile()
2023-03-08 16:47:59 +08:00
let ruleFiles = ClashResourceManager.RuleFiles.self
2023-03-08 12:27:37 +08:00
try? FileManager.default.removeItem(atPath: kConfigFolderPath + ruleFiles.mmdb.rawValue)
try? FileManager.default.removeItem(atPath: kConfigFolderPath + ruleFiles.geosite.rawValue)
try? FileManager.default.removeItem(atPath: kConfigFolderPath + ruleFiles.geoip.rawValue)
2023-03-08 16:47:59 +08:00
if let domain = Bundle.main.bundleIdentifier {
UserDefaults.standard.removePersistentDomain(forName: domain)
UserDefaults.standard.synchronize()
}
NSUserNotificationCenter.default.post(title: "Fail on launch protect", info: "You origin Config has been renamed", notiOnly: false)
}
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + Double(Int64(5 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: {
x.set(0, forKey: "launch_fail_times")
})
#endif
2018-12-09 00:06:43 +08:00
}
2018-06-13 10:44:30 +08:00
}
// MARK: Memory
2019-10-20 13:40:50 +08:00
extension AppDelegate {
2019-10-20 13:40:50 +08:00
func selectProxyGroupWithMemory() {
2019-11-02 00:04:42 +08:00
let copy = [SavedProxyModel](ConfigManager.selectedProxyRecords)
for item in copy {
2019-11-02 11:55:18 +08:00
guard item.config == ConfigManager.selectConfigName else { continue }
2019-12-11 20:27:17 +08:00
Logger.log("Auto selecting \(item.group) \(item.selected)", level: .debug)
ApiRequest.updateProxyGroup(group: item.group, selectProxy: item.selected) { success in
2019-10-20 13:40:50 +08:00
if !success {
2019-11-02 00:04:42 +08:00
ConfigManager.selectedProxyRecords.removeAll { model -> Bool in
2019-12-11 20:27:17 +08:00
return model.key == item.key
}
}
}
}
}
2019-11-02 11:55:18 +08:00
func removeUnExistProxyGroups() {
let action: (([String]) -> Void) = { list in
let unexists = ConfigManager.selectedProxyRecords.filter {
!list.contains($0.config)
}
ConfigManager.selectedProxyRecords.removeAll {
unexists.contains($0)
}
}
2022-11-20 12:08:46 +08:00
if ICloudManager.shared.useiCloud.value {
ICloudManager.shared.getConfigFilesList { list in
action(list)
}
} else {
let list = ConfigManager.getConfigFilesList()
action(list)
}
}
2019-10-20 13:40:50 +08:00
func selectOutBoundModeWithMenory() {
2019-10-20 13:40:50 +08:00
ApiRequest.updateOutBoundMode(mode: ConfigManager.selectOutBoundMode) {
2019-03-24 13:59:12 +08:00
[weak self] _ in
ConnectionManager.closeAllConnection()
2019-03-24 13:59:12 +08:00
self?.syncConfig()
}
}
2019-10-20 13:40:50 +08:00
func selectAllowLanWithMenory() {
2019-10-20 13:40:50 +08:00
ApiRequest.updateAllowLan(allow: ConfigManager.allowConnectFromLan) {
2019-03-24 13:59:12 +08:00
[weak self] in
self?.syncConfig()
}
}
2020-11-14 15:01:28 +08:00
func hasMenuSelected() -> Bool {
if #available(macOS 11, *) {
return statusMenu.items.contains { $0.state == .on }
} else {
return true
}
}
}
// MARK: NSMenuDelegate
2019-10-20 13:40:50 +08:00
2019-09-14 18:05:36 +08:00
extension AppDelegate: NSMenuDelegate {
func menuNeedsUpdate(_ menu: NSMenu) {
2022-09-10 16:40:02 +08:00
guard ConfigManager.shared.isRunning else { return }
MenuItemFactory.refreshExistingMenuItems()
updateConfigFiles()
syncConfig()
2020-11-14 15:01:28 +08:00
NotificationCenter.default.post(name: .proxyMeneViewShowLeftPadding,
object: nil,
userInfo: ["show": hasMenuSelected()])
}
func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?) {
menu.items.forEach {
($0.view as? ProxyGroupMenuHighlightDelegate)?.highlight(item: item)
}
}
func menuDidClose(_ menu: NSMenu) {
menu.items.forEach {
($0.view as? ProxyGroupMenuHighlightDelegate)?.highlight(item: nil)
}
}
2019-09-14 18:05:36 +08:00
}
2019-09-14 18:05:36 +08:00
// MARK: URL Scheme
2019-10-20 13:40:50 +08:00
2019-09-14 18:05:36 +08:00
extension AppDelegate {
@objc func handleURL(event: NSAppleEventDescriptor, reply: NSAppleEventDescriptor) {
guard let url = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue else {
return
}
2019-10-20 13:40:50 +08:00
2019-09-14 18:05:36 +08:00
guard let components = URLComponents(string: url),
2020-11-14 15:01:28 +08:00
let scheme = components.scheme,
scheme.hasPrefix("clash"),
2020-11-14 15:01:28 +08:00
let host = components.host
2019-10-20 13:40:50 +08:00
else { return }
2019-09-14 18:05:36 +08:00
if host == "install-config" {
guard let url = components.queryItems?.first(where: { item in
item.name == "url"
2019-10-20 13:40:50 +08:00
})?.value else { return }
var userInfo = ["url": url]
2019-09-14 18:05:36 +08:00
if let name = components.queryItems?.first(where: { item in
item.name == "name"
})?.value {
userInfo["name"] = name
}
2019-10-20 13:40:50 +08:00
2019-09-14 18:05:36 +08:00
remoteConfigAutoupdateMenuItem.menu?.performActionForItem(at: 0)
2019-10-20 13:40:50 +08:00
2019-09-14 18:05:36 +08:00
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
NotificationCenter.default.post(name: Notification.Name(rawValue: "didGetUrl"), object: nil, userInfo: userInfo)
}
} else if host == "update-config" {
updateConfig()
2019-09-14 18:05:36 +08:00
}
}
}