]> git.ipfire.org Git - thirdparty/wireguard-apple.git/commitdiff
macOS: Rafactor by introducing a TunnelsTracker
authorRoopesh Chander <roop@roopc.net>
Thu, 17 Jan 2019 19:57:17 +0000 (01:27 +0530)
committerRoopesh Chander <roop@roopc.net>
Thu, 17 Jan 2019 20:04:24 +0000 (01:34 +0530)
The TunnelTracker is now the central place to track what the current
tunnel is, and for keeping track of the tunnel list.

Signed-off-by: Roopesh Chander <roop@roopc.net>
WireGuard/WireGuard.xcodeproj/project.pbxproj
WireGuard/WireGuard/UI/macOS/AppDelegate.swift
WireGuard/WireGuard/UI/macOS/ImportPanelPresenter.swift
WireGuard/WireGuard/UI/macOS/StatusMenu.swift
WireGuard/WireGuard/UI/macOS/TunnelsTracker.swift [new file with mode: 0644]

index b6b1c32bda8ba85362673ce4a0579a339e2ca378..ae18c66f942680bf42784808b5abe3300d0e01a0 100644 (file)
@@ -55,6 +55,7 @@
                6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */; };
                6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; };
                6F89E17A21EDEB0E00C97BB9 /* StatusItemController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */; };
+               6F89E17C21F090CC00C97BB9 /* TunnelsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */; };
                6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */; };
                6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */; };
                6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; };
                6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = "<group>"; };
                6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrors.swift; sourceTree = "<group>"; };
                6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemController.swift; sourceTree = "<group>"; };
+               6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsTracker.swift; sourceTree = "<group>"; };
                6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; };
                6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = "<group>"; };
                6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; };
                                6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */,
                                6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */,
                                6FBA101621D655340051C35F /* StatusMenu.swift */,
+                               6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */,
                                6FBA104121D6BC210051C35F /* ErrorPresenter.swift */,
                                6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */,
                                6FB1BD6121D2607E00A991BF /* Assets.xcassets */,
                                6F4DD16C21DA558F00690EAE /* NSTableView+Reuse.swift in Sources */,
                                6FB1BDD821D50F5300A991BF /* WireGuardResult.swift in Sources */,
                                6FB1BDD921D50F5300A991BF /* LocalizationHelper.swift in Sources */,
+                               6F89E17C21F090CC00C97BB9 /* TunnelsTracker.swift in Sources */,
                                6FCD99B121E0EDA900BA4C82 /* TunnelEditViewController.swift in Sources */,
                                6FB1BDCA21D50F1700A991BF /* x25519.c in Sources */,
                                6FB1BDCB21D50F1700A991BF /* Curve25519.swift in Sources */,
index 3f089875c9c13d2e00f4fa586830f7f16103d407..95c1aa6a2a326804fb9ef6e355e9d3a499ccdd57 100644 (file)
@@ -6,8 +6,12 @@ import Cocoa
 @NSApplicationMain
 class AppDelegate: NSObject, NSApplicationDelegate {
 
+    var tunnelsManager: TunnelsManager?
+    var tunnelsTracker: TunnelsTracker?
     var statusItemController: StatusItemController?
-    var currentTunnelStatusObserver: AnyObject?
+
+    var manageTunnelsRootVC: ManageTunnelsRootViewController?
+    var manageTunnelsWindowObject: NSWindow?
 
     func applicationDidFinishLaunching(_ aNotification: Notification) {
         Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path)
@@ -20,19 +24,35 @@ class AppDelegate: NSObject, NSApplicationDelegate {
             }
 
             let tunnelsManager: TunnelsManager = result.value!
-            let statusItemController = StatusItemController()
 
             let statusMenu = StatusMenu(tunnelsManager: tunnelsManager)
+            statusMenu.windowDelegate = self
 
+            let statusItemController = StatusItemController()
             statusItemController.statusItem.menu = statusMenu
-            statusItemController.currentTunnel = statusMenu.currentTunnel
-            self.currentTunnelStatusObserver = statusMenu.observe(\.currentTunnel) { statusMenu, _ in
-                statusItemController.currentTunnel = statusMenu.currentTunnel
-            }
+
+            let tunnelsTracker = TunnelsTracker(tunnelsManager: tunnelsManager)
+            tunnelsTracker.statusMenu = statusMenu
+            tunnelsTracker.statusItemController = statusItemController
+
+            self.tunnelsManager = tunnelsManager
+            self.tunnelsTracker = tunnelsTracker
             self.statusItemController = statusItemController
+        }
+    }
+}
 
-            tunnelsManager.tunnelsListDelegate = statusMenu
-            tunnelsManager.activationDelegate = statusMenu
+extension AppDelegate: StatusMenuWindowDelegate {
+    func manageTunnelsWindow() -> NSWindow {
+        if manageTunnelsWindowObject == nil {
+            manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager!)
+            let window = NSWindow(contentViewController: manageTunnelsRootVC!)
+            window.title = tr("macWindowTitleManageTunnels")
+            window.setContentSize(NSSize(width: 800, height: 480))
+            window.setFrameAutosaveName(NSWindow.FrameAutosaveName("ManageTunnelsWindow")) // Auto-save window position and size
+            manageTunnelsWindowObject = window
+            tunnelsTracker?.manageTunnelsRootVC = manageTunnelsRootVC
         }
+        return manageTunnelsWindowObject!
     }
 }
index 03b1be7a2703428878ff225c58594ae4a8c6c41a..95fc46ae7e15a859c2f531d998aea42c3d2d881d 100644 (file)
@@ -4,8 +4,8 @@
 import Cocoa
 
 class ImportPanelPresenter {
-    static func presentImportPanel(tunnelsManager: TunnelsManager, sourceVC: NSViewController) {
-        guard let window = sourceVC.view.window else { return }
+    static func presentImportPanel(tunnelsManager: TunnelsManager, sourceVC: NSViewController?) {
+        guard let window = sourceVC?.view.window else { return }
         let openPanel = NSOpenPanel()
         openPanel.prompt = tr("macSheetButtonImport")
         openPanel.allowedFileTypes = ["conf", "zip"]
index 634ea9f7fc659e65e5443c29e4ab838db7db103b..2ae3013ccd44a13a71e532eda9a3ec0c041b4614 100644 (file)
@@ -3,27 +3,25 @@
 
 import Cocoa
 
+protocol StatusMenuWindowDelegate: class {
+    func manageTunnelsWindow() -> NSWindow
+}
+
 class StatusMenu: NSMenu {
 
     let tunnelsManager: TunnelsManager
-    var tunnelStatusObservers = [AnyObject]()
 
     var statusMenuItem: NSMenuItem?
     var networksMenuItem: NSMenuItem?
     var firstTunnelMenuItemIndex = 0
     var numberOfTunnelMenuItems = 0
 
-    @objc dynamic var currentTunnel: TunnelContainer?
-
-    var manageTunnelsRootVC: ManageTunnelsRootViewController?
-    lazy var manageTunnelsWindow: NSWindow = {
-        manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager)
-        let window = NSWindow(contentViewController: manageTunnelsRootVC!)
-        window.title = tr("macWindowTitleManageTunnels")
-        window.setContentSize(NSSize(width: 800, height: 480))
-        window.setFrameAutosaveName(NSWindow.FrameAutosaveName("ManageTunnelsWindow")) // Auto-save window position and size
-        return window
-    }()
+    var currentTunnel: TunnelContainer? {
+        didSet {
+            updateStatusMenuItems(with: currentTunnel)
+        }
+    }
+    weak var windowDelegate: StatusMenuWindowDelegate?
 
     init(tunnelsManager: TunnelsManager) {
         self.tunnelsManager = tunnelsManager
@@ -31,16 +29,6 @@ class StatusMenu: NSMenu {
 
         addStatusMenuItems()
         addItem(NSMenuItem.separator())
-        for index in 0 ..< tunnelsManager.numberOfTunnels() {
-            let tunnel = tunnelsManager.tunnel(at: index)
-            if tunnel.status != .inactive {
-                currentTunnel = tunnel
-            }
-            let isUpdated = updateStatusMenuItems(with: tunnel, ignoreInactive: true)
-            if isUpdated {
-                break
-            }
-        }
 
         firstTunnelMenuItemIndex = numberOfItems
         let isAdded = addTunnelMenuItems()
@@ -69,19 +57,21 @@ class StatusMenu: NSMenu {
         self.networksMenuItem = networksMenuItem
     }
 
-    @discardableResult
     //swiftlint:disable:next cyclomatic_complexity
-    func updateStatusMenuItems(with tunnel: TunnelContainer, ignoreInactive: Bool) -> Bool {
-        guard let statusMenuItem = statusMenuItem, let networksMenuItem = networksMenuItem else { return false }
+    func updateStatusMenuItems(with tunnel: TunnelContainer?) {
+        guard let statusMenuItem = statusMenuItem, let networksMenuItem = networksMenuItem else { return }
+        guard let tunnel = tunnel else {
+            statusMenuItem.title = tr(format: "macStatus (%@)", tr("tunnelStatusInactive"))
+            networksMenuItem.title = ""
+            networksMenuItem.isHidden = true
+            return
+        }
         var statusText: String
 
         switch tunnel.status {
         case .waiting:
-            return false
+            statusText = tr("tunnelStatusWaiting")
         case .inactive:
-            if ignoreInactive {
-                return false
-            }
             statusText = tr("tunnelStatusInactive")
         case .activating:
             statusText = tr("tunnelStatusActivating")
@@ -98,7 +88,7 @@ class StatusMenu: NSMenu {
         statusMenuItem.title = tr(format: "macStatus (%@)", statusText)
 
         if tunnel.status == .inactive {
-            networksMenuItem.title = tr("macMenuNetworksInactive")
+            networksMenuItem.title = ""
             networksMenuItem.isHidden = true
         } else {
             let allowedIPs = tunnel.tunnelConfiguration?.peers.flatMap { $0.allowedIPs }.map { $0.stringRepresentation }.joined(separator: ", ") ?? ""
@@ -109,7 +99,6 @@ class StatusMenu: NSMenu {
             }
             networksMenuItem.isHidden = false
         }
-        return true
     }
 
     func addTunnelMenuItems() -> Bool {
@@ -140,24 +129,25 @@ class StatusMenu: NSMenu {
     }
 
     @objc func tunnelClicked(sender: AnyObject) {
-        guard let tunnelMenuItem = sender as? NSMenuItem else { return }
-        guard let tunnel = tunnelMenuItem.representedObject as? TunnelContainer else { return }
+        guard let tunnelMenuItem = sender as? TunnelMenuItem else { return }
         if tunnelMenuItem.state == .off {
-            tunnelsManager.startActivation(of: tunnel)
+            tunnelsManager.startActivation(of: tunnelMenuItem.tunnel)
         } else {
-            tunnelsManager.startDeactivation(of: tunnel)
+            tunnelsManager.startDeactivation(of: tunnelMenuItem.tunnel)
         }
     }
 
     @objc func manageTunnelsClicked() {
         NSApp.activate(ignoringOtherApps: true)
+        guard let manageTunnelsWindow = windowDelegate?.manageTunnelsWindow() else { return }
         manageTunnelsWindow.makeKeyAndOrderFront(self)
     }
 
     @objc func importTunnelsClicked() {
         NSApp.activate(ignoringOtherApps: true)
+        guard let manageTunnelsWindow = windowDelegate?.manageTunnelsWindow() else { return }
         manageTunnelsWindow.makeKeyAndOrderFront(self)
-        ImportPanelPresenter.presentImportPanel(tunnelsManager: tunnelsManager, sourceVC: manageTunnelsRootVC!)
+        ImportPanelPresenter.presentImportPanel(tunnelsManager: tunnelsManager, sourceVC: manageTunnelsWindow.contentViewController)
     }
 
     @objc func aboutClicked() {
@@ -179,22 +169,8 @@ class StatusMenu: NSMenu {
 
 extension StatusMenu {
     func insertTunnelMenuItem(for tunnel: TunnelContainer, at tunnelIndex: Int) {
-        let menuItem = NSMenuItem(title: tunnel.name, action: #selector(tunnelClicked(sender:)), keyEquivalent: "")
+        let menuItem = TunnelMenuItem(tunnel: tunnel, action: #selector(tunnelClicked(sender:)))
         menuItem.target = self
-        menuItem.representedObject = tunnel
-        updateTunnelMenuItem(menuItem)
-        let statusObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in
-            updateTunnelMenuItem(menuItem)
-            if tunnel.status == .deactivating || tunnel.status == .inactive {
-                if self?.currentTunnel == tunnel {
-                    self?.currentTunnel = self?.tunnelsManager.waitingTunnel()
-                }
-            } else {
-                self?.currentTunnel = tunnel
-            }
-            self?.updateStatusMenuItems(with: tunnel, ignoreInactive: false)
-        }
-        tunnelStatusObservers.insert(statusObservationToken, at: tunnelIndex)
         insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex)
         if numberOfTunnelMenuItems == 0 {
             insertItem(NSMenuItem.separator(), at: firstTunnelMenuItemIndex + tunnelIndex + 1)
@@ -204,7 +180,6 @@ extension StatusMenu {
 
     func removeTunnelMenuItem(at tunnelIndex: Int) {
         removeItem(at: firstTunnelMenuItemIndex + tunnelIndex)
-        tunnelStatusObservers.remove(at: tunnelIndex)
         numberOfTunnelMenuItems -= 1
         if numberOfTunnelMenuItems == 0 {
             if let firstItem = item(at: firstTunnelMenuItemIndex), firstItem.isSeparatorItem {
@@ -214,73 +189,48 @@ extension StatusMenu {
     }
 
     func moveTunnelMenuItem(from oldTunnelIndex: Int, to newTunnelIndex: Int) {
-        let oldMenuItem = item(at: firstTunnelMenuItemIndex + oldTunnelIndex)!
-        let oldMenuItemTitle = oldMenuItem.title
-        let oldMenuItemTunnel = oldMenuItem.representedObject
+        guard let oldMenuItem = item(at: firstTunnelMenuItemIndex + oldTunnelIndex) as? TunnelMenuItem else { return }
+        let oldMenuItemTunnel = oldMenuItem.tunnel
         removeItem(at: firstTunnelMenuItemIndex + oldTunnelIndex)
-        let menuItem = NSMenuItem(title: oldMenuItemTitle, action: #selector(tunnelClicked(sender:)), keyEquivalent: "")
+        let menuItem = TunnelMenuItem(tunnel: oldMenuItemTunnel, action: #selector(tunnelClicked(sender:)))
         menuItem.target = self
-        menuItem.representedObject = oldMenuItemTunnel
         insertItem(menuItem, at: firstTunnelMenuItemIndex + newTunnelIndex)
-        let statusObserver = tunnelStatusObservers.remove(at: oldTunnelIndex)
-        tunnelStatusObservers.insert(statusObserver, at: newTunnelIndex)
-    }
-}
-
-private func updateTunnelMenuItem(_ tunnelMenuItem: NSMenuItem) {
-    guard let tunnel = tunnelMenuItem.representedObject as? TunnelContainer else { return }
-    tunnelMenuItem.title = tunnel.name
-    let shouldShowCheckmark = (tunnel.status != .inactive && tunnel.status != .deactivating)
-    tunnelMenuItem.state = shouldShowCheckmark ? .on : .off
-}
 
-extension StatusMenu: TunnelsManagerListDelegate {
-    func tunnelAdded(at index: Int) {
-        let tunnel = tunnelsManager.tunnel(at: index)
-        insertTunnelMenuItem(for: tunnel, at: index)
-        manageTunnelsRootVC?.tunnelsListVC?.tunnelAdded(at: index)
     }
+}
 
-    func tunnelModified(at index: Int) {
-        if let tunnelMenuItem = item(at: firstTunnelMenuItemIndex + index) {
-            updateTunnelMenuItem(tunnelMenuItem)
-        }
-        manageTunnelsRootVC?.tunnelsListVC?.tunnelModified(at: index)
-    }
+class TunnelMenuItem: NSMenuItem {
 
-    func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
-        moveTunnelMenuItem(from: oldIndex, to: newIndex)
-        manageTunnelsRootVC?.tunnelsListVC?.tunnelMoved(from: oldIndex, to: newIndex)
-    }
+    var tunnel: TunnelContainer
 
-    func tunnelRemoved(at index: Int) {
-        removeTunnelMenuItem(at: index)
-        manageTunnelsRootVC?.tunnelsListVC?.tunnelRemoved(at: index)
-    }
-}
+    private var statusObservationToken: AnyObject?
+    private var nameObservationToken: AnyObject?
 
-extension StatusMenu: TunnelsManagerActivationDelegate {
-    func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) {
-        if let manageTunnelsRootVC = manageTunnelsRootVC, manageTunnelsWindow.isVisible {
-            ErrorPresenter.showErrorAlert(error: error, from: manageTunnelsRootVC)
-        } else {
-            ErrorPresenter.showErrorAlert(error: error, from: nil)
+    init(tunnel: TunnelContainer, action selector: Selector?) {
+        self.tunnel = tunnel
+        super.init(title: tunnel.name, action: selector, keyEquivalent: "")
+        updateStatus()
+        let statusObservationToken = tunnel.observe(\.status) { [weak self] _, _ in
+            self?.updateStatus()
+        }
+        updateTitle()
+        let nameObservationToken = tunnel.observe(\TunnelContainer.name) { [weak self] _, _ in
+            self?.updateTitle()
         }
+        self.statusObservationToken = statusObservationToken
+        self.nameObservationToken = nameObservationToken
     }
 
-    func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) {
-        // Nothing to do
+    required init(coder decoder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
     }
 
-    func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) {
-        if let manageTunnelsRootVC = manageTunnelsRootVC, manageTunnelsWindow.isVisible {
-            ErrorPresenter.showErrorAlert(error: error, from: manageTunnelsRootVC)
-        } else {
-            ErrorPresenter.showErrorAlert(error: error, from: nil)
-        }
+    func updateTitle() {
+        title = tunnel.name
     }
 
-    func tunnelActivationSucceeded(tunnel: TunnelContainer) {
-        // Nothing to do
+    func updateStatus() {
+        let shouldShowCheckmark = (tunnel.status != .inactive && tunnel.status != .deactivating)
+        state = shouldShowCheckmark ? .on : .off
     }
 }
diff --git a/WireGuard/WireGuard/UI/macOS/TunnelsTracker.swift b/WireGuard/WireGuard/UI/macOS/TunnelsTracker.swift
new file mode 100644 (file)
index 0000000..781fa2e
--- /dev/null
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+// Keeps track of tunnels and informs the following objects of changes in tunnels:
+//   - Status menu
+//   - Status item controller
+//   - Tunnels list view controller in the Manage Tunnels window
+
+class TunnelsTracker {
+
+    weak var statusMenu: StatusMenu?
+    weak var statusItemController: StatusItemController?
+    weak var manageTunnelsRootVC: ManageTunnelsRootViewController?
+
+    private var tunnelsManager: TunnelsManager
+    private var tunnelStatusObservers = [AnyObject]()
+    private var currentTunnel: TunnelContainer? {
+        didSet {
+            statusMenu?.currentTunnel = currentTunnel
+            statusItemController?.currentTunnel = currentTunnel
+        }
+    }
+
+    init(tunnelsManager: TunnelsManager) {
+        self.tunnelsManager = tunnelsManager
+
+        if let waitingTunnel = tunnelsManager.waitingTunnel() {
+            currentTunnel = waitingTunnel
+        } else {
+            for index in 0 ..< tunnelsManager.numberOfTunnels() {
+                let tunnel = tunnelsManager.tunnel(at: index)
+                if tunnel.status != .inactive {
+                    currentTunnel = tunnel
+                    break
+                }
+            }
+        }
+
+        for index in 0 ..< tunnelsManager.numberOfTunnels() {
+            let tunnel = tunnelsManager.tunnel(at: index)
+            let statusObservationToken = observeStatus(of: tunnel)
+            tunnelStatusObservers.insert(statusObservationToken, at: index)
+        }
+
+        tunnelsManager.tunnelsListDelegate = self
+        tunnelsManager.activationDelegate = self
+    }
+
+    func observeStatus(of tunnel: TunnelContainer) -> AnyObject {
+        return tunnel.observe(\.status) { [weak self] tunnel, _ in
+            guard let self = self else { return }
+            if tunnel.status == .deactivating || tunnel.status == .inactive {
+                if self.currentTunnel == tunnel {
+                    if let waitingTunnel = self.tunnelsManager.waitingTunnel() {
+                        self.currentTunnel = waitingTunnel
+                    } else if tunnel.status == .inactive {
+                        self.currentTunnel = nil
+                    }
+                }
+            } else {
+                self.currentTunnel = tunnel
+            }
+        }
+    }
+}
+
+extension TunnelsTracker: TunnelsManagerListDelegate {
+    func tunnelAdded(at index: Int) {
+        let tunnel = tunnelsManager.tunnel(at: index)
+        if tunnel.status != .deactivating && tunnel.status != .inactive {
+            self.currentTunnel = tunnel
+        }
+        let statusObservationToken = observeStatus(of: tunnel)
+        tunnelStatusObservers.insert(statusObservationToken, at: index)
+
+        statusMenu?.insertTunnelMenuItem(for: tunnel, at: index)
+        manageTunnelsRootVC?.tunnelsListVC?.tunnelAdded(at: index)
+    }
+
+    func tunnelModified(at index: Int) {
+        manageTunnelsRootVC?.tunnelsListVC?.tunnelModified(at: index)
+    }
+
+    func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
+        let statusObserver = tunnelStatusObservers.remove(at: oldIndex)
+        tunnelStatusObservers.insert(statusObserver, at: newIndex)
+
+        statusMenu?.moveTunnelMenuItem(from: oldIndex, to: newIndex)
+        manageTunnelsRootVC?.tunnelsListVC?.tunnelMoved(from: oldIndex, to: newIndex)
+    }
+
+    func tunnelRemoved(at index: Int) {
+        tunnelStatusObservers.remove(at: index)
+
+        statusMenu?.removeTunnelMenuItem(at: index)
+        manageTunnelsRootVC?.tunnelsListVC?.tunnelRemoved(at: index)
+    }
+}
+
+extension TunnelsTracker: TunnelsManagerActivationDelegate {
+    func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) {
+        if let manageTunnelsRootVC = manageTunnelsRootVC, manageTunnelsRootVC.view.window?.isVisible ?? false {
+            ErrorPresenter.showErrorAlert(error: error, from: manageTunnelsRootVC)
+        } else {
+            ErrorPresenter.showErrorAlert(error: error, from: nil)
+        }
+    }
+
+    func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) {
+        // Nothing to do
+    }
+
+    func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) {
+        if let manageTunnelsRootVC = manageTunnelsRootVC, manageTunnelsRootVC.view.window?.isVisible ?? false {
+            ErrorPresenter.showErrorAlert(error: error, from: manageTunnelsRootVC)
+        } else {
+            ErrorPresenter.showErrorAlert(error: error, from: nil)
+        }
+    }
+
+    func tunnelActivationSucceeded(tunnel: TunnelContainer) {
+        // Nothing to do
+    }
+}