Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added images/readme_img.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed images/readme_img.png
Binary file not shown.
4 changes: 4 additions & 0 deletions migrator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
53665E972BDBE12300086714 /* Decodable-Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53665E962BDBE12300086714 /* Decodable-Extension.swift */; };
53665E992BDBE14B00086714 /* ConfigProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53665E982BDBE14B00086714 /* ConfigProfile.swift */; };
53665E9B2BDBE15F00086714 /* DeviceManagementState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53665E9A2BDBE15F00086714 /* DeviceManagementState.swift */; };
536776C12D5F36FF0055F7A3 /* CustomAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536776C02D5F36EB0055F7A3 /* CustomAlertView.swift */; };
5377C2272B4FED5100B5C9C1 /* MigratorNetworkProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377C2262B4FED5100B5C9C1 /* MigratorNetworkProtocol.swift */; };
5377C2292B4FF2D100B5C9C1 /* NWParameters-Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377C2282B4FF2D100B5C9C1 /* NWParameters-Extension.swift */; };
5377C22B2B51551D00B5C9C1 /* MigratorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377C22A2B51551D00B5C9C1 /* MigratorError.swift */; };
Expand Down Expand Up @@ -149,6 +150,7 @@
53665E962BDBE12300086714 /* Decodable-Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decodable-Extension.swift"; sourceTree = "<group>"; };
53665E982BDBE14B00086714 /* ConfigProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigProfile.swift; sourceTree = "<group>"; };
53665E9A2BDBE15F00086714 /* DeviceManagementState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceManagementState.swift; sourceTree = "<group>"; };
536776C02D5F36EB0055F7A3 /* CustomAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertView.swift; sourceTree = "<group>"; };
5377C2262B4FED5100B5C9C1 /* MigratorNetworkProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratorNetworkProtocol.swift; sourceTree = "<group>"; };
5377C2282B4FF2D100B5C9C1 /* NWParameters-Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWParameters-Extension.swift"; sourceTree = "<group>"; };
5377C22A2B51551D00B5C9C1 /* MigratorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratorError.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -414,6 +416,7 @@
isa = PBXGroup;
children = (
539B256A2B47FBEE0012B974 /* NoBackgroundScroller.swift */,
536776C02D5F36EB0055F7A3 /* CustomAlertView.swift */,
535A77902BA49A5E00BB8116 /* CodeVerificationFieldView.swift */,
532F90B52C64FFE5002E270F /* WindowAccessor.swift */,
539B256E2B4817AC0012B974 /* DeviceListRow.swift */,
Expand Down Expand Up @@ -622,6 +625,7 @@
532F90B62C64FFE5002E270F /* WindowAccessor.swift in Sources */,
53237C112CA9E1D8004062CC /* JamfReconMethod.swift in Sources */,
53B193102C88AEA300DEFE24 /* DeviceManagementHelper.swift in Sources */,
536776C12D5F36FF0055F7A3 /* CustomAlertView.swift in Sources */,
5377C22D2B559D7900B5C9C1 /* Data-Extension.swift in Sources */,
53665E912BDBE0C600086714 /* ConfigProfilePayload.swift in Sources */,
53F2DE382B69055000588D2D /* NWProtocolFramer-Message-Extension.swift in Sources */,
Expand Down
24 changes: 15 additions & 9 deletions migrator/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,25 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
#endif
Task { @MainActor in
if let mainMenu = NSApplication.shared.mainMenu,
let appMenu = mainMenu.items.first(where: { $0.title == Bundle.main.name }),
let aboutItem = appMenu.submenu?.items.first?.copy() as? NSMenuItem {
// let helpMenu = mainMenu.items.last {
let firstMenu = NSMenuItem(title: Bundle.main.name, action: nil, keyEquivalent: "")
firstMenu.submenu = NSMenu(title: Bundle.main.name)
firstMenu.submenu?.addItem(aboutItem)
mainMenu.items.removeAll()
mainMenu.items.append(firstMenu)
// menu.items.append(helpMenu)
mainMenu.numberOfItems > 2 {
mainMenu.removeItem(at: 1)
}
}
}

func applicationDidFinishLaunching(_ notification: Notification) {
// Tracks the cmd-q keyboard shortcut to avoid unhandled app quits.
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
case [.command] where event.characters == "q":
self.userRequestToQuit = true
default:
return event
}
return event
}
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
// Returns true to quit the app when the last window is closed.
return true
Expand Down
75 changes: 52 additions & 23 deletions migrator/Controllers/MigrationController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,27 @@ class MigrationController: ObservableObject {
case initial
case discovery
case fetching
case wrongOTPCodeSent
case connectionEstablished
case readyForMigration
case fileMigration
case appMigration
case preferencesMigration
case interrupted
case cancelled
case paused
case restoring
case restoringConnection
case completing
case completed
}

// MARK: - Private Enum Definitions

private enum OperatingMode {
case server
case browser
}

// MARK: - Static Constants

/// Singleton instance for global access.
Expand Down Expand Up @@ -71,34 +81,32 @@ class MigrationController: ObservableObject {
}
}).store(in: &cancellables)
connection?.onNewConnectionState.sink(receiveValue: { newState in
self.connectionState = newState
switch newState {
case .setup, .waiting, .preparing:
Task { @MainActor in
self.connectionState = newState
self.migrationState = .fetching
self.isConnected = false
}
case .ready:
Task { @MainActor in
self.isConnected = true
self.connectionState = newState
self.migrationState = .connectionEstablished
}
case .failed:
Task { @MainActor in
self.connection?.connection.cancel()
self.connection = nil
self.connectionState = newState
self.connectionState = .setup
self.isConnected = false
self.migrationState = .interrupted
self.migrationState = .restoringConnection
self.restoreConnection()
}
case .cancelled:
Task { @MainActor in
self.connection = nil
self.connectionState = newState
self.connectionState = .setup
self.isConnected = false
self.migrationState = .interrupted
}
break
// Task { @MainActor in
// self.connection = nil
// self.isConnected = false
// self.migrationState = .cancelled
// }
@unknown default:
break
}
Expand All @@ -113,11 +121,20 @@ class MigrationController: ObservableObject {
var migrationOption: MigrationOption!
/// The selected result in the device list.
var selectedBrowserResult: NWBrowser.Result!

// MARK: - Private Variables

/// Collection of cancellable subscriptions to manage memory and avoid retain cycles.
private var cancellables = Set<AnyCancellable>()
/// Define if the current instance is used as `server` or `browser`
private var operatingMode: OperatingMode!
/// Passcode used to secure the connection.
private var passcode: String!

private var destinationDevice: NWBrowser.Result!

/// Tracks connection states.
private var connectionState: NWConnection.State = .setup

// MARK: - Private Constants

Expand All @@ -144,8 +161,6 @@ class MigrationController: ObservableObject {
@Published var bytesReceived: Int = 0
/// Tracks the completion of the migration.
@Published var isMigrationCompleted: Bool = false
/// Tracks connection states.
@Published var connectionState: NWConnection.State = .setup
/// Tracks migration controller state.
@Published var migrationState: MigrationState = .initial

Expand Down Expand Up @@ -205,6 +220,8 @@ class MigrationController: ObservableObject {
/// Starts the network server to accept incoming connections with a given passcode.
@MainActor
func startServer(withPasscode passcode: String) {
self.operatingMode = .server
self.passcode = passcode
logger.log("migrationController.networkServer.start: starting server with passcode \"\(passcode)\"...", type: .default)
do {
try server.start(withPasscode: passcode)
Expand All @@ -226,6 +243,7 @@ class MigrationController: ObservableObject {
/// Starts the network browser to search for available network services.
@MainActor
func startBrowser() {
self.operatingMode = .browser
logger.log("migrationController.networkBrowser.start: starting browser...", type: .default)
browser.start()
migrationState = .discovery
Expand All @@ -242,11 +260,12 @@ class MigrationController: ObservableObject {

/// Attempts to connect to a specified device using a passcode. Calls the completion handler with the result.
@MainActor
func connect(to device: NWBrowser.Result, withPasscode passcode: String = "000000", completion: @escaping (Bool) -> Void) {
func connect(to device: NWBrowser.Result, withPasscode passcode: String = "000000") {
self.passcode = passcode
self.destinationDevice = device
logger.log("migrationController.connect: starting connection with device \"\(device)\", using passcode \"\(passcode)\"", type: .default)
guard self.connection == nil else {
guard !self.isConnected else {
logger.log("migrationController.connect: a connection has already been established \"\(self.connection?.connection.debugDescription ?? "nil")\", discarding new connection request...", type: .default)
completion(true)
return
}

Expand All @@ -259,9 +278,6 @@ class MigrationController: ObservableObject {
logger.log("migrationController.connect: starting connection...", type: .default)
self.hostName = device.resultName
self.checkConnectionEstablishment(5)

// Calls completion with success after setting up the connection.
completion(true)
}

@MainActor
Expand All @@ -277,6 +293,18 @@ class MigrationController: ObservableObject {
self.selectedBrowserResult = nil
}

@MainActor
func restoreConnection() {
switch self.operatingMode {
case .server:
self.startServer(withPasscode: self.passcode)
case .browser:
self.connect(to: self.destinationDevice, withPasscode: self.passcode)
case .none:
break
}
}

// MARK: - Private Methods

/// Method used to identify issues during the establishment of a connection.
Expand All @@ -294,6 +322,7 @@ class MigrationController: ObservableObject {
guard attempts > 1 else {
self.logger.log("migrationController.connect: failed to get connection establishment report. Connection will be cancelled", type: .error)
self.connection?.connection.forceCancel()
self.migrationState = .wrongOTPCodeSent
return
}
self.logger.log("migrationController.connect: failed to get connection establishment report. Trying again in 3 seconds...", type: .fault)
Expand Down
18 changes: 12 additions & 6 deletions migrator/Controllers/NetworkControllers/NetworkBrowser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ final class NetworkBrowser {
/// The NWBrowser instance used for discovering network services.
private var browser: NWBrowser

// MARK: - Private Static Variables

/// Static variable that store the Network Connection parameters.
private static var parameters: NWParameters {
let parameters = NWParameters()
parameters.includePeerToPeer = true
parameters.attribution = .developer
return parameters
}

// MARK: - Private Constants

/// Logger instance.
Expand All @@ -35,10 +45,8 @@ final class NetworkBrowser {

/// Initializes a new NetworkBrowser instance configured for discovering Bonjour services.
init() {
let parameters = NWParameters()
parameters.includePeerToPeer = true
// Configures the browser for discovering services with the specified Bonjour type and domain.
browser = NWBrowser(for: .bonjour(type: AppContext.networkServiceIdentifier+"._tcp", domain: nil), using: parameters)
browser = NWBrowser(for: .bonjour(type: AppContext.networkServiceIdentifier+"._tcp", domain: nil), using: Self.parameters)
}

// MARK: - Public Methods
Expand All @@ -61,8 +69,6 @@ final class NetworkBrowser {
// Cancels the current browsing session.
browser.cancel()
// Reinitializes the browser to be ready for a new discovery session.
let parameters = NWParameters()
parameters.includePeerToPeer = true
browser = NWBrowser(for: .bonjour(type: "_migrator._tcp", domain: nil), using: parameters)
browser = NWBrowser(for: .bonjour(type: "_migrator._tcp", domain: nil), using: Self.parameters)
}
}
46 changes: 23 additions & 23 deletions migrator/Controllers/NetworkControllers/NetworkConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@
init(endpoint: NWEndpoint, withPasscode passcode: String) {
logger.log("newtorkConnection.initOutgoingConnection: endpoint \"\(endpoint.debugDescription)\"", type: .default)
let parameters = NWParameters(passcode: passcode)
parameters.attribution = .developer
connection = NWConnection(to: endpoint, using: parameters)
connection.pathUpdateHandler = { path in
self.logger.log("newtorkConnection.pathUpdateHandler: newPath \"\(path.debugDescription)\"")
Expand Down Expand Up @@ -179,7 +178,7 @@
}

func sendMigrationCompleted() async throws {
if let data = "Completion".data(using: .utf8) {

Check warning on line 181 in migrator/Controllers/NetworkControllers/NetworkConnection.swift

View workflow job for this annotation

GitHub Actions / linting

Non-optional String -> Data Conversion Violation: Prefer non-optional `Data(_:)` initializer when converting `String` to `Data` (non_optional_string_data_conversion)
let message = NWProtocolFramer.Message(migratorMessageType: .result, infoLenght: 0)
let context = NWConnection.ContentContext(identifier: "MigrationCompleted",
metadata: [message])
Expand All @@ -199,29 +198,29 @@
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
connection.send(content: content, contentContext: contentContext, isComplete: true, completion: .contentProcessed({ error in
if let error = error {
self.logger.log("newtorkConnection.sendAsync: error sending message \"\(contentContext.identifier)\", error \"\(error.localizedDescription)\"", type: .error)
MLogger.main.log("newtorkConnection.sendAsync: error sending message \"\(contentContext.identifier)\", error \"\(error.localizedDescription)\"", type: .error)
continuation.resume(throwing: error)
} else {
self.logger.log("newtorkConnection.sendAsync: done sending message \"\(contentContext.identifier)\"")
MLogger.main.log("newtorkConnection.sendAsync: done sending message \"\(contentContext.identifier)\"")
continuation.resume()
}
}))
}
}

private func closeFile(_ fileHandle: FileHandle) throws {
do {
try fileHandle.close()
} catch let error {
throw MigratorError.fileError(type: .failedDuringFileHandling(error: error))
}
}

/// Handles the sending of a file, breaking it into chunks if necessary, and collecting symbolic links as needed.
/// - Parameter file: The file to send.
private func _sendFile(_ file: MigratorFile) async throws {
func closeFile(_ fileHandle: FileHandle) throws {
do {
try fileHandle.close()
} catch let error {
throw MigratorError.fileError(type: .failedDuringFileHandling(error: error))
}
}
logger.log("networkConnection.sendfile: preparing file \"\(file.url.fullURL().relativePath)\"")

let chunkSize: UInt64 = 200000000
let chunkSize: UInt64 = 33_554_432

if file.type == .symlink {
do {
Expand Down Expand Up @@ -258,7 +257,7 @@

if file.type == .directory || file.type == .app {
let infoData = FileMessage(with: file.url.fullURL(), part: 0, attributes: attributes)
guard var data = "directory".data(using: .utf8),

Check warning on line 260 in migrator/Controllers/NetworkControllers/NetworkConnection.swift

View workflow job for this annotation

GitHub Actions / linting

Non-optional String -> Data Conversion Violation: Prefer non-optional `Data(_:)` initializer when converting `String` to `Data` (non_optional_string_data_conversion)
let infoDataLenght = try? data.include(object: infoData) else {
throw MigratorError.internalError(type: .data)
}
Expand All @@ -271,9 +270,17 @@
self.onBytesSent.send(data.count)

logger.log("networkConnection.sendfile: start sending content of directory \"\(file.url.fullURL().relativePath)\"")
for child in file.childs {
try? await sendFile(child)
self.onBytesSent.send(data.count)
if !file.childFiles.isEmpty {
for child in file.childFiles {
try? await sendFile(child)
self.onBytesSent.send(data.count)
}
} else {
let unretainedChilds = await file.fetchUnretainedChilds()
for child in unretainedChilds {
try? await sendFile(child)
self.onBytesSent.send(data.count)
}
}
return
}
Expand Down Expand Up @@ -356,20 +363,13 @@
/// Handles the sending of a file, breaking it into chunks if necessary, and collecting symbolic links as needed.
/// - Parameter fileURL: The URL of the file to send.
private func _sendFile(at fileURL: URL) async throws {
func closeFile(_ fileHandle: FileHandle) throws {
do {
try fileHandle.close()
} catch let error {
throw MigratorError.fileError(type: .failedDuringFileHandling(error: error))
}
}
logger.log("networkConnection.sendfile: preparing file \"\(fileURL.relativePath)\"")
guard !AppContext.excludedFileExtensions.contains(fileURL.lastPathComponent) && fileURL.lastPathComponent.first != "~" else {
logger.log("networkConnection.sendfile: file \"\(fileURL.relativePath)\" needs to be ignored. This should'n happen.", type: .fault)
return
}

let chunkSize: UInt64 = 200000000
let chunkSize: UInt64 = 33_554_432
var isDirectory: ObjCBool = false

if let destinationPath = try? FileManager.default.destinationOfSymbolicLink(atPath: fileURL.relativePath),
Expand Down Expand Up @@ -405,7 +405,7 @@

guard !isDirectory.boolValue else {
let infoData = FileMessage(with: fileURL, part: 0, attributes: attributes)
guard var data = "directory".data(using: .utf8),

Check warning on line 408 in migrator/Controllers/NetworkControllers/NetworkConnection.swift

View workflow job for this annotation

GitHub Actions / linting

Non-optional String -> Data Conversion Violation: Prefer non-optional `Data(_:)` initializer when converting `String` to `Data` (non_optional_string_data_conversion)
let infoDataLenght = try? data.include(object: infoData) else {
throw MigratorError.internalError(type: .data)
}
Expand Down Expand Up @@ -502,7 +502,7 @@
private func sendSymlinks() async throws {
logger.log("networkConnection.sendSymlinks: start sending collected symlinks")
for message in symlinks {
guard var data = "link".data(using: .utf8),

Check warning on line 505 in migrator/Controllers/NetworkControllers/NetworkConnection.swift

View workflow job for this annotation

GitHub Actions / linting

Non-optional String -> Data Conversion Violation: Prefer non-optional `Data(_:)` initializer when converting `String` to `Data` (non_optional_string_data_conversion)
let infoDataLenght = try? data.include(object: message) else {
throw MigratorError.fileError(type: .noData)
}
Expand All @@ -518,7 +518,7 @@
/// Sends collected symbolic links to the connected device.
func sendDefaults(_ object: DefaultsMessage) async throws {
logger.log("networkConnection.sendDefaults: start sending \(object.key) UserDefaults")
guard var data = "defaults".data(using: .utf8),

Check warning on line 521 in migrator/Controllers/NetworkControllers/NetworkConnection.swift

View workflow job for this annotation

GitHub Actions / linting

Non-optional String -> Data Conversion Violation: Prefer non-optional `Data(_:)` initializer when converting `String` to `Data` (non_optional_string_data_conversion)
let infoDataLenght = try? data.include(object: object) else {
throw MigratorError.fileError(type: .noData)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ final class NetworkServer {
/// - Parameter passcode: A passcode required for clients to connect to this service.
func start(withPasscode passcode: String) throws {
let parameters = NWParameters(passcode: passcode)
parameters.attribution = .developer
listener = try NWListener(using: parameters)
listener?.service = NWListener.Service(type: AppContext.networkServiceIdentifier+"._tcp")
// Handler for listener state updates.
Expand Down
1 change: 1 addition & 0 deletions migrator/Extensions/NWParameters-Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
tcpOptions.keepaliveIdle = 1
tcpOptions.keepaliveCount = 2
tcpOptions.keepaliveInterval = 1
tcpOptions.noDelay = true

// Initialize `NWParameters` with custom TLS options (derived from the passcode) and the specified TCP options.
self.init(tls: NWParameters.tlsOptions(passcode: passcode), tcp: tcpOptions)
Expand All @@ -40,7 +41,7 @@
// Derive a symmetric key from the passcode.
let authenticationKey = SymmetricKey(data: passcode.data(using: .utf8)!)
// Create an authentication code for "MigratorNetworkProtocol" using HMAC with SHA256.
let authenticationCode = HMAC<SHA256>.authenticationCode(for: "MigratorNetworkProtocol".data(using: .utf8)!, using: authenticationKey)

Check warning on line 44 in migrator/Extensions/NWParameters-Extension.swift

View workflow job for this annotation

GitHub Actions / linting

Non-optional String -> Data Conversion Violation: Prefer non-optional `Data(_:)` initializer when converting `String` to `Data` (non_optional_string_data_conversion)

// Convert the authentication code to `DispatchData`.
let authenticationDispatchData = authenticationCode.withUnsafeBytes {
Expand Down
Loading
Loading