diff --git a/CHANGELOG.md b/CHANGELOG.md index fdc55d11..8842a811 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - IVPN provider. - OpenVPN: Support for `--route-nopull`. [#230](https://github.com/passepartoutvpn/passepartout-apple/pull/230) +- App log in Diagnostics screen. [#234](https://github.com/passepartoutvpn/passepartout-apple/pull/234) ### Changed diff --git a/Passepartout/App/Constants/Constants+App.swift b/Passepartout/App/Constants/Constants+App.swift index c332f262..709775c3 100644 --- a/Passepartout/App/Constants/Constants+App.swift +++ b/Passepartout/App/Constants/Constants+App.swift @@ -140,36 +140,40 @@ extension Constants { } enum Log { + enum App { + static let url = containerURL(filename: "App.log") + + static let format = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M" + } + + enum Tunnel { + static let path = containerPath(filename: "Tunnel.log") + + static let format = "$DHH:mm:ss$d - $M" + } + private static let parentPath = "Library/Caches" - private static func containerLogURL(filename: String) -> URL { - Files.containerURL - .appendingPathComponent(parentPath) - .appendingPathComponent(filename) - } - - private static func containerLogPath(filename: String) -> String { - "\(parentPath)/\(filename)" - } - - static let appLogURL = containerLogURL(filename: "App.log") - - static let tunnelLogPath = containerLogPath(filename: "Tunnel.log") - - static let logLevel: SwiftyBeaver.Level = { + static let level: SwiftyBeaver.Level = { guard let levelString = ProcessInfo.processInfo.environment["LOG_LEVEL"], let levelNum = Int(levelString) else { return .info } return .init(rawValue: levelNum) ?? .info }() - static let logFormat = "$DHH:mm:ss.SSS$d $C$L$c $N.$F:$l - $M" + static let maxBytes = 100000 - static let tunnelLogFormat = "$DHH:mm:ss$d - $M" - - static let tunnelLogMaxBytes = 100000 - - static let tunnelLogRefreshInterval: TimeInterval = 5.0 + static let refreshInterval: TimeInterval = 5.0 + + private static func containerURL(filename: String) -> URL { + Files.containerURL + .appendingPathComponent(parentPath) + .appendingPathComponent(filename) + } + + private static func containerPath(filename: String) -> String { + "\(parentPath)/\(filename)" + } } enum URLs { diff --git a/Passepartout/App/Context/AppContext+Shared.swift b/Passepartout/App/Context/AppContext+Shared.swift index 68e359ab..12e302df 100644 --- a/Passepartout/App/Context/AppContext+Shared.swift +++ b/Passepartout/App/Context/AppContext+Shared.swift @@ -24,6 +24,7 @@ // import Foundation +import PassepartoutLibrary extension AppContext { static let shared = AppContext(coreContext: .shared) @@ -32,3 +33,7 @@ extension AppContext { extension ProductManager { static let shared = AppContext.shared.productManager } + +extension LogManager { + static let shared = AppContext.shared.logManager +} diff --git a/Passepartout/App/Context/AppContext.swift b/Passepartout/App/Context/AppContext.swift index 852ad66d..119e047f 100644 --- a/Passepartout/App/Context/AppContext.swift +++ b/Passepartout/App/Context/AppContext.swift @@ -29,29 +29,29 @@ import PassepartoutLibrary @MainActor class AppContext { - private let logManager: LogManager - - private let reviewer: Reviewer + let logManager: LogManager let productManager: ProductManager + private let reviewer: Reviewer + private var cancellables: Set = [] init(coreContext: CoreContext) { - logManager = LogManager(logFile: Constants.Log.appLogURL) - logManager.logLevel = Constants.Log.logLevel - logManager.logFormat = Constants.Log.logFormat + logManager = LogManager(logFile: Constants.Log.App.url) + logManager.logLevel = Constants.Log.level + logManager.logFormat = Constants.Log.App.format logManager.configureLogging() pp_log.info("Logging to: \(logManager.logFile!)") - reviewer = Reviewer() - reviewer.eventCountBeforeRating = Constants.Rating.eventCount - productManager = ProductManager( appType: Constants.InApp.appType, buildProducts: Constants.InApp.buildProducts ) + reviewer = Reviewer() + reviewer.eventCountBeforeRating = Constants.Rating.eventCount + // post configureObjects(coreContext: coreContext) diff --git a/Passepartout/App/Views/DebugLogView.swift b/Passepartout/App/Views/DebugLogView.swift index 3eda357e..27f92c7b 100644 --- a/Passepartout/App/Views/DebugLogView.swift +++ b/Passepartout/App/Views/DebugLogView.swift @@ -28,6 +28,8 @@ import Combine import PassepartoutLibrary struct DebugLogView: View { + private let title: String + private let url: URL private let timer: AnyPublisher @@ -36,7 +38,7 @@ struct DebugLogView: View { @State private var isSharing = false - private let maxBytes = UInt64(Constants.Log.tunnelLogMaxBytes) + private let maxBytes = UInt64(Constants.Log.maxBytes) private let appName = Constants.Global.appName @@ -44,11 +46,17 @@ struct DebugLogView: View { private let shareFilename = Unlocalized.Issues.Filenames.debugLog - init(url: URL, updateInterval: TimeInterval) { + init(title: String, url: URL, refreshInterval: TimeInterval?) { + self.title = title self.url = url - timer = Timer.TimerPublisher(interval: updateInterval, runLoop: .main, mode: .common) - .autoconnect() - .eraseToAnyPublisher() + if let refreshInterval = refreshInterval { + timer = Timer.TimerPublisher(interval: refreshInterval, runLoop: .main, mode: .common) + .autoconnect() + .eraseToAnyPublisher() + } else { + timer = Empty(outputType: Date.self, failureType: Never.self) + .eraseToAnyPublisher() + } } var body: some View { @@ -78,7 +86,7 @@ struct DebugLogView: View { #endif .edgesIgnoringSafeArea([.leading, .trailing]) .onReceive(timer, perform: refreshLog) - .navigationTitle(L10n.DebugLog.title) + .navigationTitle(title) .themeDebugLogStyle() } diff --git a/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift b/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift index a375000e..4e98de6f 100644 --- a/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift +++ b/Passepartout/App/Views/DiagnosticsView+OpenVPN.swift @@ -57,8 +57,6 @@ extension DiagnosticsView { private let vpnProtocol: VPNProtocolType = .openVPN - private let logUpdateInterval = Constants.Log.tunnelLogRefreshInterval - init(providerName: ProviderName?) { providerManager = .shared vpnManager = .shared @@ -108,16 +106,10 @@ extension DiagnosticsView { private var debugLogSection: some View { Section { - let url = debugLogURL - NavigationLink(L10n.DebugLog.title) { - url.map { - DebugLogView( - url: $0, - updateInterval: logUpdateInterval - ) - } - }.disabled(url == nil) + DebugLogSection(appLogURL: appLogURL, tunnelLogURL: tunnelLogURL) Toggle(L10n.Diagnostics.Items.MasksPrivateData.caption, isOn: $vpnManager.masksPrivateData) + } header: { + Text(L10n.DebugLog.title) } footer: { Text(L10n.Diagnostics.Sections.DebugLog.footer) } @@ -160,8 +152,12 @@ extension DiagnosticsView.OpenVPNView { // "withFallbacks: false" for view to hide nil options return cfg.builder(withFallbacks: false) } - - private var debugLogURL: URL? { + + private var appLogURL: URL? { + LogManager.shared.logFile + } + + private var tunnelLogURL: URL? { vpnManager.debugLogURL(forProtocol: vpnProtocol) } } diff --git a/Passepartout/App/Views/DiagnosticsView+WireGuard.swift b/Passepartout/App/Views/DiagnosticsView+WireGuard.swift index fa455cd7..058618f6 100644 --- a/Passepartout/App/Views/DiagnosticsView+WireGuard.swift +++ b/Passepartout/App/Views/DiagnosticsView+WireGuard.swift @@ -33,8 +33,6 @@ extension DiagnosticsView { private let providerName: ProviderName? - private let logUpdateInterval = Constants.Log.tunnelLogRefreshInterval - init(providerName: ProviderName?) { vpnManager = .shared self.providerName = providerName @@ -43,21 +41,21 @@ extension DiagnosticsView { var body: some View { List { Section { - let url = debugLogURL - NavigationLink(L10n.DebugLog.title) { - url.map { - DebugLogView( - url: $0, - updateInterval: logUpdateInterval - ) - } - }.disabled(url == nil) + DebugLogSection(appLogURL: appLogURL, tunnelLogURL: tunnelLogURL) + } header: { + Text(L10n.DebugLog.title) } } } - - private var debugLogURL: URL? { - vpnManager.debugLogURL(forProtocol: .wireGuard) - } + } +} + +extension DiagnosticsView.WireGuardView { + private var appLogURL: URL? { + LogManager.shared.logFile + } + + private var tunnelLogURL: URL? { + vpnManager.debugLogURL(forProtocol: .wireGuard) } } diff --git a/Passepartout/App/Views/DiagnosticsView.swift b/Passepartout/App/Views/DiagnosticsView.swift index 11faa441..0d265863 100644 --- a/Passepartout/App/Views/DiagnosticsView.swift +++ b/Passepartout/App/Views/DiagnosticsView.swift @@ -47,3 +47,46 @@ struct DiagnosticsView: View { }.navigationTitle(L10n.Diagnostics.title) } } + +extension DiagnosticsView { + struct DebugLogSection: View { + let appLogURL: URL? + + let tunnelLogURL: URL? + + private let refreshInterval = Constants.Log.refreshInterval + + var body: some View { + appLink + tunnelLink + } + + private var appLink: some View { + navigationLink( + withTitle: L10n.Diagnostics.Items.AppLog.title, + url: appLogURL, + refreshInterval: nil + ) + } + + private var tunnelLink: some View { + navigationLink( + withTitle: Unlocalized.VPN.vpn, + url: tunnelLogURL, + refreshInterval: refreshInterval + ) + } + + private func navigationLink(withTitle title: String, url: URL?, refreshInterval: TimeInterval?) -> some View { + NavigationLink(title) { + url.map { + DebugLogView( + title: title, + url: $0, + refreshInterval: refreshInterval + ) + } + }.disabled(url == nil) + } + } +} diff --git a/Passepartout/AppShared/Constants/SwiftGen+Strings.swift b/Passepartout/AppShared/Constants/SwiftGen+Strings.swift index 88bc1171..ec86c427 100644 --- a/Passepartout/AppShared/Constants/SwiftGen+Strings.swift +++ b/Passepartout/AppShared/Constants/SwiftGen+Strings.swift @@ -210,6 +210,10 @@ internal enum L10n { } } internal enum Items { + internal enum AppLog { + /// App + internal static let title = L10n.tr("Localizable", "diagnostics.items.app_log.title", fallback: "App") + } internal enum MasksPrivateData { /// Mask network data internal static let caption = L10n.tr("Localizable", "diagnostics.items.masks_private_data.caption", fallback: "Mask network data") diff --git a/Passepartout/AppShared/Context/CoreContext.swift b/Passepartout/AppShared/Context/CoreContext.swift index dbafa064..404084ff 100644 --- a/Passepartout/AppShared/Context/CoreContext.swift +++ b/Passepartout/AppShared/Context/CoreContext.swift @@ -114,8 +114,8 @@ class CoreContext { private func configureObjects() { providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager - vpnManager.tunnelLogPath = Constants.Log.tunnelLogPath - vpnManager.tunnelLogFormat = Constants.Log.tunnelLogFormat + vpnManager.tunnelLogPath = Constants.Log.Tunnel.path + vpnManager.tunnelLogFormat = Constants.Log.Tunnel.format profileManager.observeUpdates() vpnManager.observeUpdates() diff --git a/Passepartout/AppShared/de.lproj/Localizable.strings b/Passepartout/AppShared/de.lproj/Localizable.strings index 42c33fe6..d73b39e7 100644 --- a/Passepartout/AppShared/de.lproj/Localizable.strings +++ b/Passepartout/AppShared/de.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "Diagnose"; "diagnostics.sections.debug_log.footer" = "Zensier-Status wird aktiv nach erneutem Verbinden. Netzwerk-Daten sind Hostnamen, IP-Adressen, Routingtabellen, SSID. Zugangsdaten und Private Keys werden nie gelogged."; "diagnostics.items.server_configuration.caption" = "Serverkonfiguration"; +"diagnostics.items.app_log.title" = "App"; "diagnostics.items.masks_private_data.caption" = "Netzwerkdaten zensieren"; "diagnostics.items.report_issue.caption" = "Verbindungsproblem melden"; diff --git a/Passepartout/AppShared/el.lproj/Localizable.strings b/Passepartout/AppShared/el.lproj/Localizable.strings index ed2d841a..cd06f0a5 100644 --- a/Passepartout/AppShared/el.lproj/Localizable.strings +++ b/Passepartout/AppShared/el.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "Διαγνωστικά"; "diagnostics.sections.debug_log.footer" = "Η κατάσταση κάλυψης θα είναι αποτελεσματική μετά την επανασύνδεση. Τα δεδομένα δικτύου είναι του διακομιστή, διευθύνσεις IP, δρομολόγηση και SSID. Τα διαπιστευτήρια και τα ιδιωτικά κλειδιά δεν καταγράφονται ανεξάρτητα."; "diagnostics.items.server_configuration.caption" = "Ρυθμίσεις Διακομιστή"; +"diagnostics.items.app_log.title" = "Εφαρμογή"; "diagnostics.items.masks_private_data.caption" = "Μάσκα δεδομένα δικτύου"; "diagnostics.items.report_issue.caption" = "Αναφορά ζητήματος συνδεσιμότητας"; diff --git a/Passepartout/AppShared/en.lproj/Localizable.strings b/Passepartout/AppShared/en.lproj/Localizable.strings index 6a0e7c37..b29e139d 100644 --- a/Passepartout/AppShared/en.lproj/Localizable.strings +++ b/Passepartout/AppShared/en.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "Diagnostics"; "diagnostics.sections.debug_log.footer" = "Masking status will be effective after reconnecting. Network data are hostnames, IP addresses, routing, SSID. Credentials and private keys are not logged regardless."; "diagnostics.items.server_configuration.caption" = "Server configuration"; +"diagnostics.items.app_log.title" = "App"; "diagnostics.items.masks_private_data.caption" = "Mask network data"; "diagnostics.items.report_issue.caption" = "Report connectivity issue"; diff --git a/Passepartout/AppShared/es.lproj/Localizable.strings b/Passepartout/AppShared/es.lproj/Localizable.strings index 41668179..e36a122a 100644 --- a/Passepartout/AppShared/es.lproj/Localizable.strings +++ b/Passepartout/AppShared/es.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "Diagnósticos"; "diagnostics.sections.debug_log.footer" = "El estado de ocultación será efectivo tras reconectar. Los datos de red son hostnames, direcciones IP, routing, SSID. Las credenciales y las claves privadas no son registrados a pesar."; "diagnostics.items.server_configuration.caption" = "Configuración del servidor"; +"diagnostics.items.app_log.title" = "App"; "diagnostics.items.masks_private_data.caption" = "Ocultar datos de red"; "diagnostics.items.report_issue.caption" = "Reportar problema de conectividad"; diff --git a/Passepartout/AppShared/fr.lproj/Localizable.strings b/Passepartout/AppShared/fr.lproj/Localizable.strings index 84ad1064..1c22e24c 100644 --- a/Passepartout/AppShared/fr.lproj/Localizable.strings +++ b/Passepartout/AppShared/fr.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "Diagnostiques"; "diagnostics.sections.debug_log.footer" = "Camouflage du status sera effectif après la reconnection. Les données réseaux sont les noms d'hôtes, adresses IP, routage, SSID. Les identifiants et clés privés ne sont pas enregistrés."; "diagnostics.items.server_configuration.caption" = "Configuration serveur"; +"diagnostics.items.app_log.title" = "Application"; "diagnostics.items.masks_private_data.caption" = "Masquer les données de réseau"; "diagnostics.items.report_issue.caption" = "Rapporter un problème de connection"; diff --git a/Passepartout/AppShared/it.lproj/Localizable.strings b/Passepartout/AppShared/it.lproj/Localizable.strings index f9e9f3ee..2956afb3 100644 --- a/Passepartout/AppShared/it.lproj/Localizable.strings +++ b/Passepartout/AppShared/it.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "Diagnostica"; "diagnostics.sections.debug_log.footer" = "Il mascheramento sarà effettivo dopo una riconnessione. I dati di rete sono hostname, indirizzi IP, routing, SSID. Credenziali e chiavi private non sono registrati in ogni caso."; "diagnostics.items.server_configuration.caption" = "Configurazione del server"; +"diagnostics.items.app_log.title" = "App"; "diagnostics.items.masks_private_data.caption" = "Maschera dati rete"; "diagnostics.items.report_issue.caption" = "Segnala problema connettività"; diff --git a/Passepartout/AppShared/nl.lproj/Localizable.strings b/Passepartout/AppShared/nl.lproj/Localizable.strings index c6f7df76..ee4b0ac5 100644 --- a/Passepartout/AppShared/nl.lproj/Localizable.strings +++ b/Passepartout/AppShared/nl.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "Diagnose"; "diagnostics.sections.debug_log.footer" = "De maskeerstatus is effectief na opnieuw verbinden. Netwerkgegevens zijn hostnamen, IP-adressen, routing, SSID's. Inloggegevens en privésleutels worden niet geregistreerd."; "diagnostics.items.server_configuration.caption" = "Server configuratie"; +"diagnostics.items.app_log.title" = "App"; "diagnostics.items.masks_private_data.caption" = "Netwerkgegevens maskeren"; "diagnostics.items.report_issue.caption" = "Probleem met connectiviteit melden"; diff --git a/Passepartout/AppShared/pl.lproj/Localizable.strings b/Passepartout/AppShared/pl.lproj/Localizable.strings index 9e0548c0..20d6d236 100644 --- a/Passepartout/AppShared/pl.lproj/Localizable.strings +++ b/Passepartout/AppShared/pl.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "Diagnostyka"; "diagnostics.sections.debug_log.footer" = "Status maskowania będzie widoczny po ponownym połączeniu. Dane połączenia to nazwy hostów, adresy IP, routing, SSID. Loginy i klucze prywatne nie są zapisywane."; "diagnostics.items.server_configuration.caption" = "Konfiguracja serwera"; +"diagnostics.items.app_log.title" = "Aplikacja"; "diagnostics.items.masks_private_data.caption" = "Maskuj dane sieci"; "diagnostics.items.report_issue.caption" = "Zgłoś problemy z połączeniem"; diff --git a/Passepartout/AppShared/pt.lproj/Localizable.strings b/Passepartout/AppShared/pt.lproj/Localizable.strings index b2d3cc90..f9c3d375 100644 --- a/Passepartout/AppShared/pt.lproj/Localizable.strings +++ b/Passepartout/AppShared/pt.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "Diagnóstico"; "diagnostics.sections.debug_log.footer" = "O status será escondido após reconectado. Os dados da rede são hostnames, endereços de IP, rotas, SSID. Credenciais e chaves privadas não será logadas em nenhum dos casos."; "diagnostics.items.server_configuration.caption" = "Configuração do servidor"; +"diagnostics.items.app_log.title" = "Aplicativo"; "diagnostics.items.masks_private_data.caption" = "Esconder dados da rede"; "diagnostics.items.report_issue.caption" = "Reportar problemas de conexão"; diff --git a/Passepartout/AppShared/ru.lproj/Localizable.strings b/Passepartout/AppShared/ru.lproj/Localizable.strings index 8581a049..df1180d6 100644 --- a/Passepartout/AppShared/ru.lproj/Localizable.strings +++ b/Passepartout/AppShared/ru.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "Диагностика"; "diagnostics.sections.debug_log.footer" = "Маскировка включится после повторного подключения. Информация о сети - это названия хост профилей, IP адрес, маршрутизация и SSID. Данные для входа и приватные ключи не собираются."; "diagnostics.items.server_configuration.caption" = "Конфигурация сервера"; +"diagnostics.items.app_log.title" = "Приложение"; "diagnostics.items.masks_private_data.caption" = "Маскировать информацию сети"; "diagnostics.items.report_issue.caption" = "Сообщить о проблеме подкл."; diff --git a/Passepartout/AppShared/sv.lproj/Localizable.strings b/Passepartout/AppShared/sv.lproj/Localizable.strings index fa862d6d..a10dc045 100644 --- a/Passepartout/AppShared/sv.lproj/Localizable.strings +++ b/Passepartout/AppShared/sv.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "Diagnostics"; "diagnostics.sections.debug_log.footer" = "Masking status kommer att fungera efter återanslutning. Nätverksdata är värdnamn, IP-adresser, routing, SSID. Referenser och privata nycklar loggas inte oavsett."; "diagnostics.items.server_configuration.caption" = "Server konfiguration"; +"diagnostics.items.app_log.title" = "App"; "diagnostics.items.masks_private_data.caption" = "Mask nätverksdata"; "diagnostics.items.report_issue.caption" = "Rapportera anslutningsproblem"; diff --git a/Passepartout/AppShared/zh-Hans.lproj/Localizable.strings b/Passepartout/AppShared/zh-Hans.lproj/Localizable.strings index 6c468229..c2e4f19e 100644 --- a/Passepartout/AppShared/zh-Hans.lproj/Localizable.strings +++ b/Passepartout/AppShared/zh-Hans.lproj/Localizable.strings @@ -255,6 +255,7 @@ "diagnostics.title" = "分析数据"; "diagnostics.sections.debug_log.footer" = "在重连后状态隐藏才有效。网络数据包括主机名、IP地址、路由、SSID、认证方式,但私钥不会出现在日志中。"; "diagnostics.items.server_configuration.caption" = "服务端配置"; +"diagnostics.items.app_log.title" = "应用程序"; "diagnostics.items.masks_private_data.caption" = "隐藏网络数据"; "diagnostics.items.report_issue.caption" = "报告连接问题"; diff --git a/PassepartoutLibrary/Sources/PassepartoutUtils/Reusable/LogManager.swift b/PassepartoutLibrary/Sources/PassepartoutUtils/Reusable/LogManager.swift index 9d999af0..d61d1967 100644 --- a/PassepartoutLibrary/Sources/PassepartoutUtils/Reusable/LogManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutUtils/Reusable/LogManager.swift @@ -26,6 +26,7 @@ import Foundation import SwiftyBeaver +@MainActor public class LogManager { public let logFile: URL?