From 8897e6deeb363e9238dc2164efe1965f4706bc19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 8 Feb 2026 18:25:55 +0800 Subject: [PATCH] refactor: improve alert error context and copy action --- .../Views/Abstract/BaseViewModel.swift | 12 ++--- .../Views/Abstract/GlobalChecksModifier.swift | 4 +- .../Views/Abstract/ShareButton.swift | 4 +- .../Connections/ConnectionListViewModel.swift | 2 +- .../Views/Connections/ConnectionView.swift | 2 +- .../Views/Dashboard/Cards/ClashModeCard.swift | 2 +- .../Views/Dashboard/Cards/ProfileCard.swift | 10 ++--- .../Dashboard/Cards/ProfilePickerSheet.swift | 20 ++++----- .../Components/InstallProfileButton.swift | 2 +- .../InstallSystemExtensionButton.swift | 2 +- .../Components/StartStopButton.swift | 3 +- .../Views/Dashboard/DashboardViewModel.swift | 4 +- .../Overview/OverviewViewModel.swift | 4 +- .../Views/Groups/GroupListViewModel.swift | 6 +-- ApplicationLibrary/Views/Log/LogView.swift | 2 +- .../Views/Log/LogViewModel.swift | 2 +- .../Profile/EditProfileContentViewModel.swift | 4 +- .../Views/Profile/EditProfileViewModel.swift | 6 +-- .../Profile/ImportProfileViewModel.swift | 6 +-- .../Views/Profile/NewProfileMenuView.swift | 10 ++--- .../Views/Profile/NewProfileView.swift | 4 +- .../Views/Profile/NewProfileViewModel.swift | 2 +- .../Views/Scanner/QRScannerView.swift | 2 +- .../Views/Setting/CoreView.swift | 8 ++-- .../Views/Setting/MacAppView.swift | 16 +++---- .../Views/Setting/OnDemandRulesView.swift | 4 +- .../Views/Setting/PacketTunnelView.swift | 2 +- .../Views/Setting/ProfileOverrideView.swift | 2 +- Library/Network/ExtensionEnvironments.swift | 45 ++++++++++++++++--- Library/Network/ExtensionProvider.swift | 7 +++ MacLibrary/MainViewModel.swift | 4 +- SFI/MainView.swift | 4 +- 32 files changed, 125 insertions(+), 82 deletions(-) diff --git a/ApplicationLibrary/Views/Abstract/BaseViewModel.swift b/ApplicationLibrary/Views/Abstract/BaseViewModel.swift index de70e3f..a6c7c5a 100644 --- a/ApplicationLibrary/Views/Abstract/BaseViewModel.swift +++ b/ApplicationLibrary/Views/Abstract/BaseViewModel.swift @@ -7,24 +7,24 @@ open class BaseViewModel: ObservableObject { public init() {} - public func showError(_ error: Error) { - alert = AlertState(error: error) + public func showError(_ error: Error, action: String) { + alert = AlertState(action: action, error: error) } - public func execute(_ operation: () async throws -> Void) async { + public func execute(_ operation: () async throws -> Void, action: String) async { do { try await operation() } catch { - alert = AlertState(error: error) + alert = AlertState(action: action, error: error) } } - public func executeOnBackground(_ operation: @escaping @Sendable () async throws -> Void) async { + public func executeOnBackground(_ operation: @escaping @Sendable () async throws -> Void, action: String) async { do { try await operation() } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: action, error: error) } } } diff --git a/ApplicationLibrary/Views/Abstract/GlobalChecksModifier.swift b/ApplicationLibrary/Views/Abstract/GlobalChecksModifier.swift index 5958a86..fb29f0b 100644 --- a/ApplicationLibrary/Views/Abstract/GlobalChecksModifier.swift +++ b/ApplicationLibrary/Views/Abstract/GlobalChecksModifier.swift @@ -105,7 +105,7 @@ public struct GlobalChecksModifier: ViewModifier { try await profile.importProfile() } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "import profile", error: error) } return } @@ -174,7 +174,7 @@ public struct GlobalChecksModifier: ViewModifier { } } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "check deprecated notes", error: error) } } } diff --git a/ApplicationLibrary/Views/Abstract/ShareButton.swift b/ApplicationLibrary/Views/Abstract/ShareButton.swift index 531b7fa..9c91fe5 100644 --- a/ApplicationLibrary/Views/Abstract/ShareButton.swift +++ b/ApplicationLibrary/Views/Abstract/ShareButton.swift @@ -86,7 +86,7 @@ public struct ShareButtonCompat: View { } } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "prepare share file", error: error) } } } @@ -117,7 +117,7 @@ public struct ShareButtonCompat: View { } } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "prepare share file", error: error) } } } diff --git a/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift b/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift index 9f5d26e..c5738b7 100644 --- a/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift +++ b/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift @@ -90,7 +90,7 @@ public class ConnectionListViewModel: BaseViewModel { do { try LibboxNewStandaloneCommandClient()!.closeConnections() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "close all connections", error: error) } } diff --git a/ApplicationLibrary/Views/Connections/ConnectionView.swift b/ApplicationLibrary/Views/Connections/ConnectionView.swift index 976d499..ba98c88 100644 --- a/ApplicationLibrary/Views/Connections/ConnectionView.swift +++ b/ApplicationLibrary/Views/Connections/ConnectionView.swift @@ -118,7 +118,7 @@ public struct ConnectionView: View { try await LibboxNewStandaloneCommandClient()!.closeConnection(connection.id) } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "close connection", error: error) } } } diff --git a/ApplicationLibrary/Views/Dashboard/Cards/ClashModeCard.swift b/ApplicationLibrary/Views/Dashboard/Cards/ClashModeCard.swift index 07c8229..1863183 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/ClashModeCard.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/ClashModeCard.swift @@ -195,7 +195,7 @@ public struct ClashModeCard: View { try LibboxNewStandaloneCommandClient()!.setClashMode(newMode) } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "set clash mode", error: error) } } } diff --git a/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift b/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift index 522c6e7..2e75e40 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift @@ -134,7 +134,7 @@ public struct ProfileCard: View { ) { result in viewModel.exportDocument = nil if case let .failure(error) = result { - viewModel.alert = AlertState(error: error) + viewModel.alert = AlertState(action: "export profile", error: error) } } #endif @@ -371,7 +371,7 @@ public struct ProfileCard: View { ) #endif } catch { - viewModel.alert = AlertState(error: error) + viewModel.alert = AlertState(action: "share profile", error: error) } } } @@ -394,7 +394,7 @@ public struct ProfileCard: View { } } catch { await MainActor.run { - viewModel.alert = AlertState(error: error) + viewModel.alert = AlertState(action: "export profile", error: error) } } } @@ -427,7 +427,7 @@ public struct ProfileCard: View { do { viewModel.qrsShareData = try await profile.origin.encodedContentDataAsync() } catch { - viewModel.alert = AlertState(error: error) + viewModel.alert = AlertState(action: "prepare QRS share", error: error) viewModel.showQRSShare = false } } @@ -536,7 +536,7 @@ extension ProfileCard { try await profile.updateRemoteProfile() environments.profileUpdate.send() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "update remote profile", error: error) } } } diff --git a/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift b/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift index f8c158c..31d0515 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/ProfilePickerSheet.swift @@ -402,7 +402,7 @@ struct ProfilePickerSheet: View { try await profile.origin.updateRemoteProfile() environments.profileUpdate.send() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "update remote profile", error: error) } } @@ -742,7 +742,7 @@ private struct ProfilePickerRow: View { ) { result in exportDocument = nil if case let .failure(error) = result { - alert = AlertState(error: error) + alert = AlertState(action: "export profile", error: error) } } } @@ -789,7 +789,7 @@ private struct ProfilePickerRow: View { ) { result in exportDocument = nil if case let .failure(error) = result { - alert = AlertState(error: error) + alert = AlertState(action: "export profile", error: error) } } } @@ -919,7 +919,7 @@ private struct ProfilePickerRow: View { } } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "prepare QRS share", error: error) showQRSShare = false } } @@ -1015,7 +1015,7 @@ private struct ProfilePickerRow: View { } } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "export profile", error: error) } } } @@ -1067,7 +1067,7 @@ private struct ProfilePickerRow: View { } } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "share profile", error: error) } } } @@ -1091,7 +1091,7 @@ private struct ProfilePickerRow: View { } } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "export profile", error: error) } } } @@ -1282,7 +1282,7 @@ private struct ProfilePickerRow: View { ) { result in exportDocument = nil if case let .failure(error) = result { - alert = AlertState(error: error) + alert = AlertState(action: "export profile", error: error) } } } @@ -1336,7 +1336,7 @@ private struct ProfilePickerRow: View { } } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "prepare QRS share", error: error) showQRSShare = false } } @@ -1405,7 +1405,7 @@ private struct ProfilePickerRow: View { } } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "export profile", error: error) } } } diff --git a/ApplicationLibrary/Views/Dashboard/Components/InstallProfileButton.swift b/ApplicationLibrary/Views/Dashboard/Components/InstallProfileButton.swift index 45ffece..c744b31 100644 --- a/ApplicationLibrary/Views/Dashboard/Components/InstallProfileButton.swift +++ b/ApplicationLibrary/Views/Dashboard/Components/InstallProfileButton.swift @@ -26,7 +26,7 @@ public struct InstallProfileButton: View { try await ExtensionProfile.install() await callback() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "install network extension", error: error) } } } diff --git a/ApplicationLibrary/Views/Dashboard/Components/InstallSystemExtensionButton.swift b/ApplicationLibrary/Views/Dashboard/Components/InstallSystemExtensionButton.swift index 3c9db1c..db59f03 100644 --- a/ApplicationLibrary/Views/Dashboard/Components/InstallSystemExtensionButton.swift +++ b/ApplicationLibrary/Views/Dashboard/Components/InstallSystemExtensionButton.swift @@ -31,7 +31,7 @@ } await callback() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "install system extension", error: error) } } } diff --git a/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift b/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift index 916c5ff..334274e 100644 --- a/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift +++ b/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift @@ -165,7 +165,8 @@ public struct StartStopButton: View { } catch { await MainActor.run { isStarting = false - alert = AlertState(error: error) + let action = isEnabled ? "start service" : "stop service" + alert = AlertState(action: action, error: error) } } } diff --git a/ApplicationLibrary/Views/Dashboard/DashboardViewModel.swift b/ApplicationLibrary/Views/Dashboard/DashboardViewModel.swift index ad31278..33c7ff2 100644 --- a/ApplicationLibrary/Views/Dashboard/DashboardViewModel.swift +++ b/ApplicationLibrary/Views/Dashboard/DashboardViewModel.swift @@ -69,7 +69,7 @@ public final class DashboardViewModel: BaseViewModel { await SharedPreferences.selectedProfileID.set(selectedProfileID) } } catch { - alert = AlertState(error: error) + alert = AlertState(action: "load profile list", error: error) return } } @@ -109,7 +109,7 @@ extension ExtensionProfile { ) } #endif - return AlertState(error: nsError) + return AlertState(action: "fetch last disconnect error", error: nsError) } } diff --git a/ApplicationLibrary/Views/Dashboard/Overview/OverviewViewModel.swift b/ApplicationLibrary/Views/Dashboard/Overview/OverviewViewModel.swift index f857d90..a6f8d1f 100644 --- a/ApplicationLibrary/Views/Dashboard/Overview/OverviewViewModel.swift +++ b/ApplicationLibrary/Views/Dashboard/Overview/OverviewViewModel.swift @@ -15,7 +15,7 @@ public final class OverviewViewModel: BaseViewModel { do { try await profile.reloadService() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "reload service", error: error) } } reasserting = false @@ -32,7 +32,7 @@ public final class OverviewViewModel: BaseViewModel { await MainActor.run { reasserting = false } } } catch { - await MainActor.run { alert = AlertState(error: error) } + await MainActor.run { alert = AlertState(action: "update system proxy settings", error: error) } } } } diff --git a/ApplicationLibrary/Views/Groups/GroupListViewModel.swift b/ApplicationLibrary/Views/Groups/GroupListViewModel.swift index ca4b66a..8ffd687 100644 --- a/ApplicationLibrary/Views/Groups/GroupListViewModel.swift +++ b/ApplicationLibrary/Views/Groups/GroupListViewModel.swift @@ -90,7 +90,7 @@ public class GroupListViewModel: BaseViewModel { try await LibboxNewStandaloneCommandClient()!.selectOutbound(groupTag, outboundTag: outboundTag) } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "select outbound", error: error) } } } @@ -109,7 +109,7 @@ public class GroupListViewModel: BaseViewModel { try await LibboxNewStandaloneCommandClient()!.setGroupExpand(tag, isExpand: isExpand) } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "update group expansion", error: error) } } } @@ -125,7 +125,7 @@ public class GroupListViewModel: BaseViewModel { try await LibboxNewStandaloneCommandClient()!.urlTest(tag) } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "run URL test", error: error) } } } diff --git a/ApplicationLibrary/Views/Log/LogView.swift b/ApplicationLibrary/Views/Log/LogView.swift index 49cbe98..f9e29d2 100644 --- a/ApplicationLibrary/Views/Log/LogView.swift +++ b/ApplicationLibrary/Views/Log/LogView.swift @@ -399,7 +399,7 @@ private struct LogContentInnerView: View { cleanup() logFileURL = nil if case let .failure(error) = result { - alert = AlertState(error: error) + alert = AlertState(action: "export log file", error: error) } } .sheet(isPresented: $showShareSheet) { diff --git a/ApplicationLibrary/Views/Log/LogViewModel.swift b/ApplicationLibrary/Views/Log/LogViewModel.swift index 4f51fa9..0d45c17 100644 --- a/ApplicationLibrary/Views/Log/LogViewModel.swift +++ b/ApplicationLibrary/Views/Log/LogViewModel.swift @@ -135,7 +135,7 @@ public class LogDataModel: ObservableObject { try text.write(to: fileURL, atomically: true, encoding: .utf8) logFileURL = fileURL } catch { - viewModel?.alert = AlertState(error: error) + viewModel?.alert = AlertState(action: "prepare log file", error: error) } } #endif diff --git a/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift b/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift index aeb3c67..1a4569e 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift @@ -85,7 +85,7 @@ public final class EditProfileContentViewModel: BaseViewModel { do { try await loadContentBackground() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "load profile content", error: error) } isLoading = false } @@ -111,7 +111,7 @@ public final class EditProfileContentViewModel: BaseViewModel { do { try await saveContentBackground(profile) } catch { - alert = AlertState(error: error) + alert = AlertState(action: "save profile content", error: error) return } isChanged = false diff --git a/ApplicationLibrary/Views/Profile/EditProfileViewModel.swift b/ApplicationLibrary/Views/Profile/EditProfileViewModel.swift index a4860ab..b623359 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileViewModel.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileViewModel.swift @@ -21,7 +21,7 @@ public final class EditProfileViewModel: BaseViewModel { try await profile.updateRemoteProfile() environments.profileUpdate.send() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "update remote profile", error: error) } } @@ -29,7 +29,7 @@ public final class EditProfileViewModel: BaseViewModel { do { try await ProfileManager.delete(profile) } catch { - alert = AlertState(error: error) + alert = AlertState(action: "delete profile", error: error) return } environments.profileUpdate.send() @@ -46,7 +46,7 @@ public final class EditProfileViewModel: BaseViewModel { #endif try await profile.onProfileUpdated() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "save profile", error: error) return } isChanged = false diff --git a/ApplicationLibrary/Views/Profile/ImportProfileViewModel.swift b/ApplicationLibrary/Views/Profile/ImportProfileViewModel.swift index 2eea6ff..181c690 100644 --- a/ApplicationLibrary/Views/Profile/ImportProfileViewModel.swift +++ b/ApplicationLibrary/Views/Profile/ImportProfileViewModel.swift @@ -38,7 +38,7 @@ case let .failed(error): DispatchQueue.main.async { [self] in reset() - alert = AlertState(error: error) + alert = AlertState(action: "connect to import source", error: error) } default: break } @@ -47,7 +47,7 @@ do { try await loopMessages(environments: environments) } catch { - alert = AlertState(error: error) + alert = AlertState(action: "import profile from device", error: error) reset() } } @@ -121,7 +121,7 @@ try await socket.write(request.encode()) } catch { isImporting = false - alert = AlertState(error: error) + alert = AlertState(action: "request profile content from device", error: error) reset() } } diff --git a/ApplicationLibrary/Views/Profile/NewProfileMenuView.swift b/ApplicationLibrary/Views/Profile/NewProfileMenuView.swift index 3be88a2..c40aef9 100644 --- a/ApplicationLibrary/Views/Profile/NewProfileMenuView.swift +++ b/ApplicationLibrary/Views/Profile/NewProfileMenuView.swift @@ -223,14 +223,14 @@ public struct NewProfileMenuView: View { environments.profileUpdate.send() dismiss() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "import profile", error: error) } } }, secondaryButton: .cancel() ) } catch { - alert = AlertState(error: error) + alert = AlertState(action: "read imported profile file", error: error) } } } @@ -250,7 +250,7 @@ public struct NewProfileMenuView: View { var error: NSError? let remoteProfile = LibboxParseRemoteProfileImportLink(string, &error) if let error { - alert = AlertState(error: error) + alert = AlertState(action: "parse QR code profile link", error: error) return } guard let remoteProfile else { @@ -274,14 +274,14 @@ public struct NewProfileMenuView: View { environments.profileUpdate.send() dismiss() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "import profile", error: error) } } }, secondaryButton: .cancel() ) } catch { - alert = AlertState(error: error) + alert = AlertState(action: "decode QRS profile data", error: error) } } #endif diff --git a/ApplicationLibrary/Views/Profile/NewProfileView.swift b/ApplicationLibrary/Views/Profile/NewProfileView.swift index 8bab972..f0210ee 100644 --- a/ApplicationLibrary/Views/Profile/NewProfileView.swift +++ b/ApplicationLibrary/Views/Profile/NewProfileView.swift @@ -185,7 +185,7 @@ public struct NewProfileView: View { viewModel.fileURL = urls[0] } } catch { - viewModel.alert = AlertState(error: error) + viewModel.alert = AlertState(action: "read imported profile file", error: error) return } } @@ -213,7 +213,7 @@ public struct NewProfileView: View { viewModel.fileURL = urls[0] } } catch { - viewModel.alert = AlertState(error: error) + viewModel.alert = AlertState(action: "read imported profile file", error: error) return } } diff --git a/ApplicationLibrary/Views/Profile/NewProfileViewModel.swift b/ApplicationLibrary/Views/Profile/NewProfileViewModel.swift index 1a8d74c..dec6ea6 100644 --- a/ApplicationLibrary/Views/Profile/NewProfileViewModel.swift +++ b/ApplicationLibrary/Views/Profile/NewProfileViewModel.swift @@ -72,7 +72,7 @@ public final class NewProfileViewModel: BaseViewModel { do { createdProfile = try await createProfileBackground() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "create profile", error: error) return } diff --git a/ApplicationLibrary/Views/Scanner/QRScannerView.swift b/ApplicationLibrary/Views/Scanner/QRScannerView.swift index 9d89564..d7df5f0 100644 --- a/ApplicationLibrary/Views/Scanner/QRScannerView.swift +++ b/ApplicationLibrary/Views/Scanner/QRScannerView.swift @@ -160,7 +160,7 @@ ) #endif default: - alert = AlertState(error: error) + alert = AlertState(action: "scan QR code", error: error) } } } diff --git a/ApplicationLibrary/Views/Setting/CoreView.swift b/ApplicationLibrary/Views/Setting/CoreView.swift index a97e15d..8dfda54 100644 --- a/ApplicationLibrary/Views/Setting/CoreView.swift +++ b/ApplicationLibrary/Views/Setting/CoreView.swift @@ -204,7 +204,7 @@ public struct CoreView: View { try await environments.extensionProfile!.stop() await destroyWorkingDirectory() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "stop service before destroying working directory", error: error) } } @@ -233,7 +233,7 @@ public struct CoreView: View { #endif isLoading = true } catch { - alert = AlertState(error: error) + alert = AlertState(action: "destroy working directory", error: error) } } @@ -281,7 +281,7 @@ public struct CoreView: View { try await manager.signalEnumerator(for: .workingSet) } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "notify Files app about working directory changes", error: error) } } } @@ -300,7 +300,7 @@ public struct CoreView: View { } } catch { await MainActor.run { - alert = AlertState(error: error) + alert = AlertState(action: "open working directory in Files", error: error) } } } diff --git a/ApplicationLibrary/Views/Setting/MacAppView.swift b/ApplicationLibrary/Views/Setting/MacAppView.swift index 997472f..c962e0f 100644 --- a/ApplicationLibrary/Views/Setting/MacAppView.swift +++ b/ApplicationLibrary/Views/Setting/MacAppView.swift @@ -109,14 +109,14 @@ public struct AppView: View { try HelperServiceManager.registerRootHelper() refreshHelperStatus() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "update helper service", error: error) } } } label: { Label("Update", systemImage: "arrow.down.doc.fill") } FormButton(role: .destructive) { - performHelperAction { + performHelperAction(actionName: "uninstall helper service") { try HelperServiceManager.unregisterRootHelper() } } label: { @@ -130,7 +130,7 @@ public struct AppView: View { } } else { FormButton { - performHelperAction { + performHelperAction(actionName: "install helper service") { try HelperServiceManager.registerRootHelper() } } label: { @@ -210,7 +210,7 @@ public struct AppView: View { try SMAppService.mainApp.unregister() } } catch { - alert = AlertState(error: error) + alert = AlertState(action: "update login items", error: error) } } @@ -231,7 +231,7 @@ public struct AppView: View { } } } catch { - alert = AlertState(error: error) + alert = AlertState(action: "update system extension", error: error) } } @@ -252,16 +252,16 @@ public struct AppView: View { } } } catch { - alert = AlertState(error: error) + alert = AlertState(action: "uninstall system extension", error: error) } } - private func performHelperAction(_ action: () throws -> Void) { + private func performHelperAction(actionName: String, _ action: () throws -> Void) { do { try action() refreshHelperStatus() } catch { - alert = AlertState(error: error) + alert = AlertState(action: actionName, error: error) } } diff --git a/ApplicationLibrary/Views/Setting/OnDemandRulesView.swift b/ApplicationLibrary/Views/Setting/OnDemandRulesView.swift index 1ac50be..d68379b 100644 --- a/ApplicationLibrary/Views/Setting/OnDemandRulesView.swift +++ b/ApplicationLibrary/Views/Setting/OnDemandRulesView.swift @@ -264,7 +264,7 @@ public struct OnDemandRulesView: View { await updateService() isLoading = true } catch { - alert = AlertState(error: error) + alert = AlertState(action: "reset on-demand rules", error: error) } } } label: { @@ -289,7 +289,7 @@ public struct OnDemandRulesView: View { let enabled = mode != .disabled try await profile.updateOnDemand(enabled: enabled, useDefaultRules: mode == .alwaysOn) } catch { - alert = AlertState(error: error) + alert = AlertState(action: "update on-demand rules", error: error) } } diff --git a/ApplicationLibrary/Views/Setting/PacketTunnelView.swift b/ApplicationLibrary/Views/Setting/PacketTunnelView.swift index 1f78293..248b0e1 100644 --- a/ApplicationLibrary/Views/Setting/PacketTunnelView.swift +++ b/ApplicationLibrary/Views/Setting/PacketTunnelView.swift @@ -111,7 +111,7 @@ struct PacketTunnelView: View { do { try await profile.restart() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "restart service", error: error) } } diff --git a/ApplicationLibrary/Views/Setting/ProfileOverrideView.swift b/ApplicationLibrary/Views/Setting/ProfileOverrideView.swift index 8d8ca25..6049345 100644 --- a/ApplicationLibrary/Views/Setting/ProfileOverrideView.swift +++ b/ApplicationLibrary/Views/Setting/ProfileOverrideView.swift @@ -65,7 +65,7 @@ public struct ProfileOverrideView: View { do { try await profile.reloadService() } catch { - alert = AlertState(error: error) + alert = AlertState(action: "reload service", error: error) } } diff --git a/Library/Network/ExtensionEnvironments.swift b/Library/Network/ExtensionEnvironments.swift index 34a454a..22c1793 100644 --- a/Library/Network/ExtensionEnvironments.swift +++ b/Library/Network/ExtensionEnvironments.swift @@ -1,5 +1,10 @@ import Foundation import SwiftUI +#if canImport(UIKit) + import UIKit +#elseif canImport(AppKit) + import AppKit +#endif public struct AlertState: Equatable { public var title: String @@ -36,15 +41,45 @@ public struct AlertState: Equatable { } } - public init(error: Error, dismiss: (() -> Void)? = nil) { - self.init(errorMessage: error.localizedDescription, dismiss: dismiss) + private static func formatErrorMessage(action: String, error: Error) -> String { + let normalizedAction = action.trimmingCharacters(in: .whitespacesAndNewlines) + let normalizedDescription = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines) + let actionText = normalizedAction.isEmpty ? "complete operation" : normalizedAction + if normalizedDescription.isEmpty { + return "Failed to \(actionText)" + } + return "Failed to \(actionText)\n\(normalizedDescription)" } - public init(errorMessage: String, dismiss: (() -> Void)? = nil) { + private static func copyErrorMessage(_ text: String) { + #if canImport(UIKit) + UIPasteboard.general.string = text + #elseif canImport(AppKit) + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(text, forType: .string) + #endif + } + + public init(action: String, error: Error, dismiss: (() -> Void)? = nil) { + self.init( + errorMessage: Self.formatErrorMessage(action: action, error: error), + dismiss: dismiss, + allowsCopy: true + ) + } + + public init(errorMessage: String, dismiss: (() -> Void)? = nil, allowsCopy: Bool = false) { title = String(localized: "Error") message = errorMessage - primaryButton = .default(String(localized: "Ok"), action: dismiss) - secondaryButton = nil + if allowsCopy { + primaryButton = .default(String(localized: "Copy")) { + Self.copyErrorMessage(errorMessage) + } + secondaryButton = .default(String(localized: "Ok"), action: dismiss) + } else { + primaryButton = .default(String(localized: "Ok"), action: dismiss) + secondaryButton = nil + } onDismiss = nil } diff --git a/Library/Network/ExtensionProvider.swift b/Library/Network/ExtensionProvider.swift index ac29086..ab239ed 100644 --- a/Library/Network/ExtensionProvider.swift +++ b/Library/Network/ExtensionProvider.swift @@ -148,6 +148,13 @@ open class ExtensionProvider: NEPacketTunnelProvider { throw ExtensionStartupError("(packet-tunnel) error: setup service: \(setupError.localizedDescription)") } + let stderrPath = URL(fileURLWithPath: tempPath, isDirectory: true).appendingPathComponent("stderr.log").path + var stderrError: NSError? + LibboxRedirectStderr(stderrPath, &stderrError) + if let stderrError { + throw ExtensionStartupError("(packet-tunnel) redirect stderr error: \(stderrError.localizedDescription)") + } + let ignoreMemoryLimit = (effectiveOptions["ignoreMemoryLimit"] as? NSNumber)?.boolValue ?? false LibboxSetMemoryLimit(!ignoreMemoryLimit) diff --git a/MacLibrary/MainViewModel.swift b/MacLibrary/MainViewModel.swift index 01cda0f..78fc8ea 100644 --- a/MacLibrary/MainViewModel.swift +++ b/MacLibrary/MainViewModel.swift @@ -45,7 +45,7 @@ public class MainViewModel: BaseViewModel { var error: NSError? importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error) if let error { - alert = AlertState(error: error) + alert = AlertState(action: "parse remote profile import link", error: error) } } else if url.pathExtension == "bpf" { Task { @@ -62,7 +62,7 @@ public class MainViewModel: BaseViewModel { try await .from(readURL(url)) } } catch { - alert = AlertState(error: error) + alert = AlertState(action: "import profile from URL", error: error) } } diff --git a/SFI/MainView.swift b/SFI/MainView.swift index 30a2733..056ba5a 100644 --- a/SFI/MainView.swift +++ b/SFI/MainView.swift @@ -215,7 +215,7 @@ struct MainView: View { var error: NSError? importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error) if let error { - alert = AlertState(error: error) + alert = AlertState(action: "parse remote profile import link", error: error) } } else if url.pathExtension == "bpf" { do { @@ -223,7 +223,7 @@ struct MainView: View { try .from(Data(contentsOf: url)) } } catch { - alert = AlertState(error: error) + alert = AlertState(action: "import profile from URL", error: error) } } else { alert = AlertState(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)"))