]> git.ipfire.org Git - thirdparty/wireguard-apple.git/commitdiff
macOS: Ability to view the log
authorRoopesh Chander <roop@roopc.net>
Wed, 27 Mar 2019 12:26:38 +0000 (17:56 +0530)
committerRoopesh Chander <roop@roopc.net>
Thu, 28 Mar 2019 08:27:06 +0000 (13:57 +0530)
Signed-off-by: Roopesh Chander <roop@roopc.net>
WireGuard/WireGuard.xcodeproj/project.pbxproj
WireGuard/WireGuard/Base.lproj/Localizable.strings
WireGuard/WireGuard/UI/macOS/View/LogViewCell.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/macOS/ViewController/LogViewController.swift [new file with mode: 0644]
WireGuard/WireGuard/UI/macOS/ViewController/TunnelsListTableViewController.swift

index ed00af9fd1d63a4cfc710a2cbf73bca8e016f9fc..8ecccdac4edeee26a27d20742137a4867bb3b4ab 100644 (file)
                6FCD99B121E0EDA900BA4C82 /* TunnelEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCD99B021E0EDA900BA4C82 /* TunnelEditViewController.swift */; };
                6FDB3C3B21DCF47400A0C0BF /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB3C3A21DCF47400A0C0BF /* TunnelDetailTableViewController.swift */; };
                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 */; };
                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 */; };
                6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportPanelPresenter.swift; sourceTree = "<group>"; };
                6FCD99B021E0EDA900BA4C82 /* TunnelEditViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditViewController.swift; sourceTree = "<group>"; };
                6FDB3C3A21DCF47400A0C0BF /* TunnelDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDetailTableViewController.swift; sourceTree = "<group>"; };
+               6FDB6D12224A15BE00EE4BC3 /* LogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; };
+               6FDB6D14224CB2CE00EE4BC3 /* LogViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewCell.swift; sourceTree = "<group>"; };
                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 = "<group>"; };
                6FDEF7F621863B6100D8FBF6 /* unzip.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = unzip.c; sourceTree = "<group>"; };
                                6FE3661C21F64F6B00F78C7D /* ConfTextColorTheme.swift */,
                                6F5EA59A223E58A8002B380A /* ButtonRow.swift */,
                                6FB17945222FD5960018AE71 /* OnDemandWiFiControls.swift */,
+                               6FDB6D14224CB2CE00EE4BC3 /* LogViewCell.swift */,
                        );
                        path = View;
                        sourceTree = "<group>";
                                6FDB3C3A21DCF47400A0C0BF /* TunnelDetailTableViewController.swift */,
                                6FCD99A821E0E0C700BA4C82 /* ButtonedDetailViewController.swift */,
                                6FCD99B021E0EDA900BA4C82 /* TunnelEditViewController.swift */,
+                               6FDB6D12224A15BE00EE4BC3 /* LogViewController.swift */,
                        );
                        path = ViewController;
                        sourceTree = "<group>";
                                6FB1BDD321D50F5300A991BF /* ZipArchive.swift in Sources */,
                                6FB1BDD421D50F5300A991BF /* ioapi.c in Sources */,
                                6FDB3C3C21DCF6BB00A0C0BF /* TunnelViewModel.swift in Sources */,
+                               6FDB6D13224A15BF00EE4BC3 /* LogViewController.swift in Sources */,
                                6B5C5E29220A48D30024272E /* Keychain.swift in Sources */,
                                6FCD99AF21E0EA1700BA4C82 /* ImportPanelPresenter.swift in Sources */,
                                6FB1BDD521D50F5300A991BF /* unzip.c in Sources */,
                                6F89E17A21EDEB0E00C97BB9 /* StatusItemController.swift in Sources */,
                                6F5EA59B223E58A8002B380A /* ButtonRow.swift in Sources */,
                                6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */,
+                               6FDB6D15224CB2CE00EE4BC3 /* LogViewCell.swift in Sources */,
                                6FE3661D21F64F6B00F78C7D /* ConfTextColorTheme.swift in Sources */,
                                5F52D0BF21E3788900283CEA /* NSColor+Hex.swift in Sources */,
                                6FB1BDBE21D50F0200A991BF /* Logger.swift in Sources */,
index 5289c72e9d42d080293985bc3762b0a33c952097..08725969ad5c833c22a034766ead4af0c998b440 100644 (file)
 "macMenuManageTunnels" = "Manage tunnels";
 "macMenuImportTunnels" = "Import tunnel(s) from file…";
 "macMenuAddEmptyTunnel" = "Add empty tunnel…";
-"macMenuExportLog" = "Export log to file…";
+"macMenuViewLog" = "View log";
 "macMenuExportTunnels" = "Export tunnels to zip…";
 "macMenuAbout" = "About WireGuard";
 "macMenuQuit" = "Quit";
 
 "macToolTipEditTunnel" = "Edit tunnel (⌘E)";
 "macToolTipToggleStatus" = "Toggle status (⌘T)";
+
+// Mac log view
+
+"macLogColumnTitleTime" = "Time";
+"macLogColumnTitleLogMessage" = "Log message";
+"macLogButtonTitleClose" = "Close";
+"macLogButtonTitleSave" = "Save…";
diff --git a/WireGuard/WireGuard/UI/macOS/View/LogViewCell.swift b/WireGuard/WireGuard/UI/macOS/View/LogViewCell.swift
new file mode 100644 (file)
index 0000000..1e2312a
--- /dev/null
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class LogViewCell: NSTextField {
+    init() {
+        super.init(frame: .zero)
+        isSelectable = false
+        isEditable = false
+        isBordered = false
+        backgroundColor = .clear
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func prepareForReuse() {
+        stringValue = ""
+        preferredMaxLayoutWidth = 0
+    }
+}
+
+class LogViewTimestampCell: LogViewCell {
+    override init() {
+        super.init()
+        maximumNumberOfLines = 1
+        lineBreakMode = .byClipping
+        preferredMaxLayoutWidth = 0
+        setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
+        setContentHuggingPriority(.defaultLow, for: .vertical)
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+class LogViewMessageCell: LogViewCell {
+    override init() {
+        super.init()
+        maximumNumberOfLines = 0
+        lineBreakMode = .byWordWrapping
+        setContentCompressionResistancePriority(.required, for: .vertical)
+        setContentHuggingPriority(.required, for: .vertical)
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
diff --git a/WireGuard/WireGuard/UI/macOS/ViewController/LogViewController.swift b/WireGuard/WireGuard/UI/macOS/ViewController/LogViewController.swift
new file mode 100644 (file)
index 0000000..0816fbc
--- /dev/null
@@ -0,0 +1,244 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class LogViewController: NSViewController {
+
+    enum LogColumn: String {
+        case time = "Time"
+        case logMessage = "LogMessage"
+
+        func createColumn() -> NSTableColumn {
+            return NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue))
+        }
+
+        func isRepresenting(tableColumn: NSTableColumn?) -> Bool {
+            return tableColumn?.identifier.rawValue == rawValue
+        }
+    }
+
+    let scrollView: NSScrollView = {
+        let scrollView = NSScrollView()
+        scrollView.hasVerticalScroller = true
+        scrollView.autohidesScrollers = false
+        scrollView.borderType = .bezelBorder
+        return scrollView
+    }()
+
+    let tableView: NSTableView = {
+        let tableView = NSTableView()
+        let timeColumn = LogColumn.time.createColumn()
+        timeColumn.title = tr("macLogColumnTitleTime")
+        timeColumn.width = 160
+        timeColumn.resizingMask = []
+        tableView.addTableColumn(timeColumn)
+        let messageColumn = LogColumn.logMessage.createColumn()
+        messageColumn.title = tr("macLogColumnTitleLogMessage")
+        messageColumn.minWidth = 360
+        messageColumn.resizingMask = .autoresizingMask
+        tableView.addTableColumn(messageColumn)
+        tableView.rowSizeStyle = .custom
+        tableView.rowHeight = 16
+        tableView.usesAlternatingRowBackgroundColors = true
+        tableView.usesAutomaticRowHeights = true
+        tableView.allowsColumnReordering = false
+        tableView.allowsColumnResizing = true
+        tableView.allowsMultipleSelection = true
+        return tableView
+    }()
+
+    let progressIndicator: NSProgressIndicator = {
+        let progressIndicator = NSProgressIndicator()
+        progressIndicator.controlSize = .small
+        progressIndicator.isIndeterminate = true
+        progressIndicator.style = .spinning
+        progressIndicator.isDisplayedWhenStopped = false
+        return progressIndicator
+    }()
+
+    let closeButton: NSButton = {
+        let button = NSButton()
+        button.title = tr("macLogButtonTitleClose")
+        button.setButtonType(.momentaryPushIn)
+        button.bezelStyle = .rounded
+        return button
+    }()
+
+    let saveButton: NSButton = {
+        let button = NSButton()
+        button.title = tr("macLogButtonTitleSave")
+        button.setButtonType(.momentaryPushIn)
+        button.bezelStyle = .rounded
+        return button
+    }()
+
+    let logViewHelper: LogViewHelper?
+    var logEntries = [LogViewHelper.LogEntry]()
+    var isFetchingLogEntries = false
+
+    private var updateLogEntriesTimer: Timer?
+
+    init() {
+        logViewHelper = LogViewHelper(logFilePath: FileManager.logFileURL?.path)
+        super.init(nibName: nil, bundle: nil)
+    }
+
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+
+    override func loadView() {
+        tableView.dataSource = self
+        tableView.delegate = self
+
+        closeButton.target = self
+        closeButton.action = #selector(closeClicked)
+        closeButton.isEnabled = false
+
+        saveButton.target = self
+        saveButton.action = #selector(saveClicked)
+        saveButton.isEnabled = false
+
+        let clipView = NSClipView()
+        clipView.documentView = tableView
+        scrollView.contentView = clipView
+
+        let margin: CGFloat = 20
+        let internalSpacing: CGFloat = 10
+
+        let buttonRowStackView = NSStackView()
+        buttonRowStackView.addView(closeButton, in: .leading)
+        buttonRowStackView.addView(saveButton, in: .trailing)
+        buttonRowStackView.orientation = .horizontal
+        buttonRowStackView.spacing = internalSpacing
+
+        let containerView = NSView()
+        [scrollView, progressIndicator, buttonRowStackView].forEach { view in
+            containerView.addSubview(view)
+            view.translatesAutoresizingMaskIntoConstraints = false
+        }
+        NSLayoutConstraint.activate([
+            scrollView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: margin),
+            scrollView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: margin),
+            containerView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: margin),
+            buttonRowStackView.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: internalSpacing),
+            buttonRowStackView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: margin),
+            containerView.rightAnchor.constraint(equalTo: buttonRowStackView.rightAnchor, constant: margin),
+            containerView.bottomAnchor.constraint(equalTo: buttonRowStackView.bottomAnchor, constant: margin),
+            progressIndicator.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
+            progressIndicator.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor)
+        ])
+
+        NSLayoutConstraint.activate([
+            containerView.widthAnchor.constraint(equalToConstant: 640),
+            containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 240)
+        ])
+
+        containerView.frame = NSRect(x: 0, y: 0, width: 640, height: 480)
+
+        view = containerView
+
+        progressIndicator.startAnimation(self)
+        startUpdatingLogEntries()
+    }
+
+    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.progressIndicator.isHidden {
+                self.progressIndicator.stopAnimation(self)
+                self.closeButton.isEnabled = true
+                self.saveButton.isEnabled = true
+            }
+            guard !fetchedLogEntries.isEmpty else { return }
+            let numOfEntries = self.logEntries.count
+            let lastVisibleRowIndex = self.tableView.row(at: NSPoint(x: 0, y: self.scrollView.contentView.documentVisibleRect.maxY - 1))
+            let isScrolledToEnd = lastVisibleRowIndex == numOfEntries - 1
+            self.logEntries.append(contentsOf: fetchedLogEntries)
+            self.tableView.insertRows(at: IndexSet(integersIn: numOfEntries ..< numOfEntries + fetchedLogEntries.count), withAnimation: .slideDown)
+            if isScrolledToEnd {
+                self.tableView.scrollRowToVisible(self.logEntries.count - 1)
+            }
+        }
+    }
+
+    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 saveClicked() {
+        let savePanel = NSSavePanel()
+        savePanel.prompt = tr("macSheetButtonExportLog")
+        savePanel.nameFieldLabel = tr("macNameFieldExportLog")
+
+        let dateFormatter = ISO8601DateFormatter()
+        dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename
+        let timeStampString = dateFormatter.string(from: Date())
+        savePanel.nameFieldStringValue = "wireguard-log-\(timeStampString).txt"
+
+        savePanel.beginSheetModal(for: self.view.window!) { [weak self] response in
+            guard response == .OK else { return }
+            guard let destinationURL = savePanel.url else { return }
+
+            DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+                let isWritten = Logger.global?.writeLog(to: destinationURL.path) ?? false
+                guard isWritten else {
+                    DispatchQueue.main.async { [weak self] in
+                        ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
+                    }
+                    return
+                }
+                DispatchQueue.main.async { [weak self] in
+                    self?.dismiss(self)
+                }
+            }
+
+        }
+    }
+
+    @objc func closeClicked() {
+        dismiss(self)
+    }
+
+    @objc func copy(_ sender: Any?) {
+        let text = tableView.selectedRowIndexes.sorted().reduce("") { $0 + self.logEntries[$1].text() + "\n" }
+        let pasteboard = NSPasteboard.general
+        pasteboard.clearContents()
+        pasteboard.writeObjects([text as NSString])
+    }
+}
+
+extension LogViewController: NSTableViewDataSource {
+    func numberOfRows(in tableView: NSTableView) -> Int {
+        return logEntries.count
+    }
+}
+
+extension LogViewController: NSTableViewDelegate {
+    func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
+        if LogColumn.time.isRepresenting(tableColumn: tableColumn) {
+            let cell: LogViewTimestampCell = tableView.dequeueReusableCell()
+            cell.stringValue = logEntries[row].timestamp
+            return cell
+        } else if LogColumn.logMessage.isRepresenting(tableColumn: tableColumn) {
+            let cell: LogViewMessageCell = tableView.dequeueReusableCell()
+            cell.stringValue = logEntries[row].message
+            cell.preferredMaxLayoutWidth = tableColumn?.width ?? 0
+            return cell
+        } else {
+            fatalError()
+        }
+    }
+}
index 167aa0a66daf637b00ba56727b98cef0ff1de6bf..b694f3d9c66c5b0ba23f7229d4729042cf02b75f 100644 (file)
@@ -53,7 +53,7 @@ class TunnelsListTableViewController: NSViewController {
 
         let menu = NSMenu()
         menu.addItem(imageItem)
-        menu.addItem(withTitle: tr("macMenuExportLog"), action: #selector(handleExportLogAction), keyEquivalent: "")
+        menu.addItem(withTitle: tr("macMenuViewLog"), action: #selector(handleViewLogAction), keyEquivalent: "")
         menu.addItem(withTitle: tr("macMenuExportTunnels"), action: #selector(handleExportTunnelsAction), keyEquivalent: "")
         menu.autoenablesItems = false
 
@@ -190,32 +190,9 @@ class TunnelsListTableViewController: NSViewController {
         }
     }
 
-    @objc func handleExportLogAction() {
-        guard let window = view.window else { return }
-        let savePanel = NSSavePanel()
-        savePanel.prompt = tr("macSheetButtonExportLog")
-        savePanel.nameFieldLabel = tr("macNameFieldExportLog")
-
-        let dateFormatter = ISO8601DateFormatter()
-        dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename
-        let timeStampString = dateFormatter.string(from: Date())
-        savePanel.nameFieldStringValue = "wireguard-log-\(timeStampString).txt"
-
-        savePanel.beginSheetModal(for: window) { response in
-            guard response == .OK else { return }
-            guard let destinationURL = savePanel.url else { return }
-
-            DispatchQueue.global(qos: .userInitiated).async {
-                let isWritten = Logger.global?.writeLog(to: destinationURL.path) ?? false
-                guard isWritten else {
-                    DispatchQueue.main.async { [weak self] in
-                        ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
-                    }
-                    return
-                }
-            }
-
-        }
+    @objc func handleViewLogAction() {
+        let logVC = LogViewController()
+        self.presentAsSheet(logVC)
     }
 
     @objc func handleExportTunnelsAction() {