diff --git a/launchers/macosx/I2PLauncher/routermgmt/NetworkUtil.swift b/launchers/macosx/I2PLauncher/routermgmt/NetworkUtil.swift new file mode 100644 index 000000000..0764b5113 --- /dev/null +++ b/launchers/macosx/I2PLauncher/routermgmt/NetworkUtil.swift @@ -0,0 +1,50 @@ +// +// NetworkUtil.swift +// I2PLauncher +// +// Created by Mikal Villa on 07/04/2019. +// Copyright © 2019 The I2P Project. All rights reserved. +// + +import Foundation + +class NetworkUtil { + static func checkTcpPortForListen(host: String = "127.0.0.1", port: in_port_t = 7657) -> (Bool, descr: String){ + + let socketFileDescriptor = socket(AF_INET, SOCK_STREAM, 0) + if socketFileDescriptor == -1 { + return (false, "SocketCreationFailed, \(descriptionOfLastError())") + } + + var addr = sockaddr_in() + let sizeOfSockkAddr = MemoryLayout.size + addr.sin_len = __uint8_t(sizeOfSockkAddr) + addr.sin_family = sa_family_t(AF_INET) + addr.sin_port = Int(OSHostByteOrder()) == OSLittleEndian ? _OSSwapInt16(port) : port + addr.sin_addr = in_addr(s_addr: inet_addr(host)) + addr.sin_zero = (0, 0, 0, 0, 0, 0, 0, 0) + var bind_addr = sockaddr() + memcpy(&bind_addr, &addr, Int(sizeOfSockkAddr)) + + if Darwin.bind(socketFileDescriptor, &bind_addr, socklen_t(sizeOfSockkAddr)) == -1 { + let details = descriptionOfLastError() + release(socket: socketFileDescriptor) + return (false, "\(port), BindFailed, \(details)") + } + if listen(socketFileDescriptor, SOMAXCONN ) == -1 { + let details = descriptionOfLastError() + release(socket: socketFileDescriptor) + return (false, "\(port), ListenFailed, \(details)") + } + release(socket: socketFileDescriptor) + return (true, "\(port) is free for use") + } + + static func release(socket: Int32) { + Darwin.shutdown(socket, SHUT_RDWR) + close(socket) + } + static func descriptionOfLastError() -> String { + return String(cString: UnsafePointer(strerror(errno))) + } +} diff --git a/launchers/macosx/I2PLauncher/routermgmt/RouterServices/HttpTunnelService.swift b/launchers/macosx/I2PLauncher/routermgmt/RouterServices/HttpTunnelService.swift new file mode 100644 index 000000000..f502c3dba --- /dev/null +++ b/launchers/macosx/I2PLauncher/routermgmt/RouterServices/HttpTunnelService.swift @@ -0,0 +1,42 @@ +// +// HttpTunnelService.swift +// I2PLauncher +// +// Created by Mikal Villa on 03/04/2019. +// Copyright © 2019 The I2P Project. All rights reserved. +// + +import Foundation +import Kanna + +class HttpTunnelService : Service { + + let dataURL: URL = URL(string: "http://127.0.0.1:7657/i2ptunnel/")! + + override func updateStatus(callback: @escaping (BaseService) -> Void) { + URLSession.shared.dataTask(with: dataURL) { [weak self] data, _, error in + guard let strongSelf = self else { return } + defer { callback(strongSelf) } + + guard let doc = try? HTML(html: data!, encoding: .utf8) else { return /*strongSelf._fail("Couldn't parse response")*/ } + + _ = doc.css("table#clientTunnels > tr.tunnelProperties > td.tunnelStatus").first + let maxStatus: ServiceStatus = .started + strongSelf.status = maxStatus + + switch maxStatus { + case .waiting: + strongSelf.message = "Waiting on router" + case .started: + strongSelf.message = "Started" + case .stopped: + strongSelf.message = "Stopped" + case .undetermined: + strongSelf.message = "Undetermined" + default: + strongSelf.message = "Undetermined" /*downComponents.map { $0["name"] as? String }.compactMap { $0 }.joined(separator: ", ")*/ + } + }.resume() + } + +} diff --git a/launchers/macosx/I2PLauncher/routermgmt/RouterServices/I2PRouterService.swift b/launchers/macosx/I2PLauncher/routermgmt/RouterServices/I2PRouterService.swift new file mode 100644 index 000000000..50eb997d3 --- /dev/null +++ b/launchers/macosx/I2PLauncher/routermgmt/RouterServices/I2PRouterService.swift @@ -0,0 +1,23 @@ +// +// I2PRouterService.swift +// I2PLauncher +// +// Created by Mikal Villa on 03/04/2019. +// Copyright © 2019 The I2P Project. All rights reserved. +// + +import Foundation + +class I2PRouterService : Service { + + override func updateStatus(callback: @escaping (BaseService) -> Void) { + //guard let strongSelf = self else { return } + defer { callback(self) } + DispatchQueue.main.async { + self.status = ServiceStatus(rawValue: 0)! + self.message = "Dead" + } + + } + +} diff --git a/launchers/macosx/I2PLauncher/routermgmt/RouterServices/IrcTunnelService.swift b/launchers/macosx/I2PLauncher/routermgmt/RouterServices/IrcTunnelService.swift new file mode 100644 index 000000000..e0dedc26e --- /dev/null +++ b/launchers/macosx/I2PLauncher/routermgmt/RouterServices/IrcTunnelService.swift @@ -0,0 +1,20 @@ +// +// IrcTunnelService.swift +// I2PLauncher +// +// Created by Mikal Villa on 03/04/2019. +// Copyright © 2019 The I2P Project. All rights reserved. +// + +import Foundation + +class IrcTunnelService : Service { + + override func updateStatus(callback: @escaping (BaseService) -> Void) { + defer { callback(self) } + + self.status = ServiceStatus(rawValue: 0)! + self.message = "Dead" + } + +} diff --git a/launchers/macosx/I2PLauncher/routermgmt/Service.swift b/launchers/macosx/I2PLauncher/routermgmt/Service.swift new file mode 100644 index 000000000..89af6e83e --- /dev/null +++ b/launchers/macosx/I2PLauncher/routermgmt/Service.swift @@ -0,0 +1,132 @@ +// +// Service.swift +// I2PLauncher +// +// Created by Mikal Villa on 12/04/2019. +// Copyright © 2019 The I2P Project. All rights reserved. +// + +import Foundation + +public enum ServiceStatus: Int, Comparable { + case undetermined + case waiting + case started + case notice + case killed + case crashed + case stopped + case restarting + + public static func < (lhs: ServiceStatus, rhs: ServiceStatus) -> Bool { + return lhs.rawValue < rhs.rawValue + } +} + +protocol ComparableStatus: Comparable { + var serviceStatus: ServiceStatus { get } +} + +extension ComparableStatus { + public static func < (lhs: Self, rhs: Self) -> Bool { + return lhs.serviceStatus < rhs.serviceStatus + } +} + +typealias Service = BaseService & RequiredServiceProperties + +protocol RequiredServiceProperties { + var name: String { get } + //var url: URL { get } +} + +extension RequiredServiceProperties { + // Default implementation of the property `name` is to return the class name + var name: String { return "\(type(of: self))" } +} + +public class BaseService { + public var status: ServiceStatus = .undetermined { + didSet { + if oldValue == .undetermined || status == .undetermined || oldValue == status { + self.shouldNotify = false + } else if Preferences.shared().notifyOnStatusChange { + self.shouldNotify = true + } + } + } + var message: String = "Loading…" + var shouldNotify = false + + public static func all() -> [BaseService] { + guard let servicesPlist = Bundle.main.path(forResource: "RouterServices", ofType: "plist"), + let services = NSDictionary(contentsOfFile: servicesPlist)?["services"] as? [String] else { + fatalError("The RouterServices.plist file does not exist. The build phase script might have failed.") + } + + return services.map(BaseService.named).compactMap { $0 } + } + + static func named(_ name: String) -> BaseService? { + return (NSClassFromString("I2PLauncher.\(name)") as? Service.Type)?.init() + } + + public required init() {} + + public func updateStatus(callback: @escaping (BaseService) -> Void) {} + + func _fail(_ error: Error?) { + self.status = .undetermined + self.message = error.debugDescription// ?? "Unexpected error" + } + + func _fail(_ message: String) { + self.status = .undetermined + self.message = message + } + + func notifyIfNecessary() { + guard let realSelf = self as? Service else { fatalError("BaseService should not be used directly.") } + + guard shouldNotify else { return } + + self.shouldNotify = false + + let notification = NSUserNotification() + let possessiveS = realSelf.name.hasSuffix("s") ? "'" : "'s" + notification.title = "\(realSelf.name)\(possessiveS) status has changed" + notification.informativeText = message + + NSUserNotificationCenter.default.deliver(notification) + } +} + +extension BaseService: Equatable { + public static func == (lhs: BaseService, rhs: BaseService) -> Bool { + guard + let lhs = lhs as? Service, + let rhs = rhs as? Service + else { + fatalError("BaseService should not be used directly.") + } + + return lhs.name == rhs.name + } +} + +extension BaseService: Comparable { + public static func < (lhs: BaseService, rhs: BaseService) -> Bool { + guard + let lhs = lhs as? Service, + let rhs = rhs as? Service + else { + fatalError("BaseService should not be used directly.") + } + + let sameStatus = lhs.status == rhs.status + let differentStatus = + lhs.status != .started && lhs.status != .notice + && rhs.status == .started || rhs.status == .notice + return ((lhs.name < rhs.name) && sameStatus) || differentStatus + } +}