Skip to content

Version 4.0.0

Choose a tag to compare

@RuiNelson RuiNelson released this 15 Sep 18:26
· 4 commits to main since this release
491612a

What's New

  • Async API with per-connection serialization - Every operation that reaches the server, including the internal bridge layer, is now async. libsmb2 work runs on a serial queue owned by each connection's context and resumes callers through checked continuations, so blocking libsmb2 calls never occupy the Swift concurrency thread pool. Operations on one connection stay ordered; separate connections now run in parallel instead of sharing one global queue.
  • Directory watching as AsyncSequence - SMB.NotifyWatcher is now an AsyncSequence of change batches. watchDirectory returns an already-armed watcher you iterate with for try await, replacing the old delegate/callback-queue API.
  • Task cancellation support - File.read/write chunks, uploadFile/downloadFile transfers, and recursive removeItem check for cancellation between steps and throw CancellationError. Chunks already written before cancellation stay written.
  • Faster, safer connects - Connecting now uses a 30-second deadline when the command timeout is 0, since libsmb2 otherwise fails any connect whose TCP handshake crosses a wall-clock second boundary — a failure that became likely once concurrent connects were possible. smb2_init_context/smb2_destroy_context are serialized with a process-wide lock, since they mutate libsmb2's process-wide active-context list and srandom seed.
  • The negotiated maximum read and write sizes are now cached at connect time instead of re-queried for every transfer block.

Bug Fixes

  • Fixed the notify watcher holding its connection's queue while idle, which delayed every other operation on that connection (a 40-echo benchmark went from 1.97 s to 0.007 s after the fix).
  • Fixed watchDirectory returning before the server had registered the first notify request; it now waits for an echo round trip after arming, so changes made immediately after watchDirectory returns are no longer missed.
  • Fixed service loops polling an invalid descriptor forever when a context had no connection; they now fail instead, since smb2_service reports success without a socket.
  • Fixed a window during disconnect() where another operation could run on a half-torn-down context; disconnect, close, and destroy now happen in one queue operation.
  • Fixed a potential use of freed memory when libsmb2 invokes a queued command's callback after the caller stopped waiting (for example, SMB2_STATUS_SHUTDOWN during destroy after a network error).

Migration Guide

The synchronous public API is removed. Every operation that talks to the server is async, including throwing properties such as serverGUID and maxReadSize. SMB.parseURL stays synchronous, since it doesn't touch the network.

Before:

let shares = try SMB.listShares(server: server, credentials: credentials)

let connection = try SMB.connect(server: server, credentials: credentials, share: "Documents")
defer { try? connection.disconnect() }

let serverID: UUID = try connection.serverGUID
let entries = try connection.listDirectory(at: "Anna/Inbox")
try connection.uploadFile(local: localURL, remote: "Anna/Inbox/report.pdf") { completed, total, lastBlockSpeed, averageSpeed in
    true
}

After:

let shares = try await SMB.listShares(server: server, credentials: credentials)

let connection = try await SMB.connect(server: server, credentials: credentials, share: "Documents")
defer { try? await connection.disconnect() }

let serverID: UUID = try await connection.serverGUID
let entries = try await connection.listDirectory(at: "Anna/Inbox")
try await connection.uploadFile(local: localURL, remote: "Anna/Inbox/report.pdf") { completed, total, lastBlockSpeed, averageSpeed in
    true
}

Operations on one connection now run one at a time, in the order they were requested. Open separate connections if you need operations to run in parallel.

SMB.NotifyWatcherDelegate and the callbackQueue parameter on watchDirectory are removed. Iterate the watcher directly instead:

Before:

let watcher = try connection.watchDirectory(at: "Inbox", delegate: self, callbackQueue: .main)
// changes reported through NotifyWatcherDelegate callbacks

After:

let watcher = try await connection.watchDirectory(at: "Inbox")
for try await changes in watcher {
    for change in changes {
        print(change.action, change.name)
    }
}

The minimum Swift toolchain is now 6.4 (previously 6.2/6.3), required for await inside defer bodies used throughout the connection lifecycle code.