From 39338ab133396ef54691a66ba75b79d0c429b973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 7 Dec 2025 10:24:41 +0800 Subject: [PATCH] Restart service when settings change --- .../Views/Dashboard/ActiveDashboardView.swift | 2 +- .../Dashboard/Cards/CardManagementSheet.swift | 2 +- .../Dashboard/Cards/ProfilePickerSheet.swift | 2 +- .../Overview/OverviewViewModel.swift | 12 +--------- .../Views/Setting/OnDemandRulesView.swift | 16 +++++++++++++ .../Views/Setting/PacketTunnelView.swift | 23 ++++++++++++++++++- .../Views/Setting/ProfileOverrideView.swift | 19 +++++++++++++++ IntentsExtension/Intents.swift | 8 ++----- Library/Network/ExtensionProfile.swift | 13 +++++++++++ Localizable.xcstrings | 2 +- 10 files changed, 77 insertions(+), 22 deletions(-) diff --git a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift index d4c3d2a..8c5ac4b 100644 --- a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift +++ b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift @@ -243,7 +243,7 @@ import SwiftUI @ToolbarContentBuilder private var toolbar: some ToolbarContent { #if os(tvOS) - ToolbarItemGroup(placement: .topBarLeading) { + ToolbarItemGroup(placement: .topBarLeading) { navigationButtons } #endif diff --git a/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift b/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift index 443d57f..e1dad8b 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift @@ -143,7 +143,7 @@ import SwiftUI } .toolbar { ToolbarItem(placement: .topBarTrailing) { - TVToolbarButton(title: String(localized: "Reset")) { + TVToolbarButton(title: String(localized: "Reset")) { Task { await configuration.resetToDefault() } diff --git a/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift b/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift index 9395143..6c9a11e 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift @@ -139,7 +139,7 @@ struct ProfilePickerSheet: View { .environment(\.editMode, $editMode) .toolbar { ToolbarItem(placement: .confirmationAction) { - TVToolbarButton(title: editMode.isEditing ? String(localized: "Done"): String(localized: "Edit")) { + TVToolbarButton(title: editMode.isEditing ? String(localized: "Done") : String(localized: "Edit")) { withAnimation { if editMode.isEditing { movingProfileID = nil diff --git a/ApplicationLibrary/Views/Dashboard/Overview/OverviewViewModel.swift b/ApplicationLibrary/Views/Dashboard/Overview/OverviewViewModel.swift index 07b6392..012122e 100644 --- a/ApplicationLibrary/Views/Dashboard/Overview/OverviewViewModel.swift +++ b/ApplicationLibrary/Views/Dashboard/Overview/OverviewViewModel.swift @@ -32,17 +32,7 @@ public final class OverviewViewModel: BaseViewModel { try LibboxNewStandaloneCommandClient()!.setSystemProxyEnabled(enabled) } else { await MainActor.run { reasserting = true } - try await profile.stop() - - var waitSeconds = 0 - while await profile.status != .disconnected { - try await Task.sleep(nanoseconds: NSEC_PER_SEC) - waitSeconds += 1 - if waitSeconds >= 5 { - throw NSError(domain: "Restart service timeout", code: 0) - } - } - try await profile.start() + try await profile.restart() await MainActor.run { reasserting = false } } } catch { diff --git a/ApplicationLibrary/Views/Setting/OnDemandRulesView.swift b/ApplicationLibrary/Views/Setting/OnDemandRulesView.swift index 09752e1..4bce611 100644 --- a/ApplicationLibrary/Views/Setting/OnDemandRulesView.swift +++ b/ApplicationLibrary/Views/Setting/OnDemandRulesView.swift @@ -3,7 +3,9 @@ import Library import SwiftUI public struct OnDemandRulesView: View { + @EnvironmentObject private var environments: ExtensionEnvironments @State private var isLoading = true + @State private var alert: AlertState? @State private var alwaysOn = false public init() {} @@ -23,11 +25,13 @@ public struct OnDemandRulesView: View { This should not be an intended use of the API, so you cannot disable VPN in system settings. To stop the service manually, use the in-app interface or simply delete the VPN profile. """, $alwaysOn) { newValue in await SharedPreferences.alwaysOn.set(newValue) + await restartService() } FormButton { Task { await SharedPreferences.resetOnDemandRules() + await restartService() isLoading = true } } label: { @@ -38,11 +42,23 @@ public struct OnDemandRulesView: View { } } .navigationTitle("On Demand Rules") + .alert($alert) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif } + private func restartService() async { + guard let profile = environments.extensionProfile, profile.status.isConnected else { + return + } + do { + try await profile.restart() + } catch { + alert = AlertState(error: error) + } + } + private func loadSettings() async { alwaysOn = await SharedPreferences.alwaysOn.get() isLoading = false diff --git a/ApplicationLibrary/Views/Setting/PacketTunnelView.swift b/ApplicationLibrary/Views/Setting/PacketTunnelView.swift index 581c23e..43d909f 100644 --- a/ApplicationLibrary/Views/Setting/PacketTunnelView.swift +++ b/ApplicationLibrary/Views/Setting/PacketTunnelView.swift @@ -2,7 +2,9 @@ import Library import SwiftUI struct PacketTunnelView: View { + @EnvironmentObject private var environments: ExtensionEnvironments @State private var isLoading = true + @State private var alert: AlertState? @State private var ignoreMemoryLimit = false @@ -27,6 +29,7 @@ struct PacketTunnelView: View { Do not enforce memory limits on sing-box. Will cause OOM on non-jailbroken iOS and tvOS devices. """, $ignoreMemoryLimit) { newValue in await SharedPreferences.ignoreMemoryLimit.set(newValue) + await restartService() } #if !os(tvOS) @@ -38,6 +41,7 @@ struct PacketTunnelView: View { [Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/3131931-includeallnetworks) """, $includeAllNetworks) { newValue in await SharedPreferences.includeAllNetworks.set(newValue) + await restartService() } FormToggle("excludeAPNs", """ @@ -46,14 +50,16 @@ struct PacketTunnelView: View { [Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/4140516-excludeapns) """, $excludeAPNs) { newValue in await SharedPreferences.excludeAPNs.set(newValue) + await restartService() } FormToggle("excludeCellularServices", """ - If this property is true, the system excludes cellular services — such as Wi-Fi Calling, MMS, SMS, and Visual Voicemail — but only when the **includeAllNetworks** property is also true. This property doesn’t impact services that use the cellular network only — such as VoLTE — which the system automatically excludes. + If this property is true, the system excludes cellular services — such as Wi-Fi Calling, MMS, SMS, and Visual Voicemail — but only when the **includeAllNetworks** property is also true. This property doesn't impact services that use the cellular network only — such as VoLTE — which the system automatically excludes. [Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/4140517-excludecellularservices) """, $excludeCellularServices) { newValue in await SharedPreferences.excludeCellularServices.set(newValue) + await restartService() } FormToggle("excludeLocalNetworks", """ @@ -62,6 +68,7 @@ struct PacketTunnelView: View { [Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/3143658-excludelocalnetworks) """, $excludeLocalNetworks) { newValue in await SharedPreferences.excludeLocalNetworks.set(newValue) + await restartService() } FormToggle("enforceRoutes", """ @@ -72,6 +79,7 @@ struct PacketTunnelView: View { [Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/3689459-enforceroutes) """, $enforceRoutes) { newValue in await SharedPreferences.enforceRoutes.set(newValue) + await restartService() } #endif @@ -79,6 +87,7 @@ struct PacketTunnelView: View { FormButton { Task { await SharedPreferences.resetPacketTunnel() + await restartService() isLoading = true } } label: { @@ -89,11 +98,23 @@ struct PacketTunnelView: View { } } .navigationTitle("Packet Tunnel") + .alert($alert) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif } + private func restartService() async { + guard let profile = environments.extensionProfile, profile.status.isConnected else { + return + } + do { + try await profile.restart() + } catch { + alert = AlertState(error: error) + } + } + private func loadSettings() async { ignoreMemoryLimit = await SharedPreferences.ignoreMemoryLimit.get() #if !os(tvOS) diff --git a/ApplicationLibrary/Views/Setting/ProfileOverrideView.swift b/ApplicationLibrary/Views/Setting/ProfileOverrideView.swift index 45a1af5..b353cc0 100644 --- a/ApplicationLibrary/Views/Setting/ProfileOverrideView.swift +++ b/ApplicationLibrary/Views/Setting/ProfileOverrideView.swift @@ -1,8 +1,11 @@ +import Libbox import Library import SwiftUI public struct ProfileOverrideView: View { + @EnvironmentObject private var environments: ExtensionEnvironments @State private var isLoading = true + @State private var alert: AlertState? @State private var excludeDefaultRoute = false @State private var autoRouteUseSubRangesByDefault = false @State private var excludeAPNsRoute = false @@ -20,6 +23,7 @@ public struct ProfileOverrideView: View { FormView { FormToggle("Hide VPN Icon", "Append `0.0.0.0/31` and `::/127` to `route_exclude_address` if not exists.", $excludeDefaultRoute) { newValue in await SharedPreferences.excludeDefaultRoute.set(newValue) + await reloadService() } FormToggle("No Default Route", """ @@ -27,15 +31,18 @@ public struct ProfileOverrideView: View { If `` exists in the configuration, this item will not take effect on the corresponding network (commonly used to resolve HomeKit compatibility issues). """, $autoRouteUseSubRangesByDefault) { newValue in await SharedPreferences.autoRouteUseSubRangesByDefault.set(newValue) + await reloadService() } FormToggle("Exclude APNs Route", "Append `push.apple.com` to `bypass_domain`, and `17.0.0.0/8` to `route_exclude_address`.", $excludeAPNsRoute) { newValue in await SharedPreferences.excludeAPNsRoute.set(newValue) + await reloadService() } FormButton { Task { await SharedPreferences.resetProfileOverride() + await reloadService() isLoading = true } } label: { @@ -46,11 +53,23 @@ public struct ProfileOverrideView: View { } } .navigationTitle("Profile Override") + .alert($alert) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif } + private func reloadService() async { + guard let profile = environments.extensionProfile, profile.status.isConnected else { + return + } + do { + try LibboxNewStandaloneCommandClient()?.serviceReload() + } catch { + alert = AlertState(error: error) + } + } + private func loadSettings() async { excludeDefaultRoute = await SharedPreferences.excludeDefaultRoute.get() autoRouteUseSubRangesByDefault = await SharedPreferences.autoRouteUseSubRangesByDefault.get() diff --git a/IntentsExtension/Intents.swift b/IntentsExtension/Intents.swift index b5d261e..9bc14cf 100644 --- a/IntentsExtension/Intents.swift +++ b/IntentsExtension/Intents.swift @@ -38,9 +38,7 @@ struct StartServiceIntent: AppIntent { } try LibboxNewStandaloneCommandClient()!.serviceReload() } else if extensionProfile.status.isConnected { - try await extensionProfile.stop() - try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC))) - try await extensionProfile.start() + try await extensionProfile.restart() } else { try await extensionProfile.start() } @@ -65,9 +63,7 @@ struct RestartServiceIntent: AppIntent { if extensionProfile.status == .connected { try LibboxNewStandaloneCommandClient()!.serviceReload() } else if extensionProfile.status.isConnected { - try await extensionProfile.stop() - try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC))) - try await extensionProfile.start() + try await extensionProfile.restart() } else { try await extensionProfile.start() } diff --git a/Library/Network/ExtensionProfile.swift b/Library/Network/ExtensionProfile.swift index 8cc6dcc..2b7c9fe 100644 --- a/Library/Network/ExtensionProfile.swift +++ b/Library/Network/ExtensionProfile.swift @@ -108,6 +108,19 @@ public class ExtensionProfile: ObservableObject { manager.connection.stopVPNTunnel() } + public func restart() async throws { + try await stop() + var waitSeconds = 0 + while await MainActor.run(body: { status }) != .disconnected { + try await Task.sleep(nanoseconds: NSEC_PER_SEC) + waitSeconds += 1 + if waitSeconds >= 5 { + throw NSError(domain: "Restart service timeout", code: 0) + } + } + try await start() + } + public static func load() async throws -> ExtensionProfile? { let managers = try await NETunnelProviderManager.loadAllFromPreferences() if managers.isEmpty { diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 95e87be..1c54c13 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -923,7 +923,7 @@ "If this property is true, the system excludes Apple Push Notification services (APNs) traffic, but only when the **includeAllNetworks** property is also true.\n\n[Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/4140516-excludeapns)" : { "shouldTranslate" : false }, - "If this property is true, the system excludes cellular services — such as Wi-Fi Calling, MMS, SMS, and Visual Voicemail — but only when the **includeAllNetworks** property is also true. This property doesn’t impact services that use the cellular network only — such as VoLTE — which the system automatically excludes.\n\n[Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/4140517-excludecellularservices)" : { + "If this property is true, the system excludes cellular services — such as Wi-Fi Calling, MMS, SMS, and Visual Voicemail — but only when the **includeAllNetworks** property is also true. This property doesn't impact services that use the cellular network only — such as VoLTE — which the system automatically excludes.\n\n[Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/4140517-excludecellularservices)" : { "shouldTranslate" : false }, "If this property is true, the system excludes network connections to hosts on the local network — such as AirPlay, AirDrop, and CarPlay — but only when the **includeAllNetworks** or **enforceRoutes** property is also true.\n\n[Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/3143658-excludelocalnetworks)" : {