Make SiteList.loadAll a static method

This commit is contained in:
Caleb Jasik 2025-02-11 17:28:30 -06:00
parent 8ff1da215c
commit 6564a18d3d
No known key found for this signature in database
3 changed files with 106 additions and 113 deletions

View file

@ -1,108 +1,103 @@
import NetworkExtension import NetworkExtension
class SiteList { class SiteList {
private var sites = [String: Site]()
/// Gets the root directory that can be used to share files between the UI and VPN process. Does ensure the directory exists /// Gets the root directory that can be used to share files between the UI and VPN process. Does ensure the directory exists
static func getRootDir() throws -> URL { static func getRootDir() throws -> URL {
let fileManager = FileManager.default let fileManager = FileManager.default
let rootDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")! let rootDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")!
if (!fileManager.fileExists(atPath: rootDir.absoluteString)) { if !fileManager.fileExists(atPath: rootDir.absoluteString) {
try fileManager.createDirectory(at: rootDir, withIntermediateDirectories: true) try fileManager.createDirectory(at: rootDir, withIntermediateDirectories: true)
} }
return rootDir return rootDir
} }
/// Gets the directory where all sites live, $rootDir/sites. Does ensure the directory exists /// Gets the directory where all sites live, $rootDir/sites. Does ensure the directory exists
static func getSitesDir() throws -> URL { static func getSitesDir() throws -> URL {
let fileManager = FileManager.default let fileManager = FileManager.default
let sitesDir = try getRootDir().appendingPathComponent("sites", isDirectory: true) let sitesDir = try getRootDir().appendingPathComponent("sites", isDirectory: true)
if (!fileManager.fileExists(atPath: sitesDir.absoluteString)) { if !fileManager.fileExists(atPath: sitesDir.absoluteString) {
try fileManager.createDirectory(at: sitesDir, withIntermediateDirectories: true) try fileManager.createDirectory(at: sitesDir, withIntermediateDirectories: true)
} }
return sitesDir return sitesDir
} }
/// Gets the directory where a single site would live, $rootDir/sites/$siteID /// Gets the directory where a single site would live, $rootDir/sites/$siteID
static func getSiteDir(id: String, create: Bool = false) throws -> URL { static func getSiteDir(id: String, create: Bool = false) throws -> URL {
let fileManager = FileManager.default let fileManager = FileManager.default
let siteDir = try getSitesDir().appendingPathComponent(id, isDirectory: true) let siteDir = try getSitesDir().appendingPathComponent(id, isDirectory: true)
if (create && !fileManager.fileExists(atPath: siteDir.absoluteString)) { if create && !fileManager.fileExists(atPath: siteDir.absoluteString) {
try fileManager.createDirectory(at: siteDir, withIntermediateDirectories: true) try fileManager.createDirectory(at: siteDir, withIntermediateDirectories: true)
} }
return siteDir return siteDir
} }
/// Gets the file that represents the site configuration, $rootDir/sites/$siteID/config.json /// Gets the file that represents the site configuration, $rootDir/sites/$siteID/config.json
static func getSiteConfigFile(id: String, createDir: Bool) throws -> URL { static func getSiteConfigFile(id: String, createDir: Bool) throws -> URL {
return try getSiteDir(id: id, create: createDir).appendingPathComponent("config", isDirectory: false).appendingPathExtension("json") return try getSiteDir(id: id, create: createDir).appendingPathComponent("config", isDirectory: false)
.appendingPathExtension("json")
} }
/// Gets the file that represents the site log output, $rootDir/sites/$siteID/log /// Gets the file that represents the site log output, $rootDir/sites/$siteID/log
static func getSiteLogFile(id: String, createDir: Bool) throws -> URL { static func getSiteLogFile(id: String, createDir: Bool) throws -> URL {
return try getSiteDir(id: id, create: createDir).appendingPathComponent("logs", isDirectory: false) return try getSiteDir(id: id, create: createDir).appendingPathComponent("logs", isDirectory: false)
} }
init(completion: @escaping ([String: Site]?, (any Error)?) -> ()) { static func loadAll(completion: @escaping ([String: Site]?, (any Error)?) -> Void) {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
SiteList.loadAllFromFS { sites, err in SiteList.loadAllFromFS { sites, err in
if sites != nil {
self.sites = sites! completion(sites, err)
} }
completion(sites, err) #else
} SiteList.loadAllFromNETPM { sites, err in
#else
SiteList.loadAllFromNETPM { sites, err in completion(sites, err)
if sites != nil {
self.sites = sites!
} }
completion(sites, err) #endif
}
#endif
} }
private static func loadAllFromFS(completion: @escaping ([String: Site]?, (any Error)?) -> ()) { private static func loadAllFromFS(completion: @escaping ([String: Site]?, (any Error)?) -> Void) {
let fileManager = FileManager.default let fileManager = FileManager.default
var siteDirs: [URL] var siteDirs: [URL]
var sites = [String: Site]() var sites = [String: Site]()
do { do {
siteDirs = try fileManager.contentsOfDirectory(at: getSitesDir(), includingPropertiesForKeys: nil) siteDirs = try fileManager.contentsOfDirectory(at: getSitesDir(), includingPropertiesForKeys: nil)
} catch { } catch {
completion(nil, error) completion(nil, error)
return return
} }
siteDirs.forEach { path in siteDirs.forEach { path in
do { do {
let site = try Site(path: path.appendingPathComponent("config").appendingPathExtension("json")) let site = try Site(path: path.appendingPathComponent("config").appendingPathExtension("json"))
sites[site.id] = site sites[site.id] = site
} catch { } catch {
print(error) print(error)
try? fileManager.removeItem(at: path) try? fileManager.removeItem(at: path)
print("Deleted non conforming site \(path)") print("Deleted non conforming site \(path)")
} }
} }
completion(sites, nil) completion(sites, nil)
} }
private static func loadAllFromNETPM(completion: @escaping ([String: Site]?, (any Error)?) -> ()) { private static func loadAllFromNETPM(completion: @escaping ([String: Site]?, (any Error)?) -> Void) {
var sites = [String: Site]() var sites = [String: Site]()
// dispatchGroup is used to ensure we have migrated all sites before returning them // dispatchGroup is used to ensure we have migrated all sites before returning them
// If there are no sites to migrate, there are never any entrants // If there are no sites to migrate, there are never any entrants
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
NETunnelProviderManager.loadAllFromPreferences() { newManagers, err in NETunnelProviderManager.loadAllFromPreferences { newManagers, err in
if (err != nil) { if err != nil {
return completion(nil, err) return completion(nil, err)
} }
newManagers?.forEach { manager in newManagers?.forEach { manager in
do { do {
let site = try Site(manager: manager) let site = try Site(manager: manager)
@ -112,14 +107,14 @@ class SiteList {
if error != nil { if error != nil {
print("Error while migrating site to fs: \(error!.localizedDescription)") print("Error while migrating site to fs: \(error!.localizedDescription)")
} }
print("Migrated site to fs: \(site.name)") print("Migrated site to fs: \(site.name)")
site.needsToMigrateToFS = false site.needsToMigrateToFS = false
dispatchGroup.leave() dispatchGroup.leave()
} }
} }
sites[site.id] = site sites[site.id] = site
} catch { } catch {
//TODO: notify the user about this //TODO: notify the user about this
print("Deleted non conforming site \(manager) \(error)") print("Deleted non conforming site \(manager) \(error)")
@ -127,14 +122,10 @@ class SiteList {
//TODO: delete from disk, we need to try and discover the site id though //TODO: delete from disk, we need to try and discover the site id though
} }
} }
dispatchGroup.notify(queue: .main) { dispatchGroup.notify(queue: .main) {
completion(sites, nil) completion(sites, nil)
} }
} }
} }
func getSites() -> [String: Site] {
return sites
}
} }

View file

@ -6,8 +6,8 @@ actor DNUpdater {
private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes
private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater") private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater")
func updateAll(onUpdate: @escaping (Site) -> Void) { private func updateAll(onUpdate: @escaping (Site) -> Void) {
_ = SiteList { (sites, _) -> Void in SiteList.loadAll(completion: { (sites, _) -> Void in
if let unwrappedSites = sites?.values { if let unwrappedSites = sites?.values {
// NEVPN seems to force us onto the main thread and we are about to make network calls that // NEVPN seems to force us onto the main thread and we are about to make network calls that
// could block for a while. Push ourselves onto another thread to avoid blocking the UI. // could block for a while. Push ourselves onto another thread to avoid blocking the UI.
@ -25,7 +25,7 @@ actor DNUpdater {
} }
} })
} }
func updateAllLoop(onUpdate: @escaping (Site) -> Void) { func updateAllLoop(onUpdate: @escaping (Site) -> Void) {

View file

@ -1,10 +1,10 @@
import NetworkExtension
import MobileNebula import MobileNebula
import NetworkExtension
class SiteContainer { class SiteContainer {
var site: Site var site: Site
var updater: SiteUpdater var updater: SiteUpdater
init(site: Site, updater: SiteUpdater) { init(site: Site, updater: SiteUpdater) {
self.site = site self.site = site
self.updater = updater self.updater = updater
@ -14,39 +14,39 @@ class SiteContainer {
class Sites { class Sites {
private var containers = [String: SiteContainer]() private var containers = [String: SiteContainer]()
private var messenger: (any FlutterBinaryMessenger)? private var messenger: (any FlutterBinaryMessenger)?
init(messenger: (any FlutterBinaryMessenger)?) { init(messenger: (any FlutterBinaryMessenger)?) {
self.messenger = messenger self.messenger = messenger
} }
func loadSites(completion: @escaping ([String: Site]?, (any Error)?) -> ()) { func loadSites(completion: @escaping ([String: Site]?, (any Error)?) -> Void) {
_ = SiteList { (sites, err) in SiteList.loadAll(completion: { (sites, err) in
if (err != nil) { if err != nil {
return completion(nil, err) return completion(nil, err)
} }
sites?.values.forEach{ site in sites?.values.forEach { site in
var updater = self.containers[site.id]?.updater var updater = self.containers[site.id]?.updater
if (updater != nil) { if updater != nil {
updater!.setSite(site: site) updater!.setSite(site: site)
} else { } else {
updater = SiteUpdater(messenger: self.messenger!, site: site) updater = SiteUpdater(messenger: self.messenger!, site: site)
} }
self.containers[site.id] = SiteContainer(site: site, updater: updater!) self.containers[site.id] = SiteContainer(site: site, updater: updater!)
} }
let justSites = self.containers.mapValues { let justSites = self.containers.mapValues {
return $0.site return $0.site
} }
completion(justSites, nil) completion(justSites, nil)
} })
} }
func deleteSite(id: String, callback: @escaping ((any Error)?) -> ()) { func deleteSite(id: String, callback: @escaping ((any Error)?) -> Void) {
if let site = self.containers.removeValue(forKey: id) { if let site = self.containers.removeValue(forKey: id) {
_ = KeyChain.delete(key: "\(site.site.id).dnCredentials") _ = KeyChain.delete(key: "\(site.site.id).dnCredentials")
_ = KeyChain.delete(key: "\(site.site.id).key") _ = KeyChain.delete(key: "\(site.site.id).key")
do { do {
let fileManager = FileManager.default let fileManager = FileManager.default
let siteDir = try SiteList.getSiteDir(id: site.site.id) let siteDir = try SiteList.getSiteDir(id: site.site.id)
@ -54,39 +54,39 @@ class Sites {
} catch { } catch {
print("Failed to delete site from fs: \(error.localizedDescription)") print("Failed to delete site from fs: \(error.localizedDescription)")
} }
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
site.site.manager!.removeFromPreferences(completionHandler: callback) site.site.manager!.removeFromPreferences(completionHandler: callback)
return return
#endif #endif
} }
// Nothing to remove // Nothing to remove
callback(nil) callback(nil)
} }
func getSite(id: String) -> Site? { func getSite(id: String) -> Site? {
return self.containers[id]?.site return self.containers[id]?.site
} }
func getUpdater(id: String) -> SiteUpdater? { func getUpdater(id: String) -> SiteUpdater? {
return self.containers[id]?.updater return self.containers[id]?.updater
} }
func getContainer(id: String) -> SiteContainer? { func getContainer(id: String) -> SiteContainer? {
return self.containers[id] return self.containers[id]
} }
} }
class SiteUpdater: NSObject, FlutterStreamHandler { class SiteUpdater: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?; private var eventSink: FlutterEventSink?
private var eventChannel: FlutterEventChannel; private var eventChannel: FlutterEventChannel
private var site: Site private var site: Site
private var notification: Any? private var notification: Any?
public var startFunc: (() -> Void)? public var startFunc: (() -> Void)?
private var configFd: Int32? = nil private var configFd: Int32? = nil
private var configObserver: (any DispatchSourceFileSystemObject)? = nil private var configObserver: (any DispatchSourceFileSystemObject)? = nil
init(messenger: any FlutterBinaryMessenger, site: Site) { init(messenger: any FlutterBinaryMessenger, site: Site) {
do { do {
let configPath = try SiteList.getSiteConfigFile(id: site.id, createDir: false) let configPath = try SiteList.getSiteConfigFile(id: site.id, createDir: false)
@ -95,18 +95,18 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
fileDescriptor: self.configFd!, fileDescriptor: self.configFd!,
eventMask: .write eventMask: .write
) )
} catch { } catch {
// SiteList.getSiteConfigFile should never throw because we are not creating it here // SiteList.getSiteConfigFile should never throw because we are not creating it here
self.configObserver = nil self.configObserver = nil
} }
eventChannel = FlutterEventChannel(name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger) eventChannel = FlutterEventChannel(name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger)
self.site = site self.site = site
super.init() super.init()
eventChannel.setStreamHandler(self) eventChannel.setStreamHandler(self)
self.configObserver?.setEventHandler(handler: self.configUpdated) self.configObserver?.setEventHandler(handler: self.configUpdated)
self.configObserver?.setCancelHandler { self.configObserver?.setCancelHandler {
if self.configFd != nil { if self.configFd != nil {
@ -114,72 +114,74 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
} }
self.configObserver = nil self.configObserver = nil
} }
self.configObserver?.resume() self.configObserver?.resume()
} }
func setSite(site: Site) { func setSite(site: Site) {
self.site = site self.site = site
} }
/// onListen is called when flutter code attaches an event listener /// onListen is called when flutter code attaches an event listener
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
eventSink = events; eventSink = events
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
if site.manager == nil { if site.manager == nil {
//TODO: The dn updater path seems to race to build a site that lacks a manager. The UI does not display this error //TODO: The dn updater path seems to race to build a site that lacks a manager. The UI does not display this error
// and a another listen should occur and succeed. // and a another listen should occur and succeed.
return FlutterError(code: "Internal Error", message: "Flutter manager was not present", details: nil) return FlutterError(code: "Internal Error", message: "Flutter manager was not present", details: nil)
}
self.notification = NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager!.connection , queue: nil) { n in
let oldConnected = self.site.connected
self.site.status = statusString[self.site.manager!.connection.status]
self.site.connected = statusMap[self.site.manager!.connection.status]
// Check to see if we just moved to connected and if we have a start function to call when that happens
if self.site.connected! && oldConnected != self.site.connected && self.startFunc != nil {
self.startFunc!()
self.startFunc = nil
} }
self.update(connected: self.site.connected!) self.notification = NotificationCenter.default.addObserver(
} forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager!.connection, queue: nil
#endif ) { n in
let oldConnected = self.site.connected
self.site.status = statusString[self.site.manager!.connection.status]
self.site.connected = statusMap[self.site.manager!.connection.status]
// Check to see if we just moved to connected and if we have a start function to call when that happens
if self.site.connected! && oldConnected != self.site.connected && self.startFunc != nil {
self.startFunc!()
self.startFunc = nil
}
self.update(connected: self.site.connected!)
}
#endif
return nil return nil
} }
/// onCancel is called when the flutter listener stops listening /// onCancel is called when the flutter listener stops listening
func onCancel(withArguments arguments: Any?) -> FlutterError? { func onCancel(withArguments arguments: Any?) -> FlutterError? {
if (self.notification != nil) { if self.notification != nil {
NotificationCenter.default.removeObserver(self.notification!) NotificationCenter.default.removeObserver(self.notification!)
} }
return nil return nil
} }
/// update is a way to send information to the flutter listener and generally should not be used directly /// update is a way to send information to the flutter listener and generally should not be used directly
func update(connected: Bool, replaceSite: Site? = nil) { func update(connected: Bool, replaceSite: Site? = nil) {
if (replaceSite != nil) { if replaceSite != nil {
site = replaceSite! site = replaceSite!
} }
site.connected = connected site.connected = connected
site.status = connected ? "Connected" : "Disconnected" site.status = connected ? "Connected" : "Disconnected"
let encoder = JSONEncoder() let encoder = JSONEncoder()
let data = try! encoder.encode(site) let data = try! encoder.encode(site)
self.eventSink?(String(data: data, encoding: .utf8)) self.eventSink?(String(data: data, encoding: .utf8))
} }
private func configUpdated() { private func configUpdated() {
if self.site.connected != true { if self.site.connected != true {
return return
} }
guard let newSite = try? Site(manager: self.site.manager!) else { guard let newSite = try? Site(manager: self.site.manager!) else {
return return
} }
self.update(connected: newSite.connected ?? false, replaceSite: newSite) self.update(connected: newSite.connected ?? false, replaceSite: newSite)
} }
} }