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:
@@ -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}",
|
||||
|
||||
@@ -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()
|
||||
|
||||
BIN
releases/ServerManager-v1.8.4-win-x64.exe
Normal file
BIN
releases/ServerManager-v1.8.4-win-x64.exe
Normal file
Binary file not shown.
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user