Version 4.0.0
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.NotifyWatcheris now anAsyncSequenceof change batches.watchDirectoryreturns an already-armed watcher you iterate withfor try await, replacing the old delegate/callback-queue API. - Task cancellation support -
File.read/writechunks,uploadFile/downloadFiletransfers, and recursiveremoveItemcheck for cancellation between steps and throwCancellationError. 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_contextare serialized with a process-wide lock, since they mutate libsmb2's process-wide active-context list andsrandomseed. - 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
watchDirectoryreturning before the server had registered the first notify request; it now waits for an echo round trip after arming, so changes made immediately afterwatchDirectoryreturns are no longer missed. - Fixed service loops polling an invalid descriptor forever when a context had no connection; they now fail instead, since
smb2_servicereports 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_SHUTDOWNduring 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 callbacksAfter:
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.