summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-08-19 10:10:42 +0200
committerAndrej Mihajlov <and@mullvad.net>2020-08-19 10:10:42 +0200
commitdd1689df906ae8f4782f25970aba56bd3e3bf9d5 (patch)
tree9395ff09302297ba0f0a115ab738d79af1ae4ef6
parent1ff1153eb12ac0607627d3d56ba4bfa69071c641 (diff)
parent18a02b392cff331d3d6f10a340500c343c6c2588 (diff)
downloadmullvadvpn-dd1689df906ae8f4782f25970aba56bd3e3bf9d5.tar.xz
mullvadvpn-dd1689df906ae8f4782f25970aba56bd3e3bf9d5.zip
Merge branch 'show-logs'
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj24
-rw-r--r--ios/MullvadVPN/ApplicationConfiguration.swift13
-rw-r--r--ios/MullvadVPN/Base.lproj/Main.storyboard36
-rw-r--r--ios/MullvadVPN/LogStreamerViewController.swift182
-rw-r--r--ios/MullvadVPN/Logging/CustomFormatLogHandler.swift23
-rw-r--r--ios/MullvadVPN/Logging/LogEntryParser.swift98
-rw-r--r--ios/MullvadVPN/Logging/LogStreamer.swift141
-rw-r--r--ios/MullvadVPN/Logging/StringStreamIterator.swift58
-rw-r--r--ios/MullvadVPN/Logging/TextFileStream.swift77
-rw-r--r--ios/MullvadVPN/SettingsViewController.swift18
-rw-r--r--ios/PacketTunnel/WireguardDevice.swift3
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(&timestamp)
- 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 {