Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
289ce65431 | ||
|
|
704ce3bef2 | ||
|
|
00f3b76d2a | ||
|
|
efbbfa13ee | ||
|
|
3c4d02c5ec |
88
build.py
88
build.py
@@ -182,8 +182,9 @@ def build():
|
||||
# Auto-deploy: sync shared files so Claude Code always has the latest
|
||||
deploy_shared_files()
|
||||
|
||||
# Publish release to Gitea
|
||||
# Publish release to Gitea + cleanup old remote releases
|
||||
publish_gitea_release(dst)
|
||||
cleanup_gitea_releases()
|
||||
|
||||
|
||||
def _get_gitea_auth() -> dict:
|
||||
@@ -311,6 +312,78 @@ def _version_key(path: str):
|
||||
return (0, 0, 0)
|
||||
|
||||
|
||||
def _tag_version_key(tag_name: str):
|
||||
"""Extract (major, minor, patch) from tag like 'v1.9.5'."""
|
||||
m = re.match(r'v(\d+)\.(\d+)\.(\d+)', tag_name)
|
||||
if m:
|
||||
return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
|
||||
return (0, 0, 0)
|
||||
|
||||
|
||||
def cleanup_gitea_releases():
|
||||
"""Keep the first release (v1.0.0) and the last 5 on Gitea, delete the rest + orphan tags."""
|
||||
auth = _get_gitea_auth()
|
||||
if not auth:
|
||||
return
|
||||
|
||||
# --- Clean releases ---
|
||||
try:
|
||||
req = urllib.request.Request(f"{_GITEA_API}/releases?limit=50", headers=auth)
|
||||
resp = urllib.request.urlopen(req, timeout=30)
|
||||
releases = json.loads(resp.read())
|
||||
except Exception as e:
|
||||
print(f"Gitea release list failed: {e}")
|
||||
return
|
||||
|
||||
keep_tags = set()
|
||||
if len(releases) > 6:
|
||||
releases.sort(key=lambda r: _tag_version_key(r.get("tag_name", "")))
|
||||
first = releases[0]
|
||||
last_5 = releases[-5:]
|
||||
keep_ids = {first["id"]} | {r["id"] for r in last_5}
|
||||
keep_tags = {first.get("tag_name")} | {r.get("tag_name") for r in last_5}
|
||||
|
||||
removed = []
|
||||
for r in releases:
|
||||
if r["id"] in keep_ids:
|
||||
continue
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{_GITEA_API}/releases/{r['id']}", headers=auth, method="DELETE")
|
||||
urllib.request.urlopen(req, timeout=15)
|
||||
removed.append(r.get("tag_name", "?"))
|
||||
except Exception as e:
|
||||
print(f"Failed to delete Gitea release {r.get('tag_name')}: {e}")
|
||||
if removed:
|
||||
print(f"Cleaned {len(removed)} old Gitea releases: {', '.join(removed)}")
|
||||
else:
|
||||
keep_tags = {r.get("tag_name") for r in releases}
|
||||
|
||||
# --- Clean orphan tags (tags without releases) ---
|
||||
try:
|
||||
req = urllib.request.Request(f"{_GITEA_API}/tags?limit=50", headers=auth)
|
||||
resp = urllib.request.urlopen(req, timeout=30)
|
||||
tags = json.loads(resp.read())
|
||||
except Exception:
|
||||
return
|
||||
|
||||
removed_tags = []
|
||||
for tag in tags:
|
||||
name = tag.get("name", "")
|
||||
if name in keep_tags:
|
||||
continue
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{_GITEA_API}/tags/{name}", headers=auth, method="DELETE")
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
removed_tags.append(name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if removed_tags:
|
||||
print(f"Cleaned {len(removed_tags)} orphan Gitea tags: {', '.join(removed_tags)}")
|
||||
|
||||
|
||||
def cleanup_old_releases():
|
||||
"""Keep the first release (v1.0.0) and the last 5 releases, delete the rest."""
|
||||
import glob
|
||||
@@ -327,9 +400,20 @@ def cleanup_old_releases():
|
||||
keep = set([first] + last_5)
|
||||
|
||||
removed = []
|
||||
_flags = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
|
||||
for f in all_exes:
|
||||
if f not in keep:
|
||||
os.remove(f)
|
||||
# Use git rm so deletion is staged for commit
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "rm", "-f", "--quiet", f],
|
||||
cwd=PROJECT_DIR, creationflags=_flags,
|
||||
capture_output=True,
|
||||
)
|
||||
except Exception:
|
||||
# Fallback: just delete the file
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
removed.append(os.path.basename(f))
|
||||
|
||||
if removed:
|
||||
|
||||
@@ -9,6 +9,15 @@ from typing import Dict, Optional, Tuple
|
||||
from core.ssh_client import ShellSession, SFTPSession
|
||||
|
||||
|
||||
_CRITICAL_KEYS = ('ip', 'port', 'username', 'password', 'type',
|
||||
'access_key', 'secret_key', 'use_ssl')
|
||||
|
||||
|
||||
def _server_changed(old: dict, new: dict) -> bool:
|
||||
"""Check if critical connection fields differ."""
|
||||
return any(old.get(k) != new.get(k) for k in _CRITICAL_KEYS)
|
||||
|
||||
|
||||
class SessionData:
|
||||
"""Container for session data including the actual sessions and their metadata."""
|
||||
def __init__(self, alias: str, server: dict, key_path: str):
|
||||
@@ -70,6 +79,11 @@ class SessionPool:
|
||||
self._sessions[alias] = session_data
|
||||
else:
|
||||
session_data = self._sessions[alias]
|
||||
# Invalidate if server connection data changed
|
||||
if _server_changed(session_data.server, server):
|
||||
session_data.cleanup()
|
||||
session_data.server = server
|
||||
session_data.key_path = key_path
|
||||
|
||||
# Update access time for LRU
|
||||
self._update_last_access(alias)
|
||||
@@ -108,6 +122,11 @@ class SessionPool:
|
||||
self._sessions[alias] = session_data
|
||||
else:
|
||||
session_data = self._sessions[alias]
|
||||
# Invalidate if server connection data changed
|
||||
if _server_changed(session_data.server, server):
|
||||
session_data.cleanup()
|
||||
session_data.server = server
|
||||
session_data.key_path = key_path
|
||||
|
||||
# Update access time for LRU
|
||||
self._update_last_access(alias)
|
||||
|
||||
@@ -401,12 +401,15 @@ del /f /q "%~f0" >nul 2>&1
|
||||
return
|
||||
time.sleep(0.1)
|
||||
|
||||
first_run = True
|
||||
while self._running:
|
||||
# Check if enough time passed since last check
|
||||
# On first run after startup, always check regardless of interval
|
||||
last_check = self.store.get_last_update_check()
|
||||
now = time.time()
|
||||
|
||||
if not last_check or (now - last_check) >= _CHECK_INTERVAL:
|
||||
if first_run or not last_check or (now - last_check) >= _CHECK_INTERVAL:
|
||||
first_run = False
|
||||
info = self.check_now()
|
||||
if info and self._gui_callback:
|
||||
mode = self.store.get_update_mode()
|
||||
|
||||
16
gui/app.py
16
gui/app.py
@@ -299,9 +299,19 @@ class App(ctk.CTk):
|
||||
self.sidebar._select(new_alias)
|
||||
self.session_pool.rename_server(alias, new_alias)
|
||||
else:
|
||||
info = self._tab_instances.get("info")
|
||||
if info and hasattr(info, "refresh"):
|
||||
info.refresh()
|
||||
# Data may have changed (IP, port, password) — force reconnect
|
||||
self._force_reconnect(alias)
|
||||
|
||||
def _force_reconnect(self, alias: str):
|
||||
"""Force tabs to reconnect after server data changed."""
|
||||
# Invalidate cached SSH/SFTP sessions in pool
|
||||
self.session_pool.disconnect_session(alias)
|
||||
# Reset _current_alias so set_server() bypasses early return
|
||||
for widget in self._tab_instances.values():
|
||||
if getattr(widget, '_current_alias', None) == alias:
|
||||
widget._current_alias = None
|
||||
# Re-trigger server selection (calls set_server on all tabs)
|
||||
self._on_server_select(alias)
|
||||
|
||||
def _delete_server(self, alias: str):
|
||||
if messagebox.askyesno(t("delete_server"), t("delete_confirm").format(alias=alias)):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
"""Version info for ServerManager."""
|
||||
|
||||
__version__ = "1.9.8"
|
||||
__version__ = "1.9.12"
|
||||
__app_name__ = "ServerManager"
|
||||
__author__ = "aibot777"
|
||||
__description__ = "Desktop GUI for managing remote servers"
|
||||
|
||||
Reference in New Issue
Block a user