Version 2.8.5

Version 2.8.5 - In development

The primary advancement in this version is the implementation of significant user interface (UI) enhancements. These updates were requested by GitHub user @timreichen (Tim Reichen) to enhance the intuitiveness of RsyncUI. Tim Reichen’s input is greatly appreciated.

The UI improvements utilize the Inspector view to provide detailed information about tasks. Adding new tasks is now facilitated through a dedicated sheet. Selecting a task displays its associated details. Additionally, a toggle on the toolbar allows users to switch between viewing task data and parameters.

The detail views are implemented by utilizing SwiftUI Inspector view.

Version 2.8.4

Version 2.8.4 - Dec 26, 2025

Version 2.8.4rc2 has been released as the new version, with no new builds released.

I am uncertain as to why the AsyncStream encounters issues with the aforementioned problem. Nevertheless, I intend to investigate and determine if I can resolve this issue for my own learning and interest. Specifically, I will implement a mechanism to halt the process and drain any remaining data before the termination task is executed.

Major Features & Improvements

  • Total Commits (git) Since 2.8.2: 137+
  • Development Period: December 12-24, 2025

🔄 RsyncProcessStreaming Migration - Complete

  • Unified Process Execution Model: All process execution now uses the RsyncProcessStreaming package for event-driven, streaming process handling
  • Simplified Handler Management: Introduced ProcessHandlers factory with built-in cleanup hooks to prevent memory leaks and retain cycles
  • Real-time Output Streaming: Process output is now streamed in real-time, enabling immediate UI updates and better user feedback
  • Files Updated: Estimate.swift, Execute.swift, RestoreTableView.swift, OneTaskDetailsView.swift, ExecutePushPullView.swift, VerifyTasks.swift

📦 ParseRsyncOutput Swift Package Integration

  • Extracted rsync output parsing into a reusable Swift Package for improved code reusability and testability
  • Unified parsing logic across all rsync output processing operations
  • Package repository: ParseRsyncOutput

✅ Comprehensive Testing Framework

  • New RsyncUITests target added with initial test suite
  • UI tests for arguments and deeplink URLs
  • Output processing tests covering:
    • Tail trimming (20-line limit)
    • Directory line filtering
    • Error detection patterns
    • Malformed/empty output edge cases

Core Changes

Process Execution & Streaming

  • Refactored ObservableRestore to use RsyncProcessStreaming
  • Refactored task verification to use streaming process handlers
  • Added streaming support to QuicktaskView
  • Refactored rsync version check to use streaming process
  • Remove RsyncProcess dependency - replaced with RsyncProcessStreaming
  • Streaming handler cleanup refactoring to prevent retain cycles and memory leaks

UI/UX Enhancements

  • Improved UI feedback for task progress and completion
  • Enhanced progress view with better styling and layout
  • Added configuration table view to ProfileView
  • Renamed table column from ‘Time’ to ‘Time last’ across multiple views
  • Refactored profile deletion UI in ProfileView
  • Improved help text for –delete parameter usage
  • Better handling of hiddenID in Estimate and Execute operations
  • Removed RsyncRealtimeView and related obsolete UI references

Code Quality & Refactoring

  • Refactor optional handling patterns with guard-chain flattening and single binding for counts
  • Refactor enum cases to camelCase: RsyncCommand, PushPullCommand, OtherRsyncCommand, PlanSnapshots, DayOfWeek
  • Variable naming improvements for consistency and clarity across codebase
  • Removed unnecessary whitespace and improved code formatting
  • SwiftLint configuration updates and inline rule directives
  • Refactored UI views for improved readability (AddTaskView, SidebarMainView, TasksView, QuicktaskView)
  • Refactored parameter field handling in configuration models with optional fields

Configuration & Settings

  • New argument validation feature: Added toggle for argument validation in execution flow
  • Added ‘validate arguments’ setting to user configuration
  • Improved argument validation with nil checks and proper error handling
  • Add dry-run validation to argument checks
  • Configuration model improvements with optional parameter handling

Logging & Debugging

  • Refactored and cleaned up logging and test code
  • Removed rsync output logfile support from log views
  • Gated per-line streaming logs behind DEBUG flag
  • Added debug threading check for streaming handlers
  • Silenced debug print statements in streaming handlers
  • Improved logging to use errorMessageOnly with better formatting

Dependencies & Version Updates

  • Updated all Swift package dependencies to main branch

Bug Fixes & Stability

  • Fixed guard statement order and typo in snapshot and restore operations
  • Safely unwrap streamingHandlers before use
  • Remove unnecessary [weak self] in processTermination handlers
  • Made parameter4 optional to better handle delete flag
  • Fix potential nil issues in argument execution

Architecture Improvements

  • Unified streaming architecture with simplified process lifecycle
  • Improved memory management through explicit cleanup patterns
  • Enhanced testability with extracted ParseRsyncOutput package
  • Better separation of concerns through handler factory pattern

Dependencies Updated

  • RsyncProcessStreaming (streaming process execution)
  • ParseRsyncOutput (output parsing and processing)
  • RsyncArguments (rsync command arguments)
  • RsyncProcess (legacy - being phased out in favor of streaming)

Empty stats file

The process termination signal indicates that the external rsync process has completed and the process has been terminated. All tasks within the main synchronize view are updated with the latest run, but there is also a separate logging that records the main result of each task with a timestamp.

Occasionally, when synchronizing a small amount of data, the termination signal is detected before all output from rsync has been drained. In such cases, the separate logging may be missing. The process termination signal serves as a message to perform logging, but if the last summarized rsync output is missing, there is nothing to log.

Output from rsync refers to the information that rsync provides to the terminal during the execution of a task.

If logging fails due to the aforementioned reason, this error will be generated, if enabled.

Version 2.8.2

Version 2.8.2 (build 173) - Dec 14, 2025

This version has undergone numerous linting updates. Additionally, a bug related to enabling or disabling the —delete parameter has been resolved. Furthermore, a user settings flag for enabling or disabling the Silence missing stats parameter, which generates an error if the summarized stats file is empty.

As part of my ongoing experimentation with Visual Studio Code (VSC) and the GitHub MCP services for direct integration with GitHub Copilot (AI), several code updates have been made. The majority of these updates are attributed to the implementation of additional SwiftLint rules. VSC has proven to be an effective tool for identifying and resolving linting issues and updating code.

From this version, VSC has become an integral part of my development tools for the RsyncUI project. While Xcode 26 remains my primary development environment, all release builds, including notifications and signing, are executed via command line and a Makefile.

The primary supporting tools include:

All of these tools are widely recognized and utilized by numerous developers.

The periphery application is specifically designed to eliminate unused code. SwiftFormat is utilized for code formatting, while SwiftLint ensures code quality. All instructions for SwiftFormat within the code have been removed, and both tools now share a single configuration file containing their respective instructions. This integration has proven to be highly effective.

Verify remote

Overview

This post describes how I use the Verify remote function.

The Remote Setup

I back up my bird photography to multiple locations:

  • a Raspberry Pi 5 server configured with two WD Red SA500 2.5" SSD 1TB drives set up as a mirrored ZFS pool
  • a remote cloud service (JottaCloud)
  • two 1TB NVMe external SSD drives attached to my computers

The first two locations are updated whenever data changes. JottaCloud updates are managed by a service running on my Macs. Updates to the Raspberry Pi 5 server are performed using RsyncUI.

Currently, my bird photography consists of 140GB and 8,000 files, including RAW and sidecar files. During travel, I use my MacBook Pro for photo editing, while at home, I use my Mac Mini M4. I always keep track of which Mac has been updated and stores the most recent files.

For this post, I am using my MacBook Pro as the local device, while the remote server contains more recently updated data.

Requirements for Using Verify Remote

The Procedure

Select the Pictures profile that stores my task to synchronize my local picture raw catalog with my remote server. After selecting the task, open the Verify remote function.

During the initial run, the Adjust output toggle is disabled. When enabled, the function eliminates the last 16 lines of output from both pull and push operations, as well as all other lines that are identical in both. To initiate the evaluation, select the upward arrow.

The remote server has less data than my local Mac because I deleted some photos locally that were previously synced to the remote. The output from rsync is tagged with information that is evaluated by looking at the first characters of each line. The push data in the left table indicates that my local Mac contains more data, which makes sense because the remote needs to be updated to reflect the deleted photos.

The Adjust output toggle is enabled, and a new evaluation begins.

The remote table on the right indicates that there are updates to sidecars and local data that need to be deleted.

I perform a pull of data from the remote to update my local Mac.

Following the pull, Verify remote displays that my local Mac and remote device are in sync.

Number of files

Numbers updated: December 21, 2025, version 2.8.4

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 RsyncProcessStreaming/Sources ProcessCommand/Sources
     207 text files.
     207 unique files.                                          
      15 files ignored.

github.com/AlDanial/cloc v 2.06  T=0.06 s (3336.2 files/s, 362295.4 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Swift                          201           2551           2447          17056
C                                2             36             72            254
XML                              2              0              0             53
JSON                             1              0              0              6
C/C++ Header                     1              1              3              0
-------------------------------------------------------------------------------
SUM:                           207           2588           2522          17369
-------------------------------------------------------------------------------

Main Repository

Swift Packages used by RsyncUI

All SPM packages are refactored, updated, and checked into the main branch. RsyncUI is a depended on all packages, but the last one is not mandatory. SSH keys can be generated via command line.

Swift concurrency

First, I must acknowledge that my understanding of Swift concurrency is limited. While I have a basic grasp of the subject, if you are reading this and seeking more detailed information, I strongly recommend searching for and reading articles from other sources that provide a more comprehensive explanation 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 the executors and CTP work and interact are details I don’t know about—they are managed by the Swift runtime. There are three kinds of executors:

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

Most work in RsyncUI is executed on the main thread. By default, SwiftUI ensures all UI updates are performed on the main thread. Below are some tasks within RsyncUI that are executed on the main thread:

  • preparing and executing rsync synchronization tasks (preparing involves computing the correct arguments for rsync)
  • monitoring progress and termination of the actual rsync tasks
    • monitoring progress is an asynchronous sequence running on the main thread, created by an 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 from 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 of Swift. The latest version of Swift simplifies writing asynchronous code using the async and await keywords, as well as the actor protocol. While RsyncUI doesn’t inherently require concurrency, it is automatically introduced through the use of actors, async, and await keywords. The objective is to execute most work synchronously on the main thread, provided it doesn’t 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 use 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 RsyncUI tasks are executed asynchronously on threads from the CTP, adhering to the actor protocol:

  • read synchronization tasks from file
    • JSON data decoding: asynchronous decoding that inherits the thread from the actor reading data
    • JSON data encoding: synchronous encoding on the main thread
  • read and sort log records
  • delete log records
  • preparing output from rsync for display
  • preparing data from the log file 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 of which run on the main thread as well.

Structured Concurrency

Some concurrent functions within RsyncUI are structured using async let. You may have several async let statements, and they will all execute in parallel or concurrently. 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 point where execution waits until the asynchronous function is completed before continuing. If there are several await statements in sequence, the next one will execute when the current asynchronous task is completed.

Example of structured concurrency

Approximately 800 log records, comprising the total of 1500, are selected for deletion. Two operations are concurrently executed on a background thread: the actual deletion of log records and the updating of non-deleted records for display.

The deletion of logs commences on the main thread. The user selects the logs to be deleted, and the delete button either initiates the deletion process or aborts it. The deletelogs() function, an asynchronous function, is initiated on the main thread.

Button("Delete", role: .destructive) {
      	Task {
              await deletelogs(selectedloguuids)
            }
  }

The deletelogs starts on the main thread and jumps off to an background thread inside the Task {}.

func deletelogs(_ uuids: Set<UUID>) async {
        Task {
        
            print("(1) start async let updatedRecords deletelogs")
            
            async let updatedRecords: [LogRecords]? = ActorReadLogRecordsJSON().deletelogs(
                uuids,
                logrecords: logrecords,
                profile: rsyncUIdata.profile,
                validhiddenIDs: validhiddenIDs
            )
            let records = await updatedRecords
            
            print("(2) awaited updatedRecords deletelogs from (1))")
            print("(3) start async let updatedRecords updatelogsbyhiddenID)")
            
            async let updatedLogs: [Log]? = ActorReadLogRecordsJSON().updatelogsbyhiddenID(records, hiddenID)
            logrecords = records
            logs = await (updatedLogs ?? [])
            
            print("(4) awaited updatedLogs from (3)")

            WriteLogRecordsJSON(rsyncUIdata.profile, records)
            selectedloguuids.removeAll()
        }
    }

The debug windows in Xcode display the following:

The actors also print whether they execute on the main thread. The first async let statement initiates execution, and the subsequent await statement for the result above (2) suspends the function’s execution until the asynchronous result is computed. The await statement is crucial for suspending the execution of the function until the asynchronous result is available. And then the next (3) and (4).

(1) start async let updatedRecords deletelogs
ActorReadLogRecordsJSON: deletelogs() NOT on main thread, currently on <NSThread: 0xa49e3c200>{number = 18}
ActorReadLogRecordsJSON: DEINIT
(2) awaited updatedRecords deletelogs from (1))
(3) start async let updatedRecords updatelogsbyhiddenID)
ActorReadLogRecordsJSON: updatelogsbyhiddenID() NOT on main thread, currently on <NSThread: 0xa49e3c280>{number = 17}
ActorReadLogRecordsJSON: DEINIT
(4) awaited updatedLogs from (3)
WriteLogRecordsJSON: writeJSONToPersistentStore file:///Users/thomas/.rsyncosx/VPxxxxxxxx/WDBackup/logrecords.json
WriteLogRecordsJSON DEINIT
ActorReadLogRecordsJSON: updatelogsbyhiddenID() NOT on main thread, currently on <NSThread: 0xa4bb72800>{number = 19}
ActorReadLogRecordsJSON: DEINIT

Tagging of data

Overview

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

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

Example:

  • Input string: Number of created files: 7,191 (reg: 6,846, dir: 345)
  • Converted to: [7191, 6846, 345] (the thousands separator is also removed from the string before parsing)

The function below extracts only numbers 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 rsync output is not particularly complex, though it differs somewhat between the latest version of rsync and the default versions.

Version 3.4.x

The trailing output from the latest version of rsync (version 3.4.1) looks 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)

Openrsync

The trailing output from the default version of rsync looks 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 the Tagging Works

The output from rsync is parsed and numbers are extracted. After parsing, these numbers determine whether data should be tagged for synchronization.

Latest Version of rsync

Three numbers determine whether data needs to be synchronized: the number of updates (regular files transferred), new files, and deleted files. Each can be either 0 or a positive number, and all three must be verified.

Default Versions

Only one number determines whether data needs to be synchronized: the number of updates (files transferred).