v1.8.4: fix FilesTab showing stale remote files on server switch

- Clear remote panel immediately when switching servers
- Add race condition guards in async SFTP connect/refresh
- Validate alias at each async callback to prevent stale UI updates
- Add switching_servers/disconnected/sftp_server_not_found i18n keys
- Properly handle connection results arriving after server switch

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-02-24 03:34:17 -05:00
parent 4b505f39d1
commit 6144c17a96
4 changed files with 82 additions and 13 deletions

View File

@@ -277,6 +277,9 @@ _EN = {
"items_count": "{count} items",
"sudo_mode": "Sudo",
"try_sudo_hint": "Try enabling Sudo mode",
"switching_servers": "Switching servers...",
"disconnected": "Disconnected",
"sftp_server_not_found": "[!] Server not found",
"uploading_dir": "Uploading folder: {name}",
"downloading_dir": "Downloading folder: {name}",
"transfer_file_progress": "File {cur}/{total}: {name}",
@@ -539,6 +542,9 @@ _RU = {
"items_count": "{count} элементов",
"sudo_mode": "Sudo",
"try_sudo_hint": "Попробуйте включить Sudo",
"switching_servers": "Переключение серверов...",
"disconnected": "Отключено",
"sftp_server_not_found": "[!] Сервер не найден",
"uploading_dir": "Загрузка папки: {name}",
"downloading_dir": "Скачивание папки: {name}",
"transfer_file_progress": "Файл {cur}/{total}: {name}",
@@ -801,6 +807,9 @@ _ZH = {
"items_count": "{count} 个项目",
"sudo_mode": "Sudo",
"try_sudo_hint": "尝试启用Sudo模式",
"switching_servers": "切换服务器...",
"disconnected": "已断开",
"sftp_server_not_found": "[!] 未找到服务器",
"uploading_dir": "上传文件夹: {name}",
"downloading_dir": "下载文件夹: {name}",
"transfer_file_progress": "文件 {cur}/{total}: {name}",

View File

@@ -283,6 +283,11 @@ class FilesTab(ctk.CTkFrame):
if self._current_alias == alias:
return
# Clear remote panel immediately to avoid showing stale files
self._remote_list.populate([])
self._remote_status.configure(text=t("switching_servers") if alias else t("connect_to_browse"))
self._set_remote_buttons_state("disabled")
self._disconnect_sftp()
self._current_alias = alias
@@ -304,22 +309,32 @@ class FilesTab(ctk.CTkFrame):
def _connect_sftp(self):
server = self.store.get_server(self._current_alias)
if not server:
self._remote_status.configure(text=t("sftp_server_not_found"))
self._set_remote_buttons_state("disabled")
return
# Capture current alias to prevent race condition when switching servers quickly
current_alias_at_call = self._current_alias
self._remote_status.configure(text=t("connecting_sftp"))
self._set_remote_buttons_state("disabled")
def _do():
# Only proceed if we're still on the same server
if self._current_alias != current_alias_at_call:
return
try:
# Use session pool if available
if self.session_pool:
sftp, is_new = self.session_pool.get_or_create_sftp_session(
self._current_alias,
current_alias_at_call, # Use the original alias to avoid race conditions
server,
self.store.get_ssh_key_path()
)
# Get stored state
stored_path, stored_sudo = self.session_pool.get_sftp_state(self._current_alias)
stored_path, stored_sudo = self.session_pool.get_sftp_state(current_alias_at_call)
# Set sudo mode before connecting if it was stored
sftp.sudo_mode = stored_sudo
@@ -337,19 +352,36 @@ class FilesTab(ctk.CTkFrame):
except:
pass # Fall back to normalized home path
self.after(0, lambda: self._on_sftp_connected(sftp, home))
# Final check before calling callback
if self._current_alias == current_alias_at_call:
self.after(0, lambda a=current_alias_at_call: self._on_sftp_connected(sftp, home, a))
else:
# Legacy behavior without session pool
sftp = SFTPSession(server, self.store.get_ssh_key_path())
sftp.connect()
home = sftp.normalize(".")
self.after(0, lambda: self._on_sftp_connected(sftp, home))
# Final check before calling callback
if self._current_alias == current_alias_at_call:
self.after(0, lambda a=current_alias_at_call: self._on_sftp_connected(sftp, home, a))
except Exception as e:
# Only show error if still on the same server
if self._current_alias == current_alias_at_call:
self.after(0, lambda: self._on_sftp_error(str(e)))
threading.Thread(target=_do, daemon=True).start()
def _on_sftp_connected(self, sftp: SFTPSession, home: str):
def _on_sftp_connected(self, sftp: SFTPSession, home: str, expected_alias: str):
# Only update UI if we're still on the same server that requested this connection
if self._current_alias != expected_alias:
# This connection result is from a server we've already switched away from
# If we're not using session pooling, disconnect this session
if not self.session_pool:
try:
sftp.disconnect()
except:
pass
return
self._sftp = sftp
# Update sudo var to match the restored session state
self._sudo_var.set(self._sftp.sudo_mode)
@@ -367,6 +399,11 @@ class FilesTab(ctk.CTkFrame):
self._log_msg(t("sftp_error").format(e=error))
def _disconnect_sftp(self):
# Clear the remote display immediately
self._remote_list.populate([])
self._remote_status.configure(text=t("disconnected"))
self._set_remote_buttons_state("disabled")
# Only disconnect if not using session pool (otherwise session stays alive)
if self._sftp and not self.session_pool:
try:
@@ -374,9 +411,8 @@ class FilesTab(ctk.CTkFrame):
except Exception:
pass
self._sftp = None
# If using session pool, remove callbacks to prevent processing data after switch
# If using session pool, just clear our reference to prevent interaction with old session
elif self._sftp and self.session_pool:
# Don't actually disconnect, just remove the reference to avoid further interaction
self._sftp = None
# ── Browse / Drive ──
@@ -478,6 +514,9 @@ class FilesTab(ctk.CTkFrame):
self._remote_path_entry.delete(0, "end")
self._remote_path_entry.insert(0, self._remote_path)
# Capture current server alias to prevent race condition when switching servers
current_alias_at_call = self._current_alias
def _list_remote():
"""Fetch remote listing, returns items list."""
if self._sftp.sudo_mode:
@@ -504,6 +543,10 @@ class FilesTab(ctk.CTkFrame):
return items
def _do():
# Check if we're still on the same server after async operations
if self._current_alias != current_alias_at_call:
return # Cancel if server has changed
# Ensure connection is alive, reconnect if needed
if not self._sftp.connected:
try:
@@ -515,19 +558,36 @@ class FilesTab(ctk.CTkFrame):
self.after(0, lambda: self._on_sftp_error("Reconnect failed"))
return
# Final check if server changed during connection attempt
if self._current_alias != current_alias_at_call:
return # Cancel if server has changed
try:
items = _list_remote()
# Final check before updating UI
if self._current_alias == current_alias_at_call:
self.after(0, lambda: self._populate_remote(items))
except PermissionError as e:
if self._current_alias == current_alias_at_call:
hint = f"\n{t('try_sudo_hint')}" if not self._sftp.sudo_mode else ""
self.after(0, lambda: self._on_sftp_error(str(e) + hint))
except Exception:
# Final check before attempting reconnect
if self._current_alias != current_alias_at_call:
return # Cancel if server has changed
# Operation failed — one reconnect attempt
try:
self._sftp.reconnect()
# Final check after reconnection attempt
if self._current_alias != current_alias_at_call:
return # Cancel if server has changed
items = _list_remote()
self.after(0, lambda: self._populate_remote(items))
except Exception as e2:
if self._current_alias == current_alias_at_call:
self.after(0, lambda: self._on_sftp_error(str(e2)))
threading.Thread(target=_do, daemon=True).start()

Binary file not shown.

View File

@@ -1,6 +1,6 @@
"""Version info for ServerManager."""
__version__ = "1.8.3"
__version__ = "1.8.4"
__app_name__ = "ServerManager"
__author__ = "aibot777"
__description__ = "Desktop GUI for managing remote servers"