Add app log in Diagnostics screen (#234)

This commit is contained in:
Davide De Rosa 2022-10-16 08:33:32 +02:00 committed by GitHub
parent fbc17877b1
commit 54c53707e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 138 additions and 66 deletions

View File

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- IVPN provider. - IVPN provider.
- OpenVPN: Support for `--route-nopull`. [#230](https://github.com/passepartoutvpn/passepartout-apple/pull/230) - 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 ### Changed

View File

@ -140,36 +140,40 @@ extension Constants {
} }
enum Log { 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 let parentPath = "Library/Caches"
private static func containerLogURL(filename: String) -> URL { static let level: SwiftyBeaver.Level = {
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 = {
guard let levelString = ProcessInfo.processInfo.environment["LOG_LEVEL"], let levelNum = Int(levelString) else { guard let levelString = ProcessInfo.processInfo.environment["LOG_LEVEL"], let levelNum = Int(levelString) else {
return .info return .info
} }
return .init(rawValue: levelNum) ?? .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 refreshInterval: TimeInterval = 5.0
static let tunnelLogMaxBytes = 100000 private static func containerURL(filename: String) -> URL {
Files.containerURL
.appendingPathComponent(parentPath)
.appendingPathComponent(filename)
}
static let tunnelLogRefreshInterval: TimeInterval = 5.0 private static func containerPath(filename: String) -> String {
"\(parentPath)/\(filename)"
}
} }
enum URLs { enum URLs {

View File

@ -24,6 +24,7 @@
// //
import Foundation import Foundation
import PassepartoutLibrary
extension AppContext { extension AppContext {
static let shared = AppContext(coreContext: .shared) static let shared = AppContext(coreContext: .shared)
@ -32,3 +33,7 @@ extension AppContext {
extension ProductManager { extension ProductManager {
static let shared = AppContext.shared.productManager static let shared = AppContext.shared.productManager
} }
extension LogManager {
static let shared = AppContext.shared.logManager
}

View File

@ -29,29 +29,29 @@ import PassepartoutLibrary
@MainActor @MainActor
class AppContext { class AppContext {
private let logManager: LogManager let logManager: LogManager
private let reviewer: Reviewer
let productManager: ProductManager let productManager: ProductManager
private let reviewer: Reviewer
private var cancellables: Set<AnyCancellable> = [] private var cancellables: Set<AnyCancellable> = []
init(coreContext: CoreContext) { init(coreContext: CoreContext) {
logManager = LogManager(logFile: Constants.Log.appLogURL) logManager = LogManager(logFile: Constants.Log.App.url)
logManager.logLevel = Constants.Log.logLevel logManager.logLevel = Constants.Log.level
logManager.logFormat = Constants.Log.logFormat logManager.logFormat = Constants.Log.App.format
logManager.configureLogging() logManager.configureLogging()
pp_log.info("Logging to: \(logManager.logFile!)") pp_log.info("Logging to: \(logManager.logFile!)")
reviewer = Reviewer()
reviewer.eventCountBeforeRating = Constants.Rating.eventCount
productManager = ProductManager( productManager = ProductManager(
appType: Constants.InApp.appType, appType: Constants.InApp.appType,
buildProducts: Constants.InApp.buildProducts buildProducts: Constants.InApp.buildProducts
) )
reviewer = Reviewer()
reviewer.eventCountBeforeRating = Constants.Rating.eventCount
// post // post
configureObjects(coreContext: coreContext) configureObjects(coreContext: coreContext)

View File

@ -28,6 +28,8 @@ import Combine
import PassepartoutLibrary import PassepartoutLibrary
struct DebugLogView: View { struct DebugLogView: View {
private let title: String
private let url: URL private let url: URL
private let timer: AnyPublisher<Date, Never> private let timer: AnyPublisher<Date, Never>
@ -36,7 +38,7 @@ struct DebugLogView: View {
@State private var isSharing = false @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 private let appName = Constants.Global.appName
@ -44,11 +46,17 @@ struct DebugLogView: View {
private let shareFilename = Unlocalized.Issues.Filenames.debugLog 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 self.url = url
timer = Timer.TimerPublisher(interval: updateInterval, runLoop: .main, mode: .common) if let refreshInterval = refreshInterval {
timer = Timer.TimerPublisher(interval: refreshInterval, runLoop: .main, mode: .common)
.autoconnect() .autoconnect()
.eraseToAnyPublisher() .eraseToAnyPublisher()
} else {
timer = Empty(outputType: Date.self, failureType: Never.self)
.eraseToAnyPublisher()
}
} }
var body: some View { var body: some View {
@ -78,7 +86,7 @@ struct DebugLogView: View {
#endif #endif
.edgesIgnoringSafeArea([.leading, .trailing]) .edgesIgnoringSafeArea([.leading, .trailing])
.onReceive(timer, perform: refreshLog) .onReceive(timer, perform: refreshLog)
.navigationTitle(L10n.DebugLog.title) .navigationTitle(title)
.themeDebugLogStyle() .themeDebugLogStyle()
} }

View File

@ -57,8 +57,6 @@ extension DiagnosticsView {
private let vpnProtocol: VPNProtocolType = .openVPN private let vpnProtocol: VPNProtocolType = .openVPN
private let logUpdateInterval = Constants.Log.tunnelLogRefreshInterval
init(providerName: ProviderName?) { init(providerName: ProviderName?) {
providerManager = .shared providerManager = .shared
vpnManager = .shared vpnManager = .shared
@ -108,16 +106,10 @@ extension DiagnosticsView {
private var debugLogSection: some View { private var debugLogSection: some View {
Section { Section {
let url = debugLogURL DebugLogSection(appLogURL: appLogURL, tunnelLogURL: tunnelLogURL)
NavigationLink(L10n.DebugLog.title) {
url.map {
DebugLogView(
url: $0,
updateInterval: logUpdateInterval
)
}
}.disabled(url == nil)
Toggle(L10n.Diagnostics.Items.MasksPrivateData.caption, isOn: $vpnManager.masksPrivateData) Toggle(L10n.Diagnostics.Items.MasksPrivateData.caption, isOn: $vpnManager.masksPrivateData)
} header: {
Text(L10n.DebugLog.title)
} footer: { } footer: {
Text(L10n.Diagnostics.Sections.DebugLog.footer) Text(L10n.Diagnostics.Sections.DebugLog.footer)
} }
@ -161,7 +153,11 @@ extension DiagnosticsView.OpenVPNView {
return cfg.builder(withFallbacks: false) return cfg.builder(withFallbacks: false)
} }
private var debugLogURL: URL? { private var appLogURL: URL? {
LogManager.shared.logFile
}
private var tunnelLogURL: URL? {
vpnManager.debugLogURL(forProtocol: vpnProtocol) vpnManager.debugLogURL(forProtocol: vpnProtocol)
} }
} }

View File

@ -33,8 +33,6 @@ extension DiagnosticsView {
private let providerName: ProviderName? private let providerName: ProviderName?
private let logUpdateInterval = Constants.Log.tunnelLogRefreshInterval
init(providerName: ProviderName?) { init(providerName: ProviderName?) {
vpnManager = .shared vpnManager = .shared
self.providerName = providerName self.providerName = providerName
@ -43,21 +41,21 @@ extension DiagnosticsView {
var body: some View { var body: some View {
List { List {
Section { Section {
let url = debugLogURL DebugLogSection(appLogURL: appLogURL, tunnelLogURL: tunnelLogURL)
NavigationLink(L10n.DebugLog.title) { } header: {
url.map { Text(L10n.DebugLog.title)
DebugLogView(
url: $0,
updateInterval: logUpdateInterval
)
}
}.disabled(url == nil)
} }
} }
} }
}
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)
} }
} }

View File

@ -47,3 +47,46 @@ struct DiagnosticsView: View {
}.navigationTitle(L10n.Diagnostics.title) }.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)
}
}
}

View File

@ -210,6 +210,10 @@ internal enum L10n {
} }
} }
internal enum Items { internal enum Items {
internal enum AppLog {
/// App
internal static let title = L10n.tr("Localizable", "diagnostics.items.app_log.title", fallback: "App")
}
internal enum MasksPrivateData { internal enum MasksPrivateData {
/// Mask network data /// Mask network data
internal static let caption = L10n.tr("Localizable", "diagnostics.items.masks_private_data.caption", fallback: "Mask network data") internal static let caption = L10n.tr("Localizable", "diagnostics.items.masks_private_data.caption", fallback: "Mask network data")

View File

@ -114,8 +114,8 @@ class CoreContext {
private func configureObjects() { private func configureObjects() {
providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager providerManager.rateLimitMilliseconds = Constants.RateLimit.providerManager
vpnManager.tunnelLogPath = Constants.Log.tunnelLogPath vpnManager.tunnelLogPath = Constants.Log.Tunnel.path
vpnManager.tunnelLogFormat = Constants.Log.tunnelLogFormat vpnManager.tunnelLogFormat = Constants.Log.Tunnel.format
profileManager.observeUpdates() profileManager.observeUpdates()
vpnManager.observeUpdates() vpnManager.observeUpdates()

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "Diagnose"; "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.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.server_configuration.caption" = "Serverkonfiguration";
"diagnostics.items.app_log.title" = "App";
"diagnostics.items.masks_private_data.caption" = "Netzwerkdaten zensieren"; "diagnostics.items.masks_private_data.caption" = "Netzwerkdaten zensieren";
"diagnostics.items.report_issue.caption" = "Verbindungsproblem melden"; "diagnostics.items.report_issue.caption" = "Verbindungsproblem melden";

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "Διαγνωστικά"; "diagnostics.title" = "Διαγνωστικά";
"diagnostics.sections.debug_log.footer" = "Η κατάσταση κάλυψης θα είναι αποτελεσματική μετά την επανασύνδεση. Τα δεδομένα δικτύου είναι του διακομιστή, διευθύνσεις IP, δρομολόγηση και SSID. Τα διαπιστευτήρια και τα ιδιωτικά κλειδιά δεν καταγράφονται ανεξάρτητα."; "diagnostics.sections.debug_log.footer" = "Η κατάσταση κάλυψης θα είναι αποτελεσματική μετά την επανασύνδεση. Τα δεδομένα δικτύου είναι του διακομιστή, διευθύνσεις IP, δρομολόγηση και SSID. Τα διαπιστευτήρια και τα ιδιωτικά κλειδιά δεν καταγράφονται ανεξάρτητα.";
"diagnostics.items.server_configuration.caption" = "Ρυθμίσεις Διακομιστή"; "diagnostics.items.server_configuration.caption" = "Ρυθμίσεις Διακομιστή";
"diagnostics.items.app_log.title" = "Εφαρμογή";
"diagnostics.items.masks_private_data.caption" = "Μάσκα δεδομένα δικτύου"; "diagnostics.items.masks_private_data.caption" = "Μάσκα δεδομένα δικτύου";
"diagnostics.items.report_issue.caption" = "Αναφορά ζητήματος συνδεσιμότητας"; "diagnostics.items.report_issue.caption" = "Αναφορά ζητήματος συνδεσιμότητας";

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "Diagnostics"; "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.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.server_configuration.caption" = "Server configuration";
"diagnostics.items.app_log.title" = "App";
"diagnostics.items.masks_private_data.caption" = "Mask network data"; "diagnostics.items.masks_private_data.caption" = "Mask network data";
"diagnostics.items.report_issue.caption" = "Report connectivity issue"; "diagnostics.items.report_issue.caption" = "Report connectivity issue";

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "Diagnósticos"; "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.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.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.masks_private_data.caption" = "Ocultar datos de red";
"diagnostics.items.report_issue.caption" = "Reportar problema de conectividad"; "diagnostics.items.report_issue.caption" = "Reportar problema de conectividad";

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "Diagnostiques"; "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.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.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.masks_private_data.caption" = "Masquer les données de réseau";
"diagnostics.items.report_issue.caption" = "Rapporter un problème de connection"; "diagnostics.items.report_issue.caption" = "Rapporter un problème de connection";

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "Diagnostica"; "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.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.server_configuration.caption" = "Configurazione del server";
"diagnostics.items.app_log.title" = "App";
"diagnostics.items.masks_private_data.caption" = "Maschera dati rete"; "diagnostics.items.masks_private_data.caption" = "Maschera dati rete";
"diagnostics.items.report_issue.caption" = "Segnala problema connettività"; "diagnostics.items.report_issue.caption" = "Segnala problema connettività";

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "Diagnose"; "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.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.server_configuration.caption" = "Server configuratie";
"diagnostics.items.app_log.title" = "App";
"diagnostics.items.masks_private_data.caption" = "Netwerkgegevens maskeren"; "diagnostics.items.masks_private_data.caption" = "Netwerkgegevens maskeren";
"diagnostics.items.report_issue.caption" = "Probleem met connectiviteit melden"; "diagnostics.items.report_issue.caption" = "Probleem met connectiviteit melden";

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "Diagnostyka"; "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.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.server_configuration.caption" = "Konfiguracja serwera";
"diagnostics.items.app_log.title" = "Aplikacja";
"diagnostics.items.masks_private_data.caption" = "Maskuj dane sieci"; "diagnostics.items.masks_private_data.caption" = "Maskuj dane sieci";
"diagnostics.items.report_issue.caption" = "Zgłoś problemy z połączeniem"; "diagnostics.items.report_issue.caption" = "Zgłoś problemy z połączeniem";

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "Diagnóstico"; "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.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.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.masks_private_data.caption" = "Esconder dados da rede";
"diagnostics.items.report_issue.caption" = "Reportar problemas de conexão"; "diagnostics.items.report_issue.caption" = "Reportar problemas de conexão";

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "Диагностика"; "diagnostics.title" = "Диагностика";
"diagnostics.sections.debug_log.footer" = "Маскировка включится после повторного подключения. Информация о сети - это названия хост профилей, IP адрес, маршрутизация и SSID. Данные для входа и приватные ключи не собираются."; "diagnostics.sections.debug_log.footer" = "Маскировка включится после повторного подключения. Информация о сети - это названия хост профилей, IP адрес, маршрутизация и SSID. Данные для входа и приватные ключи не собираются.";
"diagnostics.items.server_configuration.caption" = "Конфигурация сервера"; "diagnostics.items.server_configuration.caption" = "Конфигурация сервера";
"diagnostics.items.app_log.title" = "Приложение";
"diagnostics.items.masks_private_data.caption" = "Маскировать информацию сети"; "diagnostics.items.masks_private_data.caption" = "Маскировать информацию сети";
"diagnostics.items.report_issue.caption" = "Сообщить о проблеме подкл."; "diagnostics.items.report_issue.caption" = "Сообщить о проблеме подкл.";

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "Diagnostics"; "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.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.server_configuration.caption" = "Server konfiguration";
"diagnostics.items.app_log.title" = "App";
"diagnostics.items.masks_private_data.caption" = "Mask nätverksdata"; "diagnostics.items.masks_private_data.caption" = "Mask nätverksdata";
"diagnostics.items.report_issue.caption" = "Rapportera anslutningsproblem"; "diagnostics.items.report_issue.caption" = "Rapportera anslutningsproblem";

View File

@ -255,6 +255,7 @@
"diagnostics.title" = "分析数据"; "diagnostics.title" = "分析数据";
"diagnostics.sections.debug_log.footer" = "在重连后状态隐藏才有效。网络数据包括主机名、IP地址、路由、SSID、认证方式但私钥不会出现在日志中。"; "diagnostics.sections.debug_log.footer" = "在重连后状态隐藏才有效。网络数据包括主机名、IP地址、路由、SSID、认证方式但私钥不会出现在日志中。";
"diagnostics.items.server_configuration.caption" = "服务端配置"; "diagnostics.items.server_configuration.caption" = "服务端配置";
"diagnostics.items.app_log.title" = "应用程序";
"diagnostics.items.masks_private_data.caption" = "隐藏网络数据"; "diagnostics.items.masks_private_data.caption" = "隐藏网络数据";
"diagnostics.items.report_issue.caption" = "报告连接问题"; "diagnostics.items.report_issue.caption" = "报告连接问题";

View File

@ -26,6 +26,7 @@
import Foundation import Foundation
import SwiftyBeaver import SwiftyBeaver
@MainActor
public class LogManager { public class LogManager {
public let logFile: URL? public let logFile: URL?