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

View File

@@ -283,6 +283,11 @@ class FilesTab(ctk.CTkFrame):
if self._current_alias == alias: if self._current_alias == alias:
return 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._disconnect_sftp()
self._current_alias = alias self._current_alias = alias
@@ -304,22 +309,32 @@ class FilesTab(ctk.CTkFrame):
def _connect_sftp(self): def _connect_sftp(self):
server = self.store.get_server(self._current_alias) server = self.store.get_server(self._current_alias)
if not server: if not server:
self._remote_status.configure(text=t("sftp_server_not_found"))
self._set_remote_buttons_state("disabled")
return 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._remote_status.configure(text=t("connecting_sftp"))
self._set_remote_buttons_state("disabled") self._set_remote_buttons_state("disabled")
def _do(): def _do():
# Only proceed if we're still on the same server
if self._current_alias != current_alias_at_call:
return
try: try:
# Use session pool if available # Use session pool if available
if self.session_pool: if self.session_pool:
sftp, is_new = self.session_pool.get_or_create_sftp_session( 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, server,
self.store.get_ssh_key_path() self.store.get_ssh_key_path()
) )
# Get stored state # 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 # Set sudo mode before connecting if it was stored
sftp.sudo_mode = stored_sudo sftp.sudo_mode = stored_sudo
@@ -337,19 +352,36 @@ class FilesTab(ctk.CTkFrame):
except: except:
pass # Fall back to normalized home path 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: else:
# Legacy behavior without session pool # Legacy behavior without session pool
sftp = SFTPSession(server, self.store.get_ssh_key_path()) sftp = SFTPSession(server, self.store.get_ssh_key_path())
sftp.connect() sftp.connect()
home = sftp.normalize(".") 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: except Exception as e:
self.after(0, lambda: self._on_sftp_error(str(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() 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 self._sftp = sftp
# Update sudo var to match the restored session state # Update sudo var to match the restored session state
self._sudo_var.set(self._sftp.sudo_mode) 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)) self._log_msg(t("sftp_error").format(e=error))
def _disconnect_sftp(self): 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) # Only disconnect if not using session pool (otherwise session stays alive)
if self._sftp and not self.session_pool: if self._sftp and not self.session_pool:
try: try:
@@ -374,9 +411,8 @@ class FilesTab(ctk.CTkFrame):
except Exception: except Exception:
pass pass
self._sftp = None 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: elif self._sftp and self.session_pool:
# Don't actually disconnect, just remove the reference to avoid further interaction
self._sftp = None self._sftp = None
# ── Browse / Drive ── # ── Browse / Drive ──
@@ -478,6 +514,9 @@ class FilesTab(ctk.CTkFrame):
self._remote_path_entry.delete(0, "end") self._remote_path_entry.delete(0, "end")
self._remote_path_entry.insert(0, self._remote_path) 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(): def _list_remote():
"""Fetch remote listing, returns items list.""" """Fetch remote listing, returns items list."""
if self._sftp.sudo_mode: if self._sftp.sudo_mode:
@@ -504,6 +543,10 @@ class FilesTab(ctk.CTkFrame):
return items return items
def _do(): 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 # Ensure connection is alive, reconnect if needed
if not self._sftp.connected: if not self._sftp.connected:
try: try:
@@ -515,20 +558,37 @@ class FilesTab(ctk.CTkFrame):
self.after(0, lambda: self._on_sftp_error("Reconnect failed")) self.after(0, lambda: self._on_sftp_error("Reconnect failed"))
return return
# Final check if server changed during connection attempt
if self._current_alias != current_alias_at_call:
return # Cancel if server has changed
try: try:
items = _list_remote() items = _list_remote()
self.after(0, lambda: self._populate_remote(items)) # 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: except PermissionError as e:
hint = f"\n{t('try_sudo_hint')}" if not self._sftp.sudo_mode else "" if self._current_alias == current_alias_at_call:
self.after(0, lambda: self._on_sftp_error(str(e) + hint)) 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: 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 # Operation failed — one reconnect attempt
try: try:
self._sftp.reconnect() 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() items = _list_remote()
self.after(0, lambda: self._populate_remote(items)) self.after(0, lambda: self._populate_remote(items))
except Exception as e2: except Exception as e2:
self.after(0, lambda: self._on_sftp_error(str(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() threading.Thread(target=_do, daemon=True).start()

Binary file not shown.

View File

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