diff --git a/WireGuard/WireGuard.xcodeproj/project.pbxproj b/WireGuard/WireGuard.xcodeproj/project.pbxproj index 53bc8c1..9ae6f0f 100644 --- a/WireGuard/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard/WireGuard.xcodeproj/project.pbxproj @@ -160,6 +160,7 @@ 6FDB3C3C21DCF6BB00A0C0BF /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; }; 6FDB6D13224A15BF00EE4BC3 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB6D12224A15BE00EE4BC3 /* LogViewController.swift */; }; 6FDB6D15224CB2CE00EE4BC3 /* LogViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB6D14224CB2CE00EE4BC3 /* LogViewCell.swift */; }; + 6FDB6D18224CC05A00EE4BC3 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB6D16224CC04E00EE4BC3 /* LogViewController.swift */; }; 6FDEF7E421846C1A00D8FBF6 /* libwg-go.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */; }; 6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */; }; 6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7F621863B6100D8FBF6 /* unzip.c */; }; @@ -364,6 +365,7 @@ 6FDB3C3A21DCF47400A0C0BF /* TunnelDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDetailTableViewController.swift; sourceTree = ""; }; 6FDB6D12224A15BE00EE4BC3 /* LogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = ""; }; 6FDB6D14224CB2CE00EE4BC3 /* LogViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewCell.swift; sourceTree = ""; }; + 6FDB6D16224CC04E00EE4BC3 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = ""; }; 6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libwg-go.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanViewController.swift; sourceTree = ""; }; 6FDEF7F621863B6100D8FBF6 /* unzip.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = unzip.c; sourceTree = ""; }; @@ -461,6 +463,7 @@ 6F7774DF217181B1006A79B3 /* MainViewController.swift */, 6F8F0D7622267C57000E8335 /* SSIDOptionEditTableViewController.swift */, 6F9B8A8D223398610041B9C4 /* SSIDOptionDetailTableViewController.swift */, + 6FDB6D16224CC04E00EE4BC3 /* LogViewController.swift */, ); path = ViewController; sourceTree = ""; @@ -1343,6 +1346,7 @@ 6B586C53220CBA6D00427C51 /* Data+KeyEncoding.swift in Sources */, 6F693A562179E556008551C1 /* Endpoint.swift in Sources */, 6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */, + 6FDB6D18224CC05A00EE4BC3 /* LogViewController.swift in Sources */, 6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */, 5FF7B96221CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */, 5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */, diff --git a/WireGuard/WireGuard/Base.lproj/Localizable.strings b/WireGuard/WireGuard/Base.lproj/Localizable.strings index 0872596..c4e9cb1 100644 --- a/WireGuard/WireGuard/Base.lproj/Localizable.strings +++ b/WireGuard/WireGuard/Base.lproj/Localizable.strings @@ -209,10 +209,14 @@ "settingsSectionTitleExportConfigurations" = "Export configurations"; "settingsExportZipButtonTitle" = "Export zip archive"; -"settingsSectionTitleTunnelLog" = "Tunnel log"; -"settingsExportLogFileButtonTitle" = "Export log file"; +"settingsSectionTitleTunnelLog" = "Log"; +"settingsViewLogButtonTitle" = "View log"; -// Settings alerts +// Log view + +"logViewTitle" = "Log"; + +// Log alerts "alertUnableToRemovePreviousLogTitle" = "Log export failed"; "alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared"; diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/LogViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/LogViewController.swift new file mode 100644 index 0000000..bcfbaf5 --- /dev/null +++ b/WireGuard/WireGuard/UI/iOS/ViewController/LogViewController.swift @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. + +import UIKit + +class LogViewController: UIViewController { + + let textView: UITextView = { + let textView = UITextView() + textView.isEditable = false + textView.isSelectable = false + textView.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body) + textView.adjustsFontForContentSizeCategory = true + return textView + }() + + let busyIndicator: UIActivityIndicatorView = { + let busyIndicator = UIActivityIndicatorView(style: .gray) + busyIndicator.hidesWhenStopped = true + return busyIndicator + }() + + var logViewHelper: LogViewHelper? + var isFetchingLogEntries = false + private var updateLogEntriesTimer: Timer? + + override func loadView() { + view = UIView() + view.backgroundColor = .white + + view.addSubview(textView) + textView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + textView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + textView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + textView.topAnchor.constraint(equalTo: view.topAnchor), + textView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + view.addSubview(busyIndicator) + busyIndicator.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), + busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + + busyIndicator.startAnimating() + + logViewHelper = LogViewHelper(logFilePath: FileManager.logFileURL?.path) + startUpdatingLogEntries() + } + + override func viewDidLoad() { + title = tr("logViewTitle") + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped(sender:))) + } + + func updateLogEntries() { + guard !isFetchingLogEntries else { return } + isFetchingLogEntries = true + logViewHelper?.fetchLogEntriesSinceLastFetch { [weak self] fetchedLogEntries in + guard let self = self else { return } + defer { + self.isFetchingLogEntries = false + } + if self.busyIndicator.isAnimating { + self.busyIndicator.stopAnimating() + } + guard !fetchedLogEntries.isEmpty else { return } + let isScrolledToEnd = self.textView.contentSize.height - self.textView.bounds.height - self.textView.contentOffset.y < 1 + let text = fetchedLogEntries.reduce("") { $0 + $1.text() + "\n" } + self.textView.insertText(text) + if isScrolledToEnd { + let endOfCurrentText = NSRange(location: (self.textView.text as NSString).length, length: 0) + self.textView.scrollRangeToVisible(endOfCurrentText) + } + } + } + + func startUpdatingLogEntries() { + updateLogEntries() + updateLogEntriesTimer?.invalidate() + let timer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in + self?.updateLogEntries() + } + updateLogEntriesTimer = timer + RunLoop.main.add(timer, forMode: .common) + } + + @objc func saveTapped(sender: AnyObject) { + guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } + + let dateFormatter = ISO8601DateFormatter() + dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename + let timeStampString = dateFormatter.string(from: Date()) + let destinationURL = destinationDir.appendingPathComponent("wireguard-log-\(timeStampString).txt") + + DispatchQueue.global(qos: .userInitiated).async { + + if FileManager.default.fileExists(atPath: destinationURL.path) { + let isDeleted = FileManager.deleteFile(at: destinationURL) + if !isDeleted { + ErrorPresenter.showErrorAlert(title: tr("alertUnableToRemovePreviousLogTitle"), message: tr("alertUnableToRemovePreviousLogMessage"), from: self) + return + } + } + + let isWritten = Logger.global?.writeLog(to: destinationURL.path) ?? false + + DispatchQueue.main.async { + guard isWritten else { + ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self) + return + } + let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil) + if let sender = sender as? UIBarButtonItem { + activityVC.popoverPresentationController?.barButtonItem = sender + } + activityVC.completionWithItemsHandler = { _, _, _, _ in + // Remove the exported log file after the activity has completed + _ = FileManager.deleteFile(at: destinationURL) + } + self.present(activityVC, animated: true) + } + } + } +} diff --git a/WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift b/WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift index ff83b2c..9956b7b 100644 --- a/WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/ViewController/SettingsTableViewController.swift @@ -10,14 +10,14 @@ class SettingsTableViewController: UITableViewController { case iosAppVersion case goBackendVersion case exportZipArchive - case exportLogFile + case viewLog var localizedUIString: String { switch self { case .iosAppVersion: return tr("settingsVersionKeyWireGuardForIOS") case .goBackendVersion: return tr("settingsVersionKeyWireGuardGoBackend") case .exportZipArchive: return tr("settingsExportZipButtonTitle") - case .exportLogFile: return tr("settingsExportLogFileButtonTitle") + case .viewLog: return tr("settingsViewLogButtonTitle") } } } @@ -25,7 +25,7 @@ class SettingsTableViewController: UITableViewController { let settingsFieldsBySection: [[SettingsFields]] = [ [.iosAppVersion, .goBackendVersion], [.exportZipArchive], - [.exportLogFile] + [.viewLog] ] let tunnelsManager: TunnelsManager? @@ -108,41 +108,10 @@ class SettingsTableViewController: UITableViewController { } } - func exportLogForLastActivatedTunnel(sourceView: UIView) { - guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } + func presentLogView() { + let logVC = LogViewController() + navigationController?.pushViewController(logVC, animated: true) - let dateFormatter = ISO8601DateFormatter() - dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename - let timeStampString = dateFormatter.string(from: Date()) - let destinationURL = destinationDir.appendingPathComponent("wireguard-log-\(timeStampString).txt") - - DispatchQueue.global(qos: .userInitiated).async { - - if FileManager.default.fileExists(atPath: destinationURL.path) { - let isDeleted = FileManager.deleteFile(at: destinationURL) - if !isDeleted { - ErrorPresenter.showErrorAlert(title: tr("alertUnableToRemovePreviousLogTitle"), message: tr("alertUnableToRemovePreviousLogMessage"), from: self) - return - } - } - - let isWritten = Logger.global?.writeLog(to: destinationURL.path) ?? false - - DispatchQueue.main.async { - guard isWritten else { - ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self) - return - } - let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil) - activityVC.popoverPresentationController?.sourceView = sourceView - activityVC.popoverPresentationController?.sourceRect = sourceView.bounds - activityVC.completionWithItemsHandler = { _, _, _, _ in - // Remove the exported log file after the activity has completed - _ = FileManager.deleteFile(at: destinationURL) - } - self.present(activityVC, animated: true) - } - } } } @@ -192,11 +161,11 @@ extension SettingsTableViewController { } return cell } else { - assert(field == .exportLogFile) + assert(field == .viewLog) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) cell.buttonText = field.localizedUIString cell.onTapped = { [weak self] in - self?.exportLogForLastActivatedTunnel(sourceView: cell.button) + self?.presentLogView() } return cell }