Skip to content

Commit

Permalink
FEAT: Create service for network browsing with Bonjour
Browse files Browse the repository at this point in the history
  • Loading branch information
josefdolezal committed May 10, 2017
1 parent a2dc868 commit 18af83e
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 0 deletions.
16 changes: 16 additions & 0 deletions OctoPhone.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@
D23F0CEE1E92C1EE00A70BC9 /* PrintProfileCellViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23F0CED1E92C1EE00A70BC9 /* PrintProfileCellViewModelTests.swift */; };
D248115C1EA69EFC00BA0A8B /* ClosePrinterCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D248115B1EA69EFC00BA0A8B /* ClosePrinterCellViewModel.swift */; };
D248115E1EA69F1200BA0A8B /* ClosePrinterCellCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D248115D1EA69F1200BA0A8B /* ClosePrinterCellCollectionViewCell.swift */; };
D24CCCB91EC3248C00BCDDD2 /* Bonjour.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24CCCB81EC3248C00BCDDD2 /* Bonjour.swift */; };
D24CCCBB1EC3251300BCDDD2 /* BonjourService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24CCCBA1EC3251300BCDDD2 /* BonjourService.swift */; };
D24EDCBE1E90037C00A8437B /* SlicingProfilesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24EDCBD1E90037C00A8437B /* SlicingProfilesCoordinator.swift */; };
D24EDCC01E90087900A8437B /* SlicingProfilesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24EDCBF1E90087900A8437B /* SlicingProfilesViewModel.swift */; };
D24EDCC21E90089300A8437B /* SlicingProfilesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24EDCC11E90089300A8437B /* SlicingProfilesViewController.swift */; };
Expand Down Expand Up @@ -367,6 +369,8 @@
D23F0CED1E92C1EE00A70BC9 /* PrintProfileCellViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrintProfileCellViewModelTests.swift; sourceTree = "<group>"; };
D248115B1EA69EFC00BA0A8B /* ClosePrinterCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosePrinterCellViewModel.swift; sourceTree = "<group>"; };
D248115D1EA69F1200BA0A8B /* ClosePrinterCellCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClosePrinterCellCollectionViewCell.swift; sourceTree = "<group>"; };
D24CCCB81EC3248C00BCDDD2 /* Bonjour.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bonjour.swift; sourceTree = "<group>"; };
D24CCCBA1EC3251300BCDDD2 /* BonjourService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BonjourService.swift; sourceTree = "<group>"; };
D24EDCBD1E90037C00A8437B /* SlicingProfilesCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlicingProfilesCoordinator.swift; sourceTree = "<group>"; };
D24EDCBF1E90087900A8437B /* SlicingProfilesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlicingProfilesViewModel.swift; sourceTree = "<group>"; };
D24EDCC11E90089300A8437B /* SlicingProfilesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlicingProfilesViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -561,6 +565,7 @@
D203DE751DDFCB59000DEAA0 /* Utils */ = {
isa = PBXGroup;
children = (
D24CCCB71EC3245E00BCDDD2 /* Bonjour */,
D2353E021E9AC72D000C3B5F /* Constants */,
D2132B5E1E99843E00FA3ACD /* Common UI Elements */,
D2C38CD21E8029A300D06698 /* Formatters */,
Expand Down Expand Up @@ -787,6 +792,15 @@
path = "Print Profiles";
sourceTree = "<group>";
};
D24CCCB71EC3245E00BCDDD2 /* Bonjour */ = {
isa = PBXGroup;
children = (
D24CCCB81EC3248C00BCDDD2 /* Bonjour.swift */,
D24CCCBA1EC3251300BCDDD2 /* BonjourService.swift */,
);
path = Bonjour;
sourceTree = "<group>";
};
D24EDCBC1E8FF8B500A8437B /* Slicing Profiles */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1499,6 +1513,7 @@
D2ABD3E21E9B8985003EDDAA /* OPFileSizeFormatter.swift in Sources */,
D23F0CDB1E919AD600A70BC9 /* PrintProfilesViewModel.swift in Sources */,
D224017C1E6B02F50082129A /* SignalProducer+JSONAble.swift in Sources */,
D24CCCB91EC3248C00BCDDD2 /* Bonjour.swift in Sources */,
D2C8AF3B1E9435EE00DC7914 /* FormTextInputView.swift in Sources */,
D20E41A31E88747900CE99BA /* LogDetailViewController.swift in Sources */,
D2D990C51E6F6ABD0094DA64 /* LogsViewController.swift in Sources */,
Expand Down Expand Up @@ -1607,6 +1622,7 @@
D22444051EB685670026CE68 /* ControlsViewModel.swift in Sources */,
D20755941EC1B0AD00A43F49 /* StaticProvider.swift in Sources */,
D22401811E6B11CC0082129A /* GCodeAnalysis+JSONAble.swift in Sources */,
D24CCCBB1EC3251300BCDDD2 /* BonjourService.swift in Sources */,
D2D990D51E71F3D40094DA64 /* TerminalViewModel.swift in Sources */,
D2D331401E67177100C9C005 /* PrinterLoginCoordinator.swift in Sources */,
D2AC35FB1EB3DCA7009373AE /* ControlsView.swift in Sources */,
Expand Down
205 changes: 205 additions & 0 deletions OctoPhone/Utils/Bonjour/Bonjour.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//
// Bonjour.swift
// OctoPhone
//
// Created by Josef Dolezal on 10/05/2017.
// Copyright © 2017 Josef Dolezal. All rights reserved.
//

import ReactiveSwift

/// Bonjour service browser wrapper. Searches for all available services in
/// given domain and resolves their IPs.
///
/// The service first find all services, once the system tells there are no other services,
/// the IPs are getting resolved one by one.
/// Each time the service gets resolved, the value is sent to producer.
///
/// Before IPs are resolved, the system first have to resolve service it's address.
/// Once the address is known, the IP is beeing resolved.
///
/// Note: Only one search may be done at the time. So if you start the new one,
/// the old one will be interrupted. Therefor the Bonjour is implemented as singleton.
///
/// Note: Only errors from system are send to signal, IP resolving errors are thrown away.
class Bonjour: NSObject {

/// Supported searchable domains
///
/// - local: Local domain
enum Domain: String {
case local = "local."
}

/// Available service types
///
/// - workstation: Workgroup
/// - all: Is not actual service but search for service groups
/// - httpTransfer: HTTP connections
/// - linePrinter: LPR printers
/// - internetPrinter: Internet printers on HTTP
/// - remoteUSBPrinter: Remote USB printers
enum ServiceType: String {
case workstation = "_workstation._tcp."
case all = "_services._dns-sd._udp."
case httpTransfer = "_http._tcp."
case linePrinter = "_printer._tcp."
case internetPrinter = "_ipp._tcp."
case remoteUSBPrinter = "_riousbprint._tcp."
}

/// The actual network browser
private let browser = NetServiceBrowser()

/// Services found on the network which are not resolved yet
fileprivate var foundServices = [NetService]()

/// Services which IPs were resolved successfully
fileprivate var resolvedServices = [BonjourService]()

/// Signal sink
fileprivate var sink: Observer<[BonjourService], NetService.ErrorCode>?

/// The singleton instance
private static let shared = Bonjour()

/// Starts new search for services of given type in domain.
/// If there is currently another searching running, it will be interrupted.
///
/// - Parameters:
/// - type: Type of service which will be searched
/// - domain: Domain where the services will be searched
/// - Returns: New signal sending collection of resolved services
static func searchForServices(ofType type: ServiceType = .workstation,
inDomain domain: Domain = .local) -> Signal<[BonjourService], NetService.ErrorCode> {

shared.stop()

let (signal, sink) = Signal<[BonjourService], NetService.ErrorCode>.pipe()

shared.sink = sink

shared.browser.delegate = shared
shared.browser.searchForServices(ofType: type.rawValue, inDomain: domain.rawValue)

return signal
}

/// Resolves next service from stack or complete the signal if stack is empty
fileprivate func resolveNext() {
// The service must not be popped or it's deallocated and not correctly resolved.
guard let service = foundServices.last else {
sink?.sendCompleted()
return
}

service.delegate = self
service.resolve(withTimeout: 2)
}

/// Resolves service IP address. The service must have resolved addresses by system
/// before IP can be resolved.
///
/// - Parameter service: Service to be resolved.
/// - Returns: BonjourService if service is resolved correctly, false otherwise.
fileprivate func resolveIP(for service: NetService) -> BonjourService? {
// Create empty hostname for C operations
var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))

// If address is not resolved correctly stop IP resolving
guard let address = service.addresses?.first else { return nil }

// If address can not be converted to IP, return nil
guard address.withUnsafeBytes({ (pointer: UnsafePointer<sockaddr>) in
return getnameinfo(pointer, socklen_t(address.count), &hostname, socklen_t(hostname.count),
nil, 0, NI_NUMERICHOST) == 0
}) else {
return nil
}

let ip = String(cString: hostname)

// The service was resolved successfully
return BonjourService(name: service.name, address: ip, port: "\(service.port)")
}

/// Unwraps system error or return .unknownError if it can not be unwrapped.
///
/// - Parameter errorDict: System error representation
/// - Returns: Error which occured or .unknownError
fileprivate func unwrapError(fromDictionary errorDict: [String: NSNumber]) -> NetService.ErrorCode {
guard
let code = errorDict.first,
let error = NetService.ErrorCode(rawValue: code.1.intValue)
else {
// Fallback when error could not be unwrapped
return .unknownError
}

return error
}

/// Stops all active services, free all resources
private func stop() {
sink?.sendInterrupted()
foundServices.removeAll()
resolvedServices.removeAll()
browser.stop()
}
}

// MARK: - NetServiceBrowserDelegate
/// Browser delegate takes care of found services.
/// Once the searching is over, it starts the address and IP resolving.
extension Bonjour: NetServiceBrowserDelegate {
// An error occured while seraching for services
func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber]) {
let error = unwrapError(fromDictionary: errorDict)

sink?.send(error: error)
}

// New service found (not yet resolved)
func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
foundServices.append(service)

if !moreComing {
// When the last service was found, start resolving addresses
resolveNext()
}
}
}

// MARK: - NetServiceDelegate
/// Delegate for service address resolving.
/// Once address is resolved, the IP is beeing reconstructed.
/// Only one service is resolved at the time.
extension Bonjour: NetServiceDelegate {
// The service address was resolved
func netServiceDidResolveAddress(_ service: NetService) {
// The original service is not needed anymore and can be removed
if let index = foundServices.index(of: service) {
foundServices.remove(at: index)
}

// Try to resolve IP from address
if let service = resolveIP(for: service) {
resolvedServices.append(service)
sink?.send(value: resolvedServices)
}

// Take another from stack
resolveNext()
}

// Resolve of service address failed
func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) {
let error = unwrapError(fromDictionary: errorDict)

sink?.send(error: error)
}
}

// Adds confrontance to error protocol to error code to be able to use
// it's value in signal.
extension NetService.ErrorCode: Error { }
25 changes: 25 additions & 0 deletions OctoPhone/Utils/Bonjour/BonjourService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// BonjourService.swift
// OctoPhone
//
// Created by Josef Dolezal on 10/05/2017.
// Copyright © 2017 Josef Dolezal. All rights reserved.
//

/// Represents correctly resolved service
struct BonjourService {
/// Service human readable name
var name: String

/// Service IP address with port
var address: String

/// Port on which is the service accessible
var port: String

/// Returns full address on which is the service available.
/// This includes the IP and port.
var fullAddress: String {
return "\(address):\(port)"
}
}

0 comments on commit 18af83e

Please sign in to comment.