v1.8.2: session management hardening
- Fix infinite reconnect loop in terminal (_send_to_shell guard) - Safe language switch: disconnect + reconnect instead of object transplant - Improved SFTP reconnect flow with proper validation - Add log.debug to all silent exception handlers in ssh_client Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -163,14 +163,14 @@ class ShellSession:
|
|||||||
if self._channel:
|
if self._channel:
|
||||||
try:
|
try:
|
||||||
self._channel.close()
|
self._channel.close()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
log.debug(f"ShellSession channel close: {e}")
|
||||||
self._channel = None
|
self._channel = None
|
||||||
if self._client:
|
if self._client:
|
||||||
try:
|
try:
|
||||||
self._client.close()
|
self._client.close()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
log.debug(f"ShellSession client close: {e}")
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
def reconnect(self):
|
def reconnect(self):
|
||||||
@@ -353,14 +353,14 @@ class SFTPSession:
|
|||||||
if self._sftp:
|
if self._sftp:
|
||||||
try:
|
try:
|
||||||
self._sftp.close()
|
self._sftp.close()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
log.debug(f"SFTPSession sftp close: {e}")
|
||||||
self._sftp = None
|
self._sftp = None
|
||||||
if self._client:
|
if self._client:
|
||||||
try:
|
try:
|
||||||
self._client.close()
|
self._client.close()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
log.debug(f"SFTPSession client close: {e}")
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
def listdir_attr(self, path: str) -> list:
|
def listdir_attr(self, path: str) -> list:
|
||||||
|
|||||||
33
gui/app.py
33
gui/app.py
@@ -171,15 +171,14 @@ class App(ctk.CTk):
|
|||||||
# Use provided key or default to first tab
|
# Use provided key or default to first tab
|
||||||
current_key = restore_tab_key or self._tab_keys[0]
|
current_key = restore_tab_key or self._tab_keys[0]
|
||||||
|
|
||||||
# Save live SFTP session before destroying tabs
|
# Save state before destroying tabs
|
||||||
saved_sftp = self.files_tab._sftp
|
saved_remote_path = self.files_tab._remote_path
|
||||||
saved_sftp_path = self.files_tab._remote_path
|
|
||||||
saved_sftp_alias = self.files_tab._current_alias
|
|
||||||
saved_local_path = self.files_tab._local_path
|
saved_local_path = self.files_tab._local_path
|
||||||
self.files_tab._sftp = None # Prevent disconnect on destroy
|
had_sftp = self.files_tab._sftp is not None and self.files_tab._sftp.connected
|
||||||
|
|
||||||
# Disconnect terminal before destroying tabs
|
# Disconnect terminal and SFTP before destroying tabs
|
||||||
self.terminal_tab._disconnect()
|
self.terminal_tab._disconnect()
|
||||||
|
self.files_tab._disconnect_sftp()
|
||||||
|
|
||||||
# Detach tab contents
|
# Detach tab contents
|
||||||
self.terminal_tab.pack_forget()
|
self.terminal_tab.pack_forget()
|
||||||
@@ -225,23 +224,17 @@ class App(ctk.CTk):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Restore SFTP session without reconnecting
|
# Restore file paths and reconnect properly
|
||||||
if saved_sftp and saved_sftp.connected:
|
self.files_tab._local_path = saved_local_path
|
||||||
self.files_tab._sftp = saved_sftp
|
self.files_tab._refresh_local()
|
||||||
self.files_tab._current_alias = saved_sftp_alias
|
if alias and had_sftp:
|
||||||
self.files_tab._remote_path = saved_sftp_path
|
# Had active SFTP — reconnect and restore remote path
|
||||||
self.files_tab._local_path = saved_local_path
|
self.files_tab._remote_path = saved_remote_path
|
||||||
self.files_tab._set_remote_buttons_state("normal")
|
self.files_tab.set_server(alias)
|
||||||
self.files_tab._remote_status.configure(
|
|
||||||
text=t("connected_sftp").format(alias=saved_sftp_alias)
|
|
||||||
)
|
|
||||||
self.files_tab._refresh_local()
|
|
||||||
self.files_tab._refresh_remote()
|
|
||||||
elif alias:
|
elif alias:
|
||||||
# No live session — reconnect via server selection
|
|
||||||
self.files_tab.set_server(alias)
|
self.files_tab.set_server(alias)
|
||||||
|
|
||||||
# Restore server selection for other tabs
|
# Restore server selection for other tabs (terminal auto-reconnects)
|
||||||
if alias:
|
if alias:
|
||||||
self.terminal_tab.set_server(alias)
|
self.terminal_tab.set_server(alias)
|
||||||
self.info_tab.set_server(alias)
|
self.info_tab.set_server(alias)
|
||||||
|
|||||||
@@ -450,16 +450,25 @@ class FilesTab(ctk.CTkFrame):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
def _do():
|
def _do():
|
||||||
try:
|
# Ensure connection is alive, reconnect if needed
|
||||||
if not self._sftp.connected:
|
if not self._sftp.connected:
|
||||||
|
try:
|
||||||
self._sftp.reconnect()
|
self._sftp.reconnect()
|
||||||
|
except Exception as e:
|
||||||
|
self.after(0, lambda: self._on_sftp_error(str(e)))
|
||||||
|
return
|
||||||
|
if not self._sftp.connected:
|
||||||
|
self.after(0, lambda: self._on_sftp_error("Reconnect failed"))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
items = _list_remote()
|
items = _list_remote()
|
||||||
self.after(0, lambda: self._populate_remote(items))
|
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 ""
|
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))
|
self.after(0, lambda: self._on_sftp_error(str(e) + hint))
|
||||||
except Exception:
|
except Exception:
|
||||||
# Connection likely dead — try reconnect once
|
# Operation failed — one reconnect attempt
|
||||||
try:
|
try:
|
||||||
self._sftp.reconnect()
|
self._sftp.reconnect()
|
||||||
items = _list_remote()
|
items = _list_remote()
|
||||||
|
|||||||
@@ -142,9 +142,8 @@ class TerminalTab(ctk.CTkFrame):
|
|||||||
session = self._session # local ref for thread safety
|
session = self._session # local ref for thread safety
|
||||||
if session and session.connected:
|
if session and session.connected:
|
||||||
session.send(data)
|
session.send(data)
|
||||||
elif self._current_alias and not self._intentional_disconnect:
|
elif self._current_alias and not self._intentional_disconnect and self._reconnect_count == 0:
|
||||||
# Session dead — trigger reconnect
|
# Session dead, no reconnect in progress — trigger one attempt
|
||||||
self._reconnect_count = 0
|
|
||||||
self._on_disconnected()
|
self._on_disconnected()
|
||||||
|
|
||||||
def _on_resize(self, cols: int, rows: int):
|
def _on_resize(self, cols: int, rows: int):
|
||||||
|
|||||||
BIN
releases/ServerManager-v1.8.2-win-x64.exe
Normal file
BIN
releases/ServerManager-v1.8.2-win-x64.exe
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
"""Version info for ServerManager."""
|
"""Version info for ServerManager."""
|
||||||
|
|
||||||
__version__ = "1.8.1"
|
__version__ = "1.8.2"
|
||||||
__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