Version 2.6.4

Version 2.6.4 (build 158) - August 18, 2025

There are four major updates within this release:

  • method for observers, based on AsyncSequence, has been applied, please refer to the blog post Observers for more details
  • in Tasks, options for trailing slash is refactored
  • some views are refactored by using Form, Form provides a more streamlined and user-friendly interface
  • some data in some views are saved by using Swift´s UserDefaults

Some minor UI updates have been implemented, including enhanced headings in the Tasks section and Rsync parameters to enhance visual distinction.

Tasks

Within the Tasks section, the trailing slash option is refactored as follows:

  • Add trailing slash - add a trailing slash to both the source and destination
  • Do not add trailing slash - do not add a trailing slash, or if added, remove it
  • Do not check - do not check for trailing slash or not on either the source or destination

Additionally, the values for the Action and Trailing / options are saved during the session if set to values other than the default. These values persist across Add, Update, and Profile changes. They are temporarily saved by UserDefaults and are reset to default values when RsyncUI restarts.

Home Catalog (Tasks) and Quick task

In both Home Catalogs and Quick task, the views have been refactored to utilize Form. Form provides a more streamlined and user-friendly interface. Additionally, data added within Quicktask is now saved by default by UserDefaults. When Quicktask is reentered, the data is automatically restored.

RsyncUI application icon

  • The RsyncUI application icon has been updated using the new Icon Composer tool.
  • This update supports icons in the new macOS Tahoe 26 and incorporates layered icons. The icon has also undergone a slight redesign, featuring a new cloud and numbers as layers in Icon Composer.

Version 2.6.2

Version 2.6.2 (build 156) - July 21, 2025

Version 2.6.2 of RsyncUI require Xcode 26 and Swift 6.2 to compile.

Updates in this release

There was a request to display the % of completed transfer pr file. It seems difficult to pick up the correct info if like --info=progress2 is added as parameter to rsync. This parameter causes rsync to publish a lot of data, but it is difficult to get the correct info. You may test the parameter yourself by pasting the complete rsync command into a terminal window. But, instead a counter completed files was easy to add.

  • only UI updates, no model changes
  • added counter files completed and total number of files
    • the total number of files requiere an estimation run ahead of synchronization
    • the counter is displayed as part of the progress bar
  • consistent use of source and destination

ChatGPT says the following: In rsync documentation and usage, the most common terms are source and destination. These clearly indicate where the data is coming from and where it is going. While local and remote can be used when specifying locations across different systems, source and destination are universally applicable for both local and remote transfers.

In version, version 2.6.2, the terms have been revised to consecvent source and destination. The term remote is now exclusively used to refer to remote user and remote server.

Number of files

Numbers updated: August 12, 2025, version 2.6.3

There is a very nice and excellent tool, cloc (https://github.com/AlDanial/cloc), for counting of files and lines of code. Below are the numbers for Swift files which are part of the repository for compiling RsyncUI. RsyncUI does not rely on external libraries; it is constructed using default Swift libraries and Swift/SwiftUI code exclusively.

cloc DecodeEncodeGeneric/Sources ParseRsyncOutput/Sources RsyncArguments/Sources RsyncUI/RsyncUI RsyncUIDeepLinks/Sources SSHCreateKey/Sources
     192 text files.
     191 unique files.                                          
       4 files ignored.

github.com/AlDanial/cloc v 2.06  T=0.07 s (2637.9 files/s, 278295.3 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Swift                          185           2103           2342          15280
C                                2             36             72            254
XML                              2              0              0             53
JSON                             1              0              0              6
C/C++ Header                     1              1              3              0
-------------------------------------------------------------------------------
SUM:                           191           2140           2417          15593
-------------------------------------------------------------------------------

Main Repository:

Local RsyncUI packages:

SPM, Swift Package Manager, makes it easy to create local packages. And each package containes their own tests by Swift Testing, the new framwork for creating tests. All packages are created by me.

Observers

A key feature of RsyncUI is observation for two notifications:

  • NSNotification.Name.NSFileHandleDataAvailable
  • Process.didTerminateNotification

Without observation and required actions when observed, RsyncUI becomes useless. Both observations are linked to the external task executing the actual rsync task.

Notifications

The initial notification, NSNotification.Name.NSFileHandleDataAvailable, monitors the generation of output by the external task. To display the progress of a synchronization task, RsyncUI relies on monitoring the output from rsync. Consequently, the —verbose parameter to rsync is essential. This parameter instructs rsync to output information during execution.

The second notification, Process.didTerminateNotification, monitors the termination of the task, such as an abrupt halt. Typically, a termination signifies task completion. However, it can also indicate an abort action from the user, which subsequently sends an interrupt signal to the external task. If RsyncUI fails to detect this signal, it will be unable to discern when a synchronization task has been completed.

In the latest version of RsyncUI (2.6.4), a new method for observing notifications have been introduced. After some investigation and recommendation, the AsyncSequence method emerges as the preferred approach. By applying this the Combine Framework and depended code is removed.

NotificationCenter.Notifications and AsyncSequence

If you search for the following: NotificationCenter.Notifications + AsyncSequence, by AI Google summarize the search by:

NotificationCenter.Notifications provides a method to receive notifications from NotificationCenter as an AsyncSequence, enabling asynchronous iteration using the for await syntax. This modern approach to handling notifications in Swift concurrency contrasts with older methods that may utilize Combine or require explicit handling of NSNotification objects.

Quote Apple documentation: “An asynchronous sequence of notifications generated by a notification center.” The for await loops iterate over the notifications and respond to each one. Upon detecting the termination signal, the tasks must also be canceled, and the observers must be removed.

// AsyncSequence
let sequencefilehandler = NotificationCenter.default.notifications(named: NSNotification.Name.NSFileHandleDataAvailable, object: nil)
let sequencetermination = NotificationCenter.default.notifications(named: Process.didTerminateNotification, object: nil)
// Tasks
var sequenceFileHandlerTask: Task<Void, Never>?
var sequenceTerminationTask: Task<Void, Never>?
....
sequenceFileHandlerTask = Task {
            for await _ in sequencefilehandler {
                await self.datahandle(pipe)
            }
        }
        
sequenceTerminationTask = Task {
            for await _ in sequencetermination {
                Task {
                    try await Task.sleep(seconds: 0.5)
                    await self.termination()
                }
            }
        }

Upon task completion, the Observers must be released, and corresponding Tasks must be canceled to prevent a retain cycle and memory leak.

func datahandle(_ pipe: Pipe) async {
        let outHandle = pipe.fileHandleForReading
        let data = outHandle.availableData
        if data.count > 0 {
            if let str = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
                str.enumerateLines { line, _ in
                    self.output.append(line)
                    if SharedReference.shared.checkforerrorinrsyncoutput,
                       self.errordiscovered == false
                    {
                        do {
                            try self.checklineforerror?.checkforrsyncerror(line)
                        } catch let e {
                            self.errordiscovered = true
                            let error = e
                            self.propogateerror(error: error)
                        }
                    }
                }
                // Send message about files
                if usefilehandler {
                    filehandler(output.count)
                }
            }
            outHandle.waitForDataInBackgroundAndNotify()
        }
    }

func termination() async {
        processtermination(output, config?.hiddenID)
        // Log error in rsync output to file
        if errordiscovered, let config {
            Task {
                await ActorLogToFile(command: config.backupID,
                                     stringoutputfromrsync: output)
            }
        }
        SharedReference.shared.process = nil
        // Remove observers
        NotificationCenter.default.removeObserver(sequencefilehandler as Any,
                                                  name: NSNotification.Name.NSFileHandleDataAvailable,
                                                  object: nil)
        NotificationCenter.default.removeObserver(sequencetermination as Any,
                                                  name: Process.didTerminateNotification,
                                                  object: nil)
        // Cancel Tasks
        sequenceFileHandlerTask?.cancel()
        sequenceTerminationTask?.cancel()
        
        Logger.process.info("ProcessRsyncAsyncSequence: process = nil and termination discovered")
    }

Removed methods

The two following methods, which still works very well, are removed from the code.

Combine and Publisher

The Combine framework is utilized within the ProcessRsync and ProcessCommand objects, which is responsible for initiating external tasks, such as the rsync synchronize task. The rsync synchronize task is completed when the last notification is observed. By using Combine, a publisher is added to the Notification center. Every time the Notification center discover one of the notifications, it publish a message.

// Combine, subscribe to NSNotification.Name.NSFileHandleDataAvailable
NotificationCenter.default.publisher(
  for: NSNotification.Name.NSFileHandleDataAvailable)
  .sink { [self] _ in
  ....
  }.store(in: &subscriptons)
// Combine, subscribe to Process.didTerminateNotification
NotificationCenter.default.publisher(
    for: Process.didTerminateNotification)
        .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
        .sink { [self] _ in
        ....
        subscriptons.removeAll()
    }.store(in: &subscriptons)

NotificationCenter and addObserver

The second method for observing notifications involves adding Observers to the Notification center. Upon the discovery of a notification, the completion handler is executed. The Process object is annotated to execute on the main thread. It appears that the addObserver closure is marked as Sendable, indicating that mutating properties within the closure must be asynchronous. This is due to the Swift 6 language mode and strict concurrency checking.

// Observers
var notificationsfilehandle: NSObjectProtocol?
var notificationstermination: NSObjectProtocol?
....
notificationsfilehandle =
    NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable,
                                                   object: nil, queue: nil)
        { _ in
            Task {
                await self.datahandle(pipe)
            }
        }

notificationstermination =
    NotificationCenter.default.addObserver(forName: Process.didTerminateNotification,
                                                   object: task, queue: nil)
        { _ in
                Task {
                    // Debounce termination for 500 ms
                    try await Task.sleep(seconds: 0.5)
                    await self.termination()
                }
        }

Upon task completion, the Observers must be released to prevent a retain cycle and memory leak.

func datahandle(_ pipe: Pipe) async {
         let outHandle = pipe.fileHandleForReading
         let data = outHandle.availableData
         if data.count > 0 {
             if let str = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
                 str.enumerateLines { line, _ in
                     self.output.append(line)
                     if SharedReference.shared.checkforerrorinrsyncoutput,
                        self.errordiscovered == false
                     {
                         do {
                             try self.checklineforerror?.checkforrsyncerror(line)
                         } catch let e {
                             self.errordiscovered = true
                             let error = e
                             self.propogateerror(error: error)
                         }
                     }
                 }
                 // Send message about files
                 if usefilehandler {
                     filehandler(output.count)
                 }
             }
             outHandle.waitForDataInBackgroundAndNotify()
         }
     }

func termination() async {
        processtermination(output, config?.hiddenID)
        // Log error in rsync output to file
        if errordiscovered, let config {
            Task {
                await ActorLogToFile(command: config.backupID,
                                     stringoutputfromrsync: output)
            }
        }
        SharedReference.shared.process = nil
        NotificationCenter.default.removeObserver(notificationsfilehandle as Any,
                                                  name: NSNotification.Name.NSFileHandleDataAvailable,
                                                  object: nil)
        NotificationCenter.default.removeObserver(notificationstermination as Any,
                                                  name: Process.didTerminateNotification,
                                                  object: nil)
        Logger.process.info("ProcessRsyncObserving: process = nil and termination discovered")
    }

Swift concurrency

To commence, I must acknowledge that my proficiency in Swift concurrency is limited. While I possess a rudimentary comprehension of the subject matter, if you are perusing this blog and seeking further elaboration on the topic, I strongly recommend conducting a search and perusing articles from alternative sources that offer a more comprehensive understanding of Swift concurrency.

RsyncUI is a graphical user interface (GUI) application; the majority of its operations are executed on the main thread. However, some resource-intensive tasks are performed on other threads managed by the cooperative thread pool (CTP), excluding and not blocking the main thread. How to the executors and CTP works and interacts is details I dont know about, and it is managed by the Swift runtime. There are three kinds of executors:

  • the main executor manage jobs on the main thread
  • the global concurrent executor and the serial executor, both executes jobs on threads from the CTP

Most work in RsyncUI are executed on the main thread. And by default, SwiftUI makes sure all UI-updates are performed on the main thread. Below are some tasks within RsyncUI, which are executed on the main thread:

  • preparing of and execution of rsync synchronize tasks, preparing is computing the correct arguments for rsync
  • monitoring progress and termination of the real rsync tasks
    • monitoring progress is an asynchronous sequence running on the main thread, by asynchronous sequence of two specific notifications generated by the notification center
  • some write and read operations

Swift version 6 and the new concurrency model

Swift version 6 introduced strict concurrency checking. By enabling Swift 6 language mode and strict concurrency checking, Xcode assists in identifying and resolving possible data races at compile time.

Quote swift.org: “More formally, a data race occurs when one thread accesses memory while the same memory is being modified by another thread. The Swift 6 language mode eliminates these issues by preventing data races at compile time.”

RsyncUI adheres to the new concurrency model of Swift 6.

Swift concurrency and asynchronous execution

Concurrency and asynchronous execution are important parts in Swift. The latest version of Swift simplifies the writing of asynchronous code using Swift async and await keywords, as well as the actor protocol. RsyncUI does not require concurrency, but concurrency is automatically introduced by using actors , async and await keywords. It is an objective to execute most work synchronous on the main thread as long as it does not block the GUI.

Asynchronous execution can be performed on both the main thread and background threads from the CTP. When executing asynchronous operations on the main thread, it is crucial to utilize Swift’s structured concurrency, specifically the async/await keywords. The await keyword serves as a suspension point, allowing other and more critical tasks to access the main thread.

Cooperative thread pool (CTP)

The following tasks are executed asynchronous on threads from the CTP, adhering to the actor protocol:

  • read synchronize tasks from file
    • JSON data decoding, asynchronous decoding which inherits the thread from the actor reading data
    • JSON data encoding, synchronous encoding on the main thread
  • read and sort log records
  • preparing output from rsync for display
  • preparing data from the logfile for display
  • checking for updates to RsyncUI

Adhering to the actor protocol, all access to properties within an actor must be performed asynchronously. There are only five actors in RsyncUI. But there are more asynchronous functions, some are running on the main thread as well.

Structured concurrency

Some concurrent functions within RsyncUI are structured by using async let. You may have several async let, and they will all be executed in parallel or concurrent. When all async let tasks are completed, the root task or parent task, will continue execution.

Structured concurrency might also dictate the order of execution. The keyword await is a suspension where execution waits until the asynchronous function is completed before continuing the execution. If there are several await after each other, the next one will be executed when the current asynchronous task is completed.

func readconfigurations() {
    Task {
       	let monitornetworkconnection = SharedReference.shared.monitornetworkconnection
        let sshport = SharedReference.shared.sshport
        let actorReadSynchronizeConfigurationJSON = ActorReadSynchronizeConfigurationJSON()
        async let data = actorReadSynchronizeConfigurationJSON
                    .readjsonfilesynchronizeconfigurations(profile,
                                                           monitornetworkconnection,
                                                           sshport)
        rsyncUIdata.configurations = await data
    }
}
Unstructured concurrency

The code snippet below presents an unstructured concurrency. The code within the Task {} does not have a parent/child relationship to the caller. And it may be completed either before or after the execution of the calling function, the parent, is completed.

@MainActor
func somefunction() {
   // Some code
    Task {
      newversion.notifynewversion = await GetversionofRsyncUI().getversionsofrsyncui()
	}
  // Some code
}

The primary reason for utilizing an actor is to execute the task on a separate thread from the CTP, thereby preventing blocking of the main thread. The calling function operates on the main thread, while the asynchronous function executes on a background thread. In the event that the remote call takes an extended period or times out, the UI remains unblocked. The call retrieves a file from GitHub to determine whether a new version of RsyncUI is available.

actor GetversionofRsyncUI {
    @concurrent
    nonisolated func getversionsofrsyncui() async -> Bool {
        do {
            let versions = await DecodeGeneric()
            if let versionsofrsyncui =
                try await versions.decodearraydata(VersionsofRsyncUI.self,
                                                   fromwhere: Resources().getResource(resource: .urlJSON))
            {
                let runningversion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
                let check = versionsofrsyncui.filter { runningversion.isEmpty ? true : $0.version == runningversion }
                if check.count > 0 {
                    return true
                } else {
                    return false
                }
            }
        } catch {
            return false
        }
        return false
    }
}

Tagging of data

Tagging of data to be synchronized

It is imperative that RsyncUI tags tasks with data to be synchronized correctly. If the tagging fails, there may be source data that is not synchronized. RsyncUI supports the latest version of rsync and the older default version of rsync included in macOS 14 and macOS 15.

The tagging of data to be synchronized is computed within the package ParseRsyncOutput, a local Swift Package for RsyncUI.

From version 2.4.0, there is a verification of the tagging, if the output from rsync is greater than 20 lines and tagging for data to synchronize is not set, an alert is thrown. Normally, if there are no data to synchronize output from rsync is about 20 lines. Extract numbers from a string containing letters and digits is from version 2.4.0 of RsyncUI is now a one line code.

Example:

  • the string Number of created files: 7,191 (reg: 6,846, dir: 345) as input
  • is converted to [7191,6846,345], the thousand mark is also removed from string ahead parsing

The function below extract numbers only from the input.

 public func returnIntNumber( _ input: String) -> [Int] {
        var numbers: [Int] = []
        let str = input.replacingOccurrences(of: ",", with: "")
        let stringArray = str.components(separatedBy: CharacterSet.decimalDigits.inverted).compactMap { $0.isEmpty == true ? nil : $0 }
        for item in stringArray where item.isEmpty == false {
            if let number = Int(item) {
                numbers.append(number)
            }
        }
        if numbers.count == 0 {
            return [0]
        } else {
            return numbers
        }
    }

The parsing of the output of rsync is not particularly complex, and it is somewhat different for the latest version of rsync compared to the default versions of rsync.

Latest version of rsync

The trail of output from latest version of rsync, version 3.4.1, is like:

....
Number of files: 7,192 (reg: 6,846, dir: 346)
Number of created files: 7,191 (reg: 6,846, dir: 345)
Number of deleted files: 0
Number of regular files transferred: 6,846
Total file size: 24,788,299 bytes
Total transferred file size: 24,788,299 bytes
Literal data: 0 bytes
Matched data: 0 bytes
File list size: 0
File list generation time: 0.003 seconds
File list transfer time: 0.000 seconds
Total bytes sent: 394,304
Total bytes received: 22,226
sent 394,304 bytes  received 22,226 bytes  833,060.00 bytes/sec
total size is 24,788,299  speedup is 59.51 (DRY RUN)

Default version of rsync

The trail of output from default version of rsync is like:

....
Number of files: 7192
Number of files transferred: 6846
Total file size: 24788299 bytes
Total transferred file size: 24788299 bytes
Literal data: 0 bytes
Matched data: 0 bytes
File list size: 336861
File list generation time: 0.052 seconds
File list transfer time: 0.000 seconds
Total bytes sent: 380178
Total bytes received: 43172
sent 380178 bytes  received 43172 bytes  169340.00 bytes/sec
total size is 24788299  speedup is 58.55

How does the tagging work

The output from rsync is parsed and numbers are extracted. After parsing of output, the numbers decide if there is tagging of data to be synchronized.

Latest version of rsync

There are three numbers which decide data to synchronize or not: number of updates (regular files transferred), new files or deleted files. And they all may be 0 or a number, all three must be verified.

Default versions

There is only one number which decide data to synchronize or not: number of updates (files transferred).

URLs Notepad

A few samples of URL´s I am executing from Notepad. URL´s must start with rsyncuiapp://, if not RsyncUI will not recognize the command. My URL´s saved in Notepad for easy access and execution of my most used tasks. The Notepad page might also be saved as a PDF document which includes til URL links.

Sample URLs which I am using

URLs for estimate and execute.

Remote server - raspberrypi with zfs filesystem

  • rsyncuiapp://loadprofileandestimate?profile=Pictures
  • rsyncuiapp://loadprofileandestimate?profile=default

NVME SSD disks

The mount point, such as /Volumes/WDBackup, and the profile name are set to the same value solely for the purpose of simplifying the identification of the mounted disk and the corresponding profile to use in RsyncUI. As documented here, I am performing backups on several SSD disks and also to a remote server. The rationale behind having multiple backups is that it is straightforward to update all backups on a regular basis using RsyncUI. Additionally, if one disk or the server fails, I always have an updated backup to restore from.

Mounted as WDBackup

  • rsyncuiapp://loadprofileandestimate?profile=WDBackup

Mounted as Samsung

  • rsyncuiapp://loadprofileandestimate?profile=Samsung

Mounted as LaCie

  • rsyncuiapp://loadprofileandestimate?profile=LaCie

URL for verify a remote

Verify remote is only for remote destinations. Remote server - raspberrypi with zfs filesystem. Profile is Pictures and Synchronize ID = Pictures backup, the space is replaced by a _ in URL for the id tag in URL, which is the search for the wanted Synchronize ID.

  • rsyncuiapp://loadprofileandverify?profile=Pictures&id=Pictures_backup

Console and OSLog

Included in Swift 5 is a unified logging feature called OSLog. This feature provides several methods for logging and investigating the application’s activities. By utilizing OSLog, print statements are no longer necessary to follow the execution of code. All logging is performed through OSLog, which is displayed as part of Xcode. OSLog is integrated into all objects that perform work, making it straightforward to identify which commands RsyncUI is executing.

OSLog information in Xcode. The logging displays commands and arguments as shown below. This feature facilitates the verification that RsyncUI is executing the correct command.

And the OSLogs might be read by using the Console app. Be sure to set:

  • the Action in Console app menu to Include Info Messages
  • enter no.blogspot.RsyncUI as subsystem within the Search field