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",
|
"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}",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
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 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user