diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-08-19 10:10:42 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-08-19 10:10:42 +0200 |
| commit | dd1689df906ae8f4782f25970aba56bd3e3bf9d5 (patch) | |
| tree | 9395ff09302297ba0f0a115ab738d79af1ae4ef6 | |
| parent | 1ff1153eb12ac0607627d3d56ba4bfa69071c641 (diff) | |
| parent | 18a02b392cff331d3d6f10a340500c343c6c2588 (diff) | |
| download | mullvadvpn-dd1689df906ae8f4782f25970aba56bd3e3bf9d5.tar.xz mullvadvpn-dd1689df906ae8f4782f25970aba56bd3e3bf9d5.zip | |
Merge branch 'show-logs'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 24 | ||||
| -rw-r--r-- | ios/MullvadVPN/ApplicationConfiguration.swift | 13 | ||||
| -rw-r--r-- | ios/MullvadVPN/Base.lproj/Main.storyboard | 36 | ||||
| -rw-r--r-- | ios/MullvadVPN/LogStreamerViewController.swift | 182 | ||||
| -rw-r--r-- | ios/MullvadVPN/Logging/CustomFormatLogHandler.swift | 23 | ||||
| -rw-r--r-- | ios/MullvadVPN/Logging/LogEntryParser.swift | 98 | ||||
| -rw-r--r-- | ios/MullvadVPN/Logging/LogStreamer.swift | 141 | ||||
| -rw-r--r-- | ios/MullvadVPN/Logging/StringStreamIterator.swift | 58 | ||||
| -rw-r--r-- | ios/MullvadVPN/Logging/TextFileStream.swift | 77 | ||||
| -rw-r--r-- | ios/MullvadVPN/SettingsViewController.swift | 18 | ||||
| -rw-r--r-- | ios/PacketTunnel/WireguardDevice.swift | 3 |
11 files changed, 652 insertions, 21 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index de3226aa5d..f5516375e0 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 580EE22824B3289300F9D8A1 /* AssociatedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */; }; 580EE22924B3289300F9D8A1 /* AssociatedValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */; }; 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */; }; + 58141EC924DAC0ED0013F79C /* TextFileStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58141EC824DAC0ED0013F79C /* TextFileStream.swift */; }; 5815039724D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */; }; 5815039824D6ECAE00C9C50E /* CustomFormatLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */; }; 5815039D24D6ECE600C9C50E /* TextFileOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5815039C24D6ECE600C9C50E /* TextFileOutputStream.swift */; }; @@ -89,6 +90,7 @@ 5857F24724C882D700CF6F47 /* SelectLocationNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5857F24624C882D700CF6F47 /* SelectLocationNavigationController.swift */; }; 585834F824D2BC1F00A8AF56 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 585834F724D2BC1F00A8AF56 /* Logging */; }; 585834FC24D2BC9500A8AF56 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 585834FB24D2BC9500A8AF56 /* Logging */; }; + 585FE2F124E1365400439C50 /* LogStreamer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585FE2F024E1365400439C50 /* LogStreamer.swift */; }; 5860F1C223A785C600CEA666 /* WireguardDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860F1C123A785C600CEA666 /* WireguardDevice.swift */; }; 5860F1C423A8D25F00CEA666 /* WireguardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860F1C323A8D25F00CEA666 /* WireguardConfiguration.swift */; }; 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */; }; @@ -152,6 +154,9 @@ 58BFA5CC22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; 58BFA5CD22A7CE1F00A6173D /* ApplicationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */; }; 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */; }; + 58C3B06724EA768100C0348E /* LogStreamerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3B06624EA768100C0348E /* LogStreamerViewController.swift */; }; + 58C3B06924EAA25000C0348E /* StringStreamIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C3B06824EAA25000C0348E /* StringStreamIterator.swift */; }; + 58C4CB0124EBE5A700A22D49 /* LogEntryParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C4CB0024EBE5A700A22D49 /* LogEntryParser.swift */; }; 58C6B34F22BB7AC0003C19AD /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B34E22BB7AC0003C19AD /* IPAddressRange.swift */; }; 58C6B35122BB7CFD003C19AD /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B34E22BB7AC0003C19AD /* IPAddressRange.swift */; }; 58C6B35422BB87C4003C19AD /* WireguardPrivateKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C6B35322BB87C4003C19AD /* WireguardPrivateKey.swift */; }; @@ -268,6 +273,7 @@ 580EE22324B3243100F9D8A1 /* AsyncBlockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncBlockOperation.swift; sourceTree = "<group>"; }; 580EE22724B3289300F9D8A1 /* AssociatedValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssociatedValue.swift; sourceTree = "<group>"; }; 5811DE4F239014550011EB53 /* NEVPNStatus+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NEVPNStatus+Debug.swift"; sourceTree = "<group>"; }; + 58141EC824DAC0ED0013F79C /* TextFileStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileStream.swift; sourceTree = "<group>"; }; 5815039324D6EB7200C9C50E /* LogRotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRotation.swift; sourceTree = "<group>"; }; 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatLogHandler.swift; sourceTree = "<group>"; }; 5815039C24D6ECE600C9C50E /* TextFileOutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileOutputStream.swift; sourceTree = "<group>"; }; @@ -290,6 +296,7 @@ 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPEndpoint.swift; sourceTree = "<group>"; }; 5857F24224C8662600CF6F47 /* SelectLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationHeaderView.swift; sourceTree = "<group>"; }; 5857F24624C882D700CF6F47 /* SelectLocationNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectLocationNavigationController.swift; sourceTree = "<group>"; }; + 585FE2F024E1365400439C50 /* LogStreamer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStreamer.swift; sourceTree = "<group>"; }; 5860F1C123A785C600CEA666 /* WireguardDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardDevice.swift; sourceTree = "<group>"; }; 5860F1C323A8D25F00CEA666 /* WireguardConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardConfiguration.swift; sourceTree = "<group>"; }; 5862805322428EF100F5A6E1 /* TranslucentButtonBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslucentButtonBlurView.swift; sourceTree = "<group>"; }; @@ -335,6 +342,9 @@ 58BFA5C522A7C97F00A6173D /* RelayCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCache.swift; sourceTree = "<group>"; }; 58BFA5CB22A7CE1F00A6173D /* ApplicationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationConfiguration.swift; sourceTree = "<group>"; }; 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputGroupView.swift; sourceTree = "<group>"; }; + 58C3B06624EA768100C0348E /* LogStreamerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStreamerViewController.swift; sourceTree = "<group>"; }; + 58C3B06824EAA25000C0348E /* StringStreamIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringStreamIterator.swift; sourceTree = "<group>"; }; + 58C4CB0024EBE5A700A22D49 /* LogEntryParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntryParser.swift; sourceTree = "<group>"; }; 58C6B34E22BB7AC0003C19AD /* IPAddressRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPAddressRange.swift; sourceTree = "<group>"; }; 58C6B35322BB87C4003C19AD /* WireguardPrivateKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireguardPrivateKey.swift; sourceTree = "<group>"; }; 58C6B35D22BBBFE3003C19AD /* Data+HexCoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+HexCoding.swift"; sourceTree = "<group>"; }; @@ -462,9 +472,13 @@ children = ( 581503A224D6F1EC00C9C50E /* ChainedError+Logger.swift */, 5815039624D6ECAE00C9C50E /* CustomFormatLogHandler.swift */, - 5815039C24D6ECE600C9C50E /* TextFileOutputStream.swift */, + 58C4CB0024EBE5A700A22D49 /* LogEntryParser.swift */, 581503A524D6F4AE00C9C50E /* Logging.swift */, 5815039324D6EB7200C9C50E /* LogRotation.swift */, + 585FE2F024E1365400439C50 /* LogStreamer.swift */, + 58C3B06824EAA25000C0348E /* StringStreamIterator.swift */, + 5815039C24D6ECE600C9C50E /* TextFileOutputStream.swift */, + 58141EC824DAC0ED0013F79C /* TextFileStream.swift */, ); path = Logging; sourceTree = "<group>"; @@ -510,7 +524,6 @@ 58CE5E62224146200008646E /* MullvadVPN */ = { isa = PBXGroup; children = ( - 5815039F24D6ECF200C9C50E /* Logging */, 587AD7C92342283900E93A53 /* Account.swift */, 582BB1B42295780F0055B6EF /* AccountExpiry.swift */, 58C3A4B122456F1A00340BDB /* AccountInputGroupView.swift */, @@ -554,7 +567,9 @@ 58CE5E6C224146210008646E /* LaunchScreen.storyboard */, 58A1AA8623F43901009F7EA6 /* Location.swift */, 58BA692D23E99EFF009DC256 /* Locking.swift */, + 5815039F24D6ECF200C9C50E /* Logging */, 58CE5E65224146200008646E /* LoginViewController.swift */, + 58C3B06624EA768100C0348E /* LogStreamerViewController.swift */, 58CE5E67224146200008646E /* Main.storyboard */, 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */, 58CB0EDF24B86751001EF0D8 /* MullvadRest.swift */, @@ -992,6 +1007,7 @@ 582BB1AF229566420055B6EF /* SettingsCell.swift in Sources */, 5873884D239E6D7E00E96C4E /* EmbeddedViewContainerView.swift in Sources */, 58F3C0A4249CB069003E76BE /* HeaderBarView.swift in Sources */, + 585FE2F124E1365400439C50 /* LogStreamer.swift in Sources */, 58B9EB132488ED2100095626 /* AlertPresenter.swift in Sources */, 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */, 5862805422428EF100F5A6E1 /* TranslucentButtonBlurView.swift in Sources */, @@ -1010,6 +1026,7 @@ 58C6B35E22BBBFE3003C19AD /* Data+HexCoding.swift in Sources */, 5857F24324C8662600CF6F47 /* SelectLocationHeaderView.swift in Sources */, 58AEEF652344A36000C9BBD5 /* KeychainError.swift in Sources */, + 58C3B06924EAA25000C0348E /* StringStreamIterator.swift in Sources */, 580EE22824B3289300F9D8A1 /* AssociatedValue.swift in Sources */, 581503A624D6F4AE00C9C50E /* Logging.swift in Sources */, 58CCA01222424D11004F3011 /* SettingsViewController.swift in Sources */, @@ -1029,10 +1046,13 @@ 587AD7C623421D7000E93A53 /* TunnelSettings.swift in Sources */, 581503A324D6F1EC00C9C50E /* ChainedError+Logger.swift in Sources */, 58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */, + 58C3B06724EA768100C0348E /* LogStreamerViewController.swift in Sources */, 58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, 58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */, 5811DE50239014550011EB53 /* NEVPNStatus+Debug.swift in Sources */, + 58141EC924DAC0ED0013F79C /* TextFileStream.swift in Sources */, 58C3A4B222456F1B00340BDB /* AccountInputGroupView.swift in Sources */, + 58C4CB0124EBE5A700A22D49 /* LogEntryParser.swift in Sources */, 58F840B22464491D0044E708 /* ChainedError.swift in Sources */, 58FAEDFF24533A7000CB0F5B /* KeychainReturn.swift in Sources */, 580EE20C24B3225F00F9D8A1 /* DelayOperation.swift in Sources */, diff --git a/ios/MullvadVPN/ApplicationConfiguration.swift b/ios/MullvadVPN/ApplicationConfiguration.swift index 90e4849882..8dc5063fcb 100644 --- a/ios/MullvadVPN/ApplicationConfiguration.swift +++ b/ios/MullvadVPN/ApplicationConfiguration.swift @@ -15,4 +15,17 @@ class ApplicationConfiguration { /// The application identifier for the PacketTunnel extension static let packetTunnelExtensionIdentifier = "net.mullvad.MullvadVPN.PacketTunnel" + + /// The application log files + static var logFileURLs: [URL] { + let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Self.securityGroupIdentifier) + let fileNames = ["net.mullvad.MullvadVPN", "net.mullvad.MullvadVPN.PacketTunnel"] + + return fileNames.compactMap { (fileName) -> URL? in + return containerURL? + .appendingPathComponent("Logs", isDirectory: true) + .appendingPathComponent(fileName, isDirectory: false) + .appendingPathExtension("log") + } + } } diff --git a/ios/MullvadVPN/Base.lproj/Main.storyboard b/ios/MullvadVPN/Base.lproj/Main.storyboard index 7bb8bab957..f2ce083c98 100644 --- a/ios/MullvadVPN/Base.lproj/Main.storyboard +++ b/ios/MullvadVPN/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="ZwP-1v-DUg"> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="ZwP-1v-DUg"> <device id="retina4_7" orientation="portrait" appearance="light"/> <dependencies> <deployment identifier="iOS"/> @@ -406,6 +406,32 @@ <outlet property="titleLabel" destination="Amw-A3-ePS" id="cGS-cX-LXr"/> </connections> </tableViewCell> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Basic" id="kzz-4X-xg1" customClass="SettingsBasicCell" customModule="MullvadVPN" customModuleProvider="target"> + <rect key="frame" x="0.0" y="186" width="375" height="43.5"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="kzz-4X-xg1" id="KpJ-UC-PyV"> + <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PWF-Y6-mDf"> + <rect key="frame" x="16" y="11" width="343" height="21.5"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <constraints> + <constraint firstItem="PWF-Y6-mDf" firstAttribute="top" secondItem="KpJ-UC-PyV" secondAttribute="topMargin" id="7c8-Np-uyI"/> + <constraint firstAttribute="trailingMargin" secondItem="PWF-Y6-mDf" secondAttribute="trailing" id="G6s-Z9-acp"/> + <constraint firstAttribute="bottomMargin" secondItem="PWF-Y6-mDf" secondAttribute="bottom" id="ICb-f1-Zux"/> + <constraint firstItem="PWF-Y6-mDf" firstAttribute="leading" secondItem="KpJ-UC-PyV" secondAttribute="leadingMargin" id="teH-t2-aJn"/> + </constraints> + </tableViewCellContentView> + <color key="backgroundColor" name="Primary"/> + <connections> + <outlet property="titleLabel" destination="PWF-Y6-mDf" id="G5i-Wm-WZN"/> + </connections> + </tableViewCell> </prototypes> <sections/> <connections> @@ -905,7 +931,7 @@ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JYh-33-d0O"> - <rect key="frame" x="0.0" y="0.0" width="375" height="598"/> + <rect key="frame" x="0.0" y="0.0" width="375" height="597"/> <subviews> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="N9k-cQ-tlw" userLabel="Content view"> <rect key="frame" x="0.0" y="0.0" width="375" height="558"/> @@ -944,7 +970,7 @@ <nil key="highlightedColor"/> </label> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Cas-Tk-gcz" customClass="LinkButton" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="20" y="516" width="20" height="22"/> + <rect key="frame" x="20" y="516" width="128" height="22"/> <fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="18"/> <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <state key="normal" title="Privacy Policy" image="IconExtlink"/> @@ -980,10 +1006,10 @@ </constraints> </scrollView> <view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="16P-Q0-ZO9" userLabel="Footer"> - <rect key="frame" x="0.0" y="598" width="375" height="69"/> + <rect key="frame" x="0.0" y="597" width="375" height="70"/> <subviews> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ttw-7B-1MM" customClass="AppButton" customModule="MullvadVPN" customModuleProvider="target"> - <rect key="frame" x="16" y="24" width="343" height="21"/> + <rect key="frame" x="16" y="24" width="343" height="22"/> <accessibility key="accessibilityConfiguration" identifier="AgreeButton"/> <state key="normal" title="Agree and continue" backgroundImage="DefaultButton"> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> diff --git a/ios/MullvadVPN/LogStreamerViewController.swift b/ios/MullvadVPN/LogStreamerViewController.swift new file mode 100644 index 0000000000..d033a8fc70 --- /dev/null +++ b/ios/MullvadVPN/LogStreamerViewController.swift @@ -0,0 +1,182 @@ +// +// LogStreamerViewController.swift +// MullvadVPN +// +// Created by pronebird on 17/08/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +#if DEBUG + +import Foundation +import UIKit +import Logging + +class LogStreamerViewController: UIViewController, UITextViewDelegate { + + private let textView = UITextView() + private let streamer: LogStreamer<UTF8> + private let logEntryParser = LogEntryParser() + private var currentTextColor: UIColor? + private let timestampFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss.SSS" + return formatter + }() + + private var autoScrollButtonItem: UIBarButtonItem { + return UIBarButtonItem(barButtonSystemItem: autoScroll ? .pause : .play, target: self, action: #selector(handleToggleAutoscroll(_:))) + } + + private var dismissButtonItem: UIBarButtonItem { + return UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDismissButton(_:))) + } + + var autoScroll: Bool = true { + didSet { + updateAutoScrollBarItem() + handleAutoScroll() + } + } + + init(fileURLs: [URL]) { + streamer = LogStreamer(fileURLs: fileURLs) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - View lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.title = NSLocalizedString("App logs", comment: "") + navigationItem.leftBarButtonItem = autoScrollButtonItem + navigationItem.rightBarButtonItem = dismissButtonItem + + addSubviews() + startStreamer() + } + + // MARK: - UITextViewDelegate + + func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { + let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview) + + // Disable autoscroll if user scrolled up + if translation.y > 0 { + autoScroll = false + } + } + + func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { + // Disable autoscroll when user requested scroll to top + autoScroll = false + return true + } + + // MARK: - Private + + private func addSubviews() { + textView.translatesAutoresizingMaskIntoConstraints = false + textView.isEditable = false + if #available(iOS 13.0, *) { + textView.font = UIFont.monospacedSystemFont(ofSize: UIFont.systemFontSize, weight: .regular) + } else { + textView.font = UIFont(name: "Courier", size: UIFont.systemFontSize) + } + textView.delegate = self + + view.addSubview(textView) + + NSLayoutConstraint.activate([ + textView.topAnchor.constraint(equalTo: view.topAnchor), + textView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + textView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + textView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + private func startStreamer() { + self.streamer.start { [weak self] (str) in + guard let self = self else { return } + + DispatchQueue.main.async { + // Try parsing the entry + let entry = self.logEntryParser.parse(str) + + // Since the log streamer sends the log file line-by-line, it's possible that only a + // part of a multiline message is captured at first. + let message = entry.map { (entry) -> String in + // Reformat the log entry date + let timestamp = self.timestampFormatter.string(from: entry.timestamp) + + return "\(timestamp) \(entry.module) \(entry.message)\n" + } ?? "\(str)\n" + + + // Compute the range for replacing the text color + let start = self.textView.text.utf16.count + let end = start + message.utf16.count + let textRange = NSRange(start..<end) + + self.textView.insertText(message) + self.handleAutoScroll() + + // Update the current log entry color + if let logLevel = entry?.level { + self.currentTextColor = self.textColor(for: logLevel) + } + + // Apply the color attribute to the inserted text + if let textColor = self.currentTextColor { + self.textView.textStorage.addAttributes([.foregroundColor: textColor], range: textRange) + } + } + } + } + + private func handleAutoScroll() { + if autoScroll && !textView.isTracking && (!textView.isDragging || textView.isDecelerating) { + scrollToBottom() + } + } + + private func scrollToBottom() { + let textRange = NSRange(..<textView.text.endIndex, in: textView.text) + + textView.scrollRangeToVisible(textRange) + } + + private func updateAutoScrollBarItem() { + navigationItem.leftBarButtonItem = autoScrollButtonItem + } + + private func textColor(for logLevel: Logger.Level) -> UIColor { + switch logLevel { + case .debug, .trace: + return .lightGray + case .error, .critical: + return .red + case .info, .notice: + return .blue + case .warning: + return .orange + } + } + + // MARK: - Actions + + @objc func handleDismissButton(_ sender: Any) { + dismiss(animated: true) + } + + @objc func handleToggleAutoscroll(_ sender: Any) { + autoScroll = !autoScroll + } +} + +#endif diff --git a/ios/MullvadVPN/Logging/CustomFormatLogHandler.swift b/ios/MullvadVPN/Logging/CustomFormatLogHandler.swift index e8c93a7c42..340f83d8ae 100644 --- a/ios/MullvadVPN/Logging/CustomFormatLogHandler.swift +++ b/ios/MullvadVPN/Logging/CustomFormatLogHandler.swift @@ -16,6 +16,14 @@ struct CustomFormatLogHandler: LogHandler { private let label: String private let streams: [TextOutputStream] + private let dateFormatter = Self.makeDateFormatter() + + static func makeDateFormatter() -> DateFormatter { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss.SSS" + return dateFormatter + } + init(label: String, streams: [TextOutputStream]) { self.label = label self.streams = streams @@ -43,7 +51,8 @@ struct CustomFormatLogHandler: LogHandler { } let prettyMetadata = Self.formatMetadata(mergedMetadata) let metadataOutput = prettyMetadata.isEmpty ? "" : " \(prettyMetadata)" - let formattedMessage = "[\(Self.timestamp())][\(self.label)][\(level)]\(metadataOutput) \(message)\n" + let timestamp = dateFormatter.string(from: Date()) + let formattedMessage = "[\(timestamp)][\(self.label)][\(level)]\(metadataOutput) \(message)\n" for var stream in streams { stream.write(formattedMessage) @@ -53,16 +62,4 @@ struct CustomFormatLogHandler: LogHandler { private static func formatMetadata(_ metadata: Logger.Metadata) -> String { return metadata.map { "\($0)=\($1)" }.joined(separator: " ") } - - private static func timestamp() -> String { - var buffer = [Int8](repeating: 0, count: 255) - var timestamp = time(nil) - let localTime = localtime(×tamp) - strftime(&buffer, buffer.count, "%Y-%m-%dT%H:%M:%S%z", localTime) - return buffer.withUnsafeBufferPointer { - $0.withMemoryRebound(to: CChar.self) { - String(cString: $0.baseAddress!) - } - } - } } diff --git a/ios/MullvadVPN/Logging/LogEntryParser.swift b/ios/MullvadVPN/Logging/LogEntryParser.swift new file mode 100644 index 0000000000..c7b3a68db7 --- /dev/null +++ b/ios/MullvadVPN/Logging/LogEntryParser.swift @@ -0,0 +1,98 @@ +// +// LogEntryParser.swift +// MullvadVPN +// +// Created by pronebird on 18/08/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +#if DEBUG + +import Foundation +import Logging + +struct ParsedLogEntry { + let timestamp: Date + let level: Logger.Level + let module: String + let message: String +} + +class LogEntryParser { + /// Date formatter used for decoding the timestamp + private let dateFormatter = CustomFormatLogHandler.makeDateFormatter() + + /// Parse a log entry in the following format: + /// [<DATE>][<MODULE>][<LOG_LEVEL>] <MESSAGE> + func parse(_ str: String) -> ParsedLogEntry? { + let ranges = Self.stringRangesWithinSquareBrackets(string: str, maxResults: 3) + guard ranges.count == 3 else { + return nil + } + + let strings = ranges.map { String(str[$0]) } + + guard let timestamp = dateFormatter.date(from: strings[0]), + let logLevel = Logger.Level(rawValue: strings[2]) else { + return nil + } + + // Extract the log message following the log level + let startIndex = str.index(ranges.last!.upperBound, offsetBy: 1, limitedBy: str.endIndex) + let message = startIndex.map({ (startIndex) -> String in + return str[startIndex..<str.endIndex].trimmingCharacters(in: .whitespaces) + }) ?? "" + + return ParsedLogEntry( + timestamp: timestamp, + level: logLevel, + module: strings[1], + message: message + ) + } + + /// Find consecutive ranges of strings within square brackets. + private static func stringRangesWithinSquareBrackets(string: String, maxResults: Int) -> [Range<String.Index>] { + var results = [Range<String.Index>]() + var maybeStartIndex: String.Index? + + guard maxResults > 0 else { return results } + + loop: for (offset, char) in string.enumerated() { + switch char { + case "[": + if maybeStartIndex == nil { + maybeStartIndex = string.index(string.startIndex, offsetBy: offset + 1, limitedBy: string.endIndex) + } else { + // out of order + break loop + } + + case "]": + if let startIndex = maybeStartIndex { + maybeStartIndex = nil + + let endIndex = string.index(string.startIndex, offsetBy: offset) + + results.append((startIndex..<endIndex)) + + if results.count >= maxResults { + // done + break loop + } + } else { + // out of order + break loop + } + + default: + continue + } + } + + return results + } + +} + +#endif diff --git a/ios/MullvadVPN/Logging/LogStreamer.swift b/ios/MullvadVPN/Logging/LogStreamer.swift new file mode 100644 index 0000000000..23da9fae31 --- /dev/null +++ b/ios/MullvadVPN/Logging/LogStreamer.swift @@ -0,0 +1,141 @@ +// +// LogStreamer.swift +// MullvadVPN +// +// Created by pronebird on 10/08/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +#if DEBUG + +import Foundation + +private let kLogPollIntervalSeconds = 2 + +/// A class that consolidates multiple log streams into one +class LogStreamer<Codec> where Codec: UnicodeCodec { + private let fileURLs: [URL] + private var pendingFileURLs: [URL] + private var streams = [TextFileStream<Codec>]() + private var eventSources = [DispatchSourceFileSystemObject]() + private let queue = DispatchQueue(label: "net.mullvad.MullvadVPN.LogStreamer<\(Codec.self)>") + private var retry: DispatchWorkItem? + private var handlerBlock: ((String) -> Void)? + private var isStarted = false + + init(fileURLs: [URL]) { + self.fileURLs = fileURLs + self.pendingFileURLs = fileURLs + } + + deinit { + cancelAndRemoveAllEventSources() + } + + func start(handler: @escaping (String) -> Void) { + queue.async { + guard !self.isStarted else { return } + + self.isStarted = true + self.handlerBlock = handler + self.poll() + } + } + + func stop() { + queue.async { + guard self.isStarted else { return } + + self.isStarted = false + + self.retry?.cancel() + self.handlerBlock = nil + + self.cancelAndRemoveAllEventSources() + self.streams.removeAll() + self.pendingFileURLs = self.fileURLs + } + } + + private func openRemainingStreams() -> Bool { + var failedURLs = [URL]() + for fileURL in pendingFileURLs { + if let stream = TextFileStream<Codec>(fileURL: fileURL, separator: "\n") { + streams.append(stream) + + stream.read { [weak self] (s) in + guard let self = self else { return } + + self.queue.async { + self.handlerBlock?(s) + } + } + + addFileWatch(fileURL: fileURL, stream: stream) + } else { + failedURLs.append(fileURL) + } + } + + pendingFileURLs = failedURLs + + return failedURLs.isEmpty + } + + private func poll() { + if !self.openRemainingStreams() { + self.scheduleRetry() + } + } + + private func scheduleRetry() { + let workItem = DispatchWorkItem(block: { [weak self] in + guard let self = self, self.isStarted else { return } + + self.poll() + }) + queue.asyncAfter(wallDeadline: .now() + .seconds(kLogPollIntervalSeconds), execute: workItem) + retry = workItem + } + + /// Watch file renames and re-add the stream once that happens + private func addFileWatch(fileURL: URL, stream: TextFileStream<Codec>) { + let source = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: stream.fileDescriptor, + eventMask: .rename, + queue: queue + ) + + source.setEventHandler { [weak self, weak source] in + guard let self = self, let source = source, self.isStarted else { return } + + // Cancel current event source + source.cancel() + + // Release the stream + self.streams.removeAll { (s) -> Bool in + return stream === s + } + + // Release the current event source + self.eventSources.removeAll { (s) -> Bool in + return source === s + } + + // Add the file URL to backlog & start polling + self.pendingFileURLs.append(fileURL) + self.poll() + } + + source.activate() + + eventSources.append(source) + } + + private func cancelAndRemoveAllEventSources() { + eventSources.forEach { $0.cancel() } + eventSources.removeAll() + } +} + +#endif diff --git a/ios/MullvadVPN/Logging/StringStreamIterator.swift b/ios/MullvadVPN/Logging/StringStreamIterator.swift new file mode 100644 index 0000000000..c13b921c33 --- /dev/null +++ b/ios/MullvadVPN/Logging/StringStreamIterator.swift @@ -0,0 +1,58 @@ +// +// StringStreamIterator.swift +// MullvadVPN +// +// Created by pronebird on 17/08/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +#if DEBUG + +import Foundation + +class StringStreamIterator<Codec>: IteratorProtocol where Codec: UnicodeCodec { + let separator: Character + + private var string = "" + private var data = [Codec.CodeUnit]() + private var parser = Codec.ForwardParser() + + init(separator: Character) { + self.separator = separator + } + + func append<S>(bytes: S) where S: Sequence, S.Element == Codec.CodeUnit { + data.append(contentsOf: bytes) + } + + func next() -> String? { + var dataIterator = data.makeIterator() + var bytesRead = 0 + + defer { + if bytesRead > 0 { + data.removeSubrange(..<bytesRead) + } + } + + while case .valid(let encodedScalar) = parser.parseScalar(from: &dataIterator) { + let unicodeScalar = Codec.decode(encodedScalar) + let character = Character(unicodeScalar) + + bytesRead += encodedScalar.count + + if character == separator { + let returnString = string + string = "" + + return returnString + } else { + string.append(character) + } + } + + return nil + } +} + +#endif diff --git a/ios/MullvadVPN/Logging/TextFileStream.swift b/ios/MullvadVPN/Logging/TextFileStream.swift new file mode 100644 index 0000000000..de1aad6d91 --- /dev/null +++ b/ios/MullvadVPN/Logging/TextFileStream.swift @@ -0,0 +1,77 @@ +// +// TextFileStream.swift +// MullvadVPN +// +// Created by pronebird on 05/08/2020. +// Copyright © 2020 Mullvad VPN AB. All rights reserved. +// + +#if DEBUG + +import Foundation +import Darwin + +class TextFileStream<Codec> where Codec: UnicodeCodec { + let fileDescriptor: Int32 + + private let readSource: DispatchSourceRead + private let queue = DispatchQueue(label: "net.mullvad.MullvadVPN.TextFileStream<\(Codec.self)>") + private let stringStream: StringStreamIterator<Codec> + + init?(fileURL: URL, separator: Character) { + let filePath = fileURL.path.utf8CString.map { $0 } + + let fileDescriptor = open(filePath, O_RDONLY) + if (fileDescriptor == -1) { + return nil + } + + // Avoid blocking the read operation + _ = fcntl(fileDescriptor, F_SETFL, O_NONBLOCK); + + let readSource = DispatchSource.makeReadSource(fileDescriptor: fileDescriptor, queue: queue) + readSource.setCancelHandler { + close(fileDescriptor) + } + + stringStream = StringStreamIterator(separator: separator) + + self.readSource = readSource + self.fileDescriptor = fileDescriptor + } + + deinit { + readSource.cancel() + } + + func read(_ handler: @escaping (String) -> Void) { + readSource.setEventHandler { [weak self] in + guard let self = self else { return } + + let estimated = Int(self.readSource.data + 1) + var buffer = [Codec.CodeUnit](repeating: 0, count: estimated) + let actual = Darwin.read(self.fileDescriptor, &buffer, estimated) + + if actual == -1 { + print("TextFileStream<\(Codec.self)>: read error: \(errno)") + } + + if actual > 0 { + let bytes = buffer[..<actual] + self.stringStream.append(bytes: bytes) + + while let s = self.stringStream.next() { + handler(s) + } + } + } + readSource.activate() + } + + func cancel() { + readSource.cancel() + } + +} + +#endif diff --git a/ios/MullvadVPN/SettingsViewController.swift b/ios/MullvadVPN/SettingsViewController.swift index bee3d3a458..6991202afc 100644 --- a/ios/MullvadVPN/SettingsViewController.swift +++ b/ios/MullvadVPN/SettingsViewController.swift @@ -21,6 +21,7 @@ class SettingsViewController: UITableViewController { case account = "Account" case appVersion = "AppVersion" case basicDisclosure = "BasicDisclosure" + case basic = "Basic" } private weak var accountRow: StaticTableViewRow? @@ -99,6 +100,23 @@ class SettingsViewController: UITableViewController { middleSection.addRows([versionRow]) staticDataSource.addSections([middleSection]) + + #if DEBUG + let logStreamerRow = StaticTableViewRow(reuseIdentifier: CellIdentifier.basic.rawValue) { (_, cell) in + let cell = cell as! SettingsBasicCell + + cell.titleLabel.text = NSLocalizedString("App logs", comment: "") + } + logStreamerRow.actionBlock = { [weak self] (indexPath) in + let logController = LogStreamerViewController(fileURLs: ApplicationConfiguration.logFileURLs) + let navController = UINavigationController(rootViewController: logController) + + navController.modalPresentationStyle = .fullScreen + + self?.present(navController, animated: true) + } + middleSection.addRows([logStreamerRow]) + #endif } } diff --git a/ios/PacketTunnel/WireguardDevice.swift b/ios/PacketTunnel/WireguardDevice.swift index 71f8fe2e80..c24cd3dab0 100644 --- a/ios/PacketTunnel/WireguardDevice.swift +++ b/ios/PacketTunnel/WireguardDevice.swift @@ -101,7 +101,8 @@ class WireguardDevice { } wgSetLogger { (level, messagePtr) in - guard let message = messagePtr.map({ String(cString: $0) }) else { return } + guard let message = messagePtr.map({ String(cString: $0) })? + .trimmingCharacters(in: .newlines) else { return } let logLevel = WireguardLogLevel(rawValue: level) ?? .debug WireguardDevice.loggingQueue.async { |
