connection updates
This commit is contained in:
@@ -683,6 +683,7 @@ final class DatabaseModel: ObservableObject {
|
|||||||
saveActiveQuery()
|
saveActiveQuery()
|
||||||
if let oldID = selectedID { NativeMySQLRunner.disconnect(profileID: oldID) }
|
if let oldID = selectedID { NativeMySQLRunner.disconnect(profileID: oldID) }
|
||||||
result = QueryResult()
|
result = QueryResult()
|
||||||
|
isRunning = false
|
||||||
selectedID = id
|
selectedID = id
|
||||||
UserDefaults.standard.set(id.uuidString, forKey: "DBSyn.selectedConnectionID")
|
UserDefaults.standard.set(id.uuidString, forKey: "DBSyn.selectedConnectionID")
|
||||||
selectedDatabase = profile.database
|
selectedDatabase = profile.database
|
||||||
@@ -887,7 +888,7 @@ final class DatabaseModel: ObservableObject {
|
|||||||
introspect(database: usedSchema)
|
introspect(database: usedSchema)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
guard selectedID == profile.id else { isRunning = false; return }
|
guard selectedID == profile.id else { return }
|
||||||
if listingDatabases {
|
if listingDatabases {
|
||||||
databases = lastResult.rows.compactMap { $0.first }
|
databases = lastResult.rows.compactMap { $0.first }
|
||||||
status = "Connected to \(profile.name)"
|
status = "Connected to \(profile.name)"
|
||||||
@@ -914,7 +915,7 @@ final class DatabaseModel: ObservableObject {
|
|||||||
if !listingDatabases {
|
if !listingDatabases {
|
||||||
queryDurationText = String(format: "%.3f s", Date().timeIntervalSince(queryStarted))
|
queryDurationText = String(format: "%.3f s", Date().timeIntervalSince(queryStarted))
|
||||||
}
|
}
|
||||||
isRunning = false
|
if selectedID == profile.id { isRunning = false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+79
-30
@@ -437,40 +437,78 @@ private final class PersistentMySQLSessions {
|
|||||||
let connection: MySQLWireConnection
|
let connection: MySQLWireConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
private let queue = DispatchQueue(label: "DBSyn.MySQLSessions")
|
// Each profile has its own serial executor. An unreachable server cannot
|
||||||
private var connections: [UUID: Entry] = [:]
|
// block requests to any other saved connection.
|
||||||
|
private final class SessionSlot {
|
||||||
|
let queue: DispatchQueue
|
||||||
|
var entry: Entry?
|
||||||
|
// Only accessed on queue except generation, which is lock protected.
|
||||||
|
private let generationLock = NSLock()
|
||||||
|
private var currentGeneration: UInt64 = 0
|
||||||
|
|
||||||
|
init(id: UUID) {
|
||||||
|
queue = DispatchQueue(label: "DBSyn.MySQLSession.\(id.uuidString)")
|
||||||
|
}
|
||||||
|
func generation() -> UInt64 {
|
||||||
|
generationLock.lock(); defer { generationLock.unlock() }
|
||||||
|
return currentGeneration
|
||||||
|
}
|
||||||
|
func invalidate() {
|
||||||
|
generationLock.lock(); currentGeneration &+= 1; generationLock.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let slotsLock = NSLock()
|
||||||
|
private var slots: [UUID: SessionSlot] = [:]
|
||||||
|
|
||||||
|
private func slot(for id: UUID) -> SessionSlot {
|
||||||
|
slotsLock.lock(); defer { slotsLock.unlock() }
|
||||||
|
if let existing = slots[id] { return existing }
|
||||||
|
let created = SessionSlot(id: id)
|
||||||
|
slots[id] = created
|
||||||
|
return created
|
||||||
|
}
|
||||||
|
|
||||||
func execute(profile: ConnectionProfile, database: String,
|
func execute(profile: ConnectionProfile, database: String,
|
||||||
password: String, sql: String) throws -> QueryResult {
|
password: String, sql: String) throws -> QueryResult {
|
||||||
try queue.sync {
|
let session = slot(for: profile.id)
|
||||||
// A changed connection profile or password requires a new session.
|
let expectedGeneration = session.generation()
|
||||||
if let previous = connections[profile.id],
|
return try session.queue.sync {
|
||||||
(previous.profile != profile || previous.password != password) {
|
guard session.generation() == expectedGeneration else {
|
||||||
connections.removeValue(forKey: profile.id)
|
throw MySQLWireError(message: "Connection request superseded")
|
||||||
}
|
}
|
||||||
|
if let previous = session.entry,
|
||||||
if connections[profile.id] == nil {
|
(previous.profile != profile || previous.password != password) {
|
||||||
|
session.entry = nil
|
||||||
|
DBSynSSHTunnels.shared.stop(profile.id)
|
||||||
|
}
|
||||||
|
if session.entry == nil {
|
||||||
let endpoint = try DBSynSSHTunnels.shared.endpoint(for: profile)
|
let endpoint = try DBSynSSHTunnels.shared.endpoint(for: profile)
|
||||||
let connection = try MySQLWireConnection(host: endpoint.0, port: endpoint.1, requireTLS: !profile.useSSHTunnel && profile.useTLS && !["127.0.0.1", "localhost", "::1"].contains(endpoint.0.lowercased()))
|
let connection = try MySQLWireConnection(
|
||||||
|
host: endpoint.0, port: endpoint.1,
|
||||||
|
requireTLS: !profile.useSSHTunnel && profile.useTLS &&
|
||||||
|
!["127.0.0.1", "localhost", "::1"].contains(endpoint.0.lowercased())
|
||||||
|
)
|
||||||
connection.markHost(endpoint.0)
|
connection.markHost(endpoint.0)
|
||||||
try connection.authenticate(user: profile.username,
|
try connection.authenticate(user: profile.username,
|
||||||
password: password, database: database)
|
password: password, database: database)
|
||||||
connections[profile.id] = Entry(profile: profile,
|
guard session.generation() == expectedGeneration else {
|
||||||
password: password,
|
throw MySQLWireError(message: "Connection request superseded")
|
||||||
connection: connection)
|
}
|
||||||
|
session.entry = Entry(profile: profile, password: password,
|
||||||
|
connection: connection)
|
||||||
}
|
}
|
||||||
|
guard let entry = session.entry else {
|
||||||
guard let entry = connections[profile.id] else {
|
|
||||||
throw MySQLWireError(message: "MySQL session was not created")
|
throw MySQLWireError(message: "MySQL session was not created")
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
// Crucially, no new authentication takes place here. USE,
|
let result = try entry.connection.query(sql)
|
||||||
// session variables and transactions survive between queries.
|
guard session.generation() == expectedGeneration else {
|
||||||
return try entry.connection.query(sql)
|
throw MySQLWireError(message: "Connection request superseded")
|
||||||
|
}
|
||||||
|
return result
|
||||||
} catch {
|
} catch {
|
||||||
// The wire protocol can be out of sync after a failed query.
|
session.entry = nil
|
||||||
// Discard it rather than silently issuing commands on a bad socket.
|
|
||||||
connections.removeValue(forKey: profile.id)
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -479,23 +517,36 @@ private final class PersistentMySQLSessions {
|
|||||||
func stream(profile: ConnectionProfile, database: String,
|
func stream(profile: ConnectionProfile, database: String,
|
||||||
password: String, sql: String,
|
password: String, sql: String,
|
||||||
onRow: (String) throws -> Void) throws -> Int64 {
|
onRow: (String) throws -> Void) throws -> Int64 {
|
||||||
try queue.sync {
|
let session = slot(for: profile.id)
|
||||||
guard let entry = connections[profile.id],
|
let expectedGeneration = session.generation()
|
||||||
|
return try session.queue.sync {
|
||||||
|
guard session.generation() == expectedGeneration,
|
||||||
|
let entry = session.entry,
|
||||||
entry.profile == profile, entry.password == password else {
|
entry.profile == profile, entry.password == password else {
|
||||||
throw MySQLWireError(message: "Export session is no longer connected")
|
throw MySQLWireError(message: "Export session is no longer connected")
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
return try entry.connection.streamFirstColumn(sql, onRow: onRow)
|
return try entry.connection.streamFirstColumn(sql, onRow: { row in
|
||||||
|
guard session.generation() == expectedGeneration else {
|
||||||
|
throw MySQLWireError(message: "Export cancelled: connection switched")
|
||||||
|
}
|
||||||
|
try onRow(row)
|
||||||
|
})
|
||||||
} catch {
|
} catch {
|
||||||
connections.removeValue(forKey: profile.id)
|
session.entry = nil
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnect(profileID: UUID) {
|
func disconnect(profileID: UUID) {
|
||||||
queue.sync { _ = connections.removeValue(forKey: profileID) }
|
let session = slot(for: profileID)
|
||||||
DBSynSSHTunnels.shared.stop(profileID)
|
// Invalidate immediately without waiting for a blocked network read.
|
||||||
|
session.invalidate()
|
||||||
|
session.queue.async {
|
||||||
|
session.entry = nil
|
||||||
|
DBSynSSHTunnels.shared.stop(profileID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,8 +577,6 @@ enum NativeMySQLRunner {
|
|||||||
// Connection shutdown may wait behind a slow network operation on the
|
// Connection shutdown may wait behind a slow network operation on the
|
||||||
// session's serial queue. Never make the SwiftUI main thread wait for it.
|
// session's serial queue. Never make the SwiftUI main thread wait for it.
|
||||||
static func disconnect(profileID: UUID) {
|
static func disconnect(profileID: UUID) {
|
||||||
Task.detached(priority: .utility) {
|
PersistentMySQLSessions.shared.disconnect(profileID: profileID)
|
||||||
PersistentMySQLSessions.shared.disconnect(profileID: profileID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user