- Multiple managers (
ServerManager,ThumbnailManagerRemote,CreateTorrent) maintain separate connection pools - Connections can become stale or invalid without proper detection
- No centralized validation of connection health
- Risk of reusing connections to wrong servers or in invalid states
- Heavy use of
await withCheckedContinuationpatterns that can deadlock - Multiple locks and async operations that increase complexity
- Connection state scattered across multiple variables
- Timeout-based reconnection logic that can fail
- When connections fail, cleanup isn't always complete
- Dangling references to broken connections
- Race conditions in connection establishment
- Connection pools accessed from multiple threads without proper synchronization
- State changes not properly coordinated between different managers
- Simplified State Management: No need to track connection state across operations
- Guaranteed Cleanup: Connections are always closed when operations complete
- No Stale Connections: Fresh connection for each operation eliminates reuse issues
- Better Error Recovery: Failed connections don't affect future operations
- Thread Safety: No shared state to coordinate between threads
- Removed complex timeout and reconnection logic
- Eliminated async continuation patterns that could deadlock
- Simplified connect/disconnect methods
- Removed automatic registration with connection manager
- Added static methods for common operations (executeCommand, downloadFile, etc.)
- Each method creates fresh connection, performs operation, and cleans up automatically
- Added
SafeSSHManageractor for cases requiring multiple operations
- Removed connection pooling
- Added wrapper methods that use the safe helper functions
// BAD: Persistent connection that can become stale
@State private var sshConnection: SSHConnection?
func someOperation() async {
if sshConnection == nil {
sshConnection = SSHConnection(server: server)
}
try await sshConnection?.connect()
// ... use connection ...
// Connection may not be properly cleaned up
}// GOOD: Create-and-destroy pattern
func someOperation() async throws {
try await SSHConnection.withConnection(server: server) { connection in
try await connection.connect()
// ... use connection ...
// Automatic cleanup guaranteed
}
}
// OR even better, use helper methods:
func someOperation() async throws {
let result = try await SSHConnection.executeCommand(on: server, command: "ls -la")
// Connection automatically created, used, and destroyed
}// GOOD: Use SafeSSHManager for multiple operations
let manager = SafeSSHManager(server: server)
defer { await manager.cleanup() }
try await manager.connect()
try await manager.executeCommand("command1")
try await manager.executeCommand("command2")
// Cleanup guaranteed by deferCreateTorrent.swift- Remove persistentsshConnectionpropertyThumbnailManagerRemote.swift- Remove connection pooling- Any other views/managers that cache SSH connections
- Network Overhead: Creating new connections has some overhead
- Mitigation: For operations requiring multiple commands, use
SafeSSHManagerorwithConnection - Trade-off: Slight performance cost for significantly improved reliability and safety
- Test app backgrounding/foregrounding cycles
- Test network connectivity changes
- Test rapid connection requests
- Test server configuration changes
- Test memory pressure scenarios
The create-and-destroy pattern eliminates the root causes of your SSH connection crashes:
- No reuse of stale connections
- No connections to wrong servers
- Proper cleanup on all failure paths
- Simplified state management
This approach prioritizes reliability over minor performance optimizations, which is the right choice for a production app where crashes are unacceptable.