Skip to content

Commit 3e3d28e

Browse files
committed
fix: prevent UI freeze when keychain shows permission dialog
Keychain access on macOS can block indefinitely when the system shows a permission dialog. This causes the app to freeze completely. Added get_password_with_timeout() method that spawns keychain access in a separate thread with a 500ms timeout. If timeout occurs, returns None to trigger the password prompt instead of hanging.
1 parent cbaa128 commit 3e3d28e

File tree

2 files changed

+63
-1
lines changed

2 files changed

+63
-1
lines changed

crates/tsql/src/app/app.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4375,7 +4375,8 @@ impl App {
43754375
/// Connect to a saved connection entry.
43764376
pub fn connect_to_entry(&mut self, entry: ConnectionEntry) {
43774377
// Try to get the password from keychain or env var
4378-
let password = match entry.get_password() {
4378+
// Use timeout to avoid blocking UI if keychain shows permission dialog
4379+
let password = match entry.get_password_with_timeout(500) {
43794380
Ok(Some(pwd)) => Some(pwd),
43804381
Ok(None) => None,
43814382
Err(e) => {

crates/tsql/src/config/connections.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,67 @@ impl ConnectionEntry {
301301
Ok(None)
302302
}
303303

304+
/// Get the password with a timeout to avoid blocking the UI.
305+
///
306+
/// On macOS, keychain access can block indefinitely if the system
307+
/// shows a permission dialog. This method spawns the keychain access
308+
/// in a separate thread with a short timeout to prevent UI freezes.
309+
///
310+
/// Returns:
311+
/// - Ok(Some(password)) if password was retrieved
312+
/// - Ok(None) if no password available or timeout occurred
313+
/// - Err if there was an error (other than timeout/no entry)
314+
pub fn get_password_with_timeout(&self, timeout_ms: u64) -> Result<Option<String>> {
315+
use std::sync::mpsc;
316+
use std::thread;
317+
use std::time::Duration;
318+
319+
// Try environment variable first (no blocking risk)
320+
if let Some(ref env_var) = self.password_env {
321+
let var_name = env_var.strip_prefix('$').unwrap_or(env_var);
322+
if let Ok(pwd) = std::env::var(var_name) {
323+
return Ok(Some(pwd));
324+
}
325+
}
326+
327+
// If not configured for keychain, return None
328+
if !self.password_in_keychain {
329+
return Ok(None);
330+
}
331+
332+
// Spawn keychain access in a separate thread with timeout
333+
let name = self.name.clone();
334+
let (tx, rx) = mpsc::channel();
335+
336+
thread::spawn(move || {
337+
let result = (|| -> Result<Option<String>> {
338+
let entry = keyring::Entry::new(KEYRING_SERVICE, &name)
339+
.context("Failed to create keyring entry")?;
340+
341+
match entry.get_password() {
342+
Ok(password) => Ok(Some(password)),
343+
Err(keyring::Error::NoEntry) => Ok(None),
344+
Err(e) => Err(anyhow!("Failed to get password from keychain: {}", e)),
345+
}
346+
})();
347+
let _ = tx.send(result);
348+
});
349+
350+
// Wait for result with timeout
351+
match rx.recv_timeout(Duration::from_millis(timeout_ms)) {
352+
Ok(result) => result,
353+
Err(mpsc::RecvTimeoutError::Timeout) => {
354+
// Keychain access is blocked (probably showing system dialog)
355+
// Return None to trigger password prompt
356+
Ok(None)
357+
}
358+
Err(mpsc::RecvTimeoutError::Disconnected) => {
359+
// Thread panicked or was killed
360+
Ok(None)
361+
}
362+
}
363+
}
364+
304365
/// Format connection for display (without password)
305366
pub fn display_string(&self) -> String {
306367
format!(

0 commit comments

Comments
 (0)