pluginHolder: fix plugin store stale-cache refresh + hourly scheduler

Remove stuck plugin-store refresh locks, show correct cache status in UI, and add a management command for hourly refresh.
This commit is contained in:
master3395
2026-03-26 22:45:54 +01:00
parent b5b313090a
commit 8d3e2cd51a
5 changed files with 123 additions and 2 deletions

View File

@@ -0,0 +1 @@
#

View File

@@ -0,0 +1 @@
#

View File

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
import os
import time
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Refresh CyberPanel plugin store cache (hourly scheduler)."
def add_arguments(self, parser):
parser.add_argument(
"--force",
action="store_true",
help="Refresh even if cache is not expired.",
)
parser.add_argument(
"--stale-lock-seconds",
type=int,
default=900,
help="Remove the cache-refresh lock if it is older than this many seconds.",
)
def handle(self, *args, **options):
force = bool(options.get("force", False))
stale_lock_seconds = int(options.get("stale_lock_seconds", 900))
try:
from pluginHolder import views as plugin_views
except Exception as e:
# Avoid printing secrets; just show a minimal message.
self.stderr.write("Failed to import pluginHolder views for cache refresh.")
return 1
# Only refresh when needed (unless --force is used).
try:
cache_expiry_timestamp, _ = plugin_views._get_cache_expiry_time()
cache_expired = plugin_views._is_cache_expired(cache_expiry_timestamp)
except Exception:
cache_expired = True
if not force and cache_expiry_timestamp and not cache_expired:
self.stdout.write("Plugin store cache is still fresh; no refresh needed.")
return 0
lock_path = plugin_views.PLUGIN_STORE_REFRESH_LOCK_FILE
try:
plugin_views._ensure_cache_dir()
except Exception:
pass
# Remove stale lock left behind by a crashed/aborted refresh.
if os.path.exists(lock_path):
try:
age_s = time.time() - os.path.getmtime(lock_path)
if age_s > stale_lock_seconds:
os.remove(lock_path)
try:
plugin_views.logging.writeToFile(
f"Management refresh: removed stale plugin store refresh lock (age: {age_s:.0f}s)"
)
except Exception:
pass
except Exception:
pass
# Acquire lock to avoid stampedes when multiple instances refresh.
try:
fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
with os.fdopen(fd, "w") as f:
f.write(str(os.getpid()))
except FileExistsError:
self.stdout.write("Plugin store refresh skipped: lock already exists.")
return 0
except Exception:
self.stderr.write("Plugin store refresh failed: could not acquire refresh lock.")
return 1
try:
plugins = plugin_views._fetch_plugins_from_github()
if not plugins:
self.stdout.write("Plugin store refresh fetched 0 plugins; cache not updated.")
return 0
plugin_views._save_plugins_cache(plugins)
self.stdout.write(f"Plugin store cache refreshed successfully. plugins={len(plugins)}")
return 0
except Exception as e:
# Log error summary server-side; don't leak internal exception details to stdout.
try:
plugin_views.logging.writeToFile(f"Plugin store cache refresh failed: {str(e)}")
except Exception:
pass
self.stderr.write("Plugin store cache refresh failed. Check error logs.")
return 1
finally:
try:
if os.path.exists(lock_path):
os.remove(lock_path)
except Exception:
pass

View File

@@ -3278,8 +3278,8 @@ function updateCacheExpiryTime() {
if (expired) {
expiryElement.textContent = refreshStarted
? ('Utlopt (' + formatted + ') - oppdaterer i bakgrunnen')
: ('Utlopt (' + formatted + ')');
? ('Expired (' + formatted + ') - updating in background')
: ('Expired (' + formatted + ')');
} else {
expiryElement.textContent = formatted;
}

View File

@@ -33,6 +33,7 @@ PLUGIN_STORE_CACHE_FILE = os.path.join(PLUGIN_STORE_CACHE_DIR, 'plugins_cache.js
PLUGIN_STORE_CACHE_DURATION = 3600 # Base cache duration: 1 hour (3600 seconds)
PLUGIN_STORE_CACHE_RANDOM_OFFSET = 600 # Random offset: ±10 minutes (600 seconds) to prevent simultaneous requests
PLUGIN_STORE_REFRESH_LOCK_FILE = os.path.join(PLUGIN_STORE_CACHE_DIR, 'plugins_cache_refresh.lock')
PLUGIN_STORE_REFRESH_LOCK_STALE_SECONDS = 900 # 15 minutes; remove leftover lock if stuck
GITHUB_REPO_API = 'https://api.github.com/repos/master3395/cyberpanel-plugins/contents'
GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/master3395/cyberpanel-plugins/main'
GITHUB_COMMITS_API = 'https://api.github.com/repos/master3395/cyberpanel-plugins/commits'
@@ -1046,6 +1047,22 @@ def _try_start_plugin_store_refresh_background():
try:
_ensure_cache_dir()
# If a previous refresh crashed and left the lock behind, remove it
# so background refresh can resume. This is critical for hourly updates.
try:
if os.path.exists(lock_path):
age_s = time.time() - os.path.getmtime(lock_path)
if age_s > PLUGIN_STORE_REFRESH_LOCK_STALE_SECONDS:
try:
os.remove(lock_path)
logging.writeToFile(
f"Removed stale plugin store refresh lock (age: {age_s:.0f}s)"
)
except Exception:
pass
except Exception:
pass
# Try to acquire a file lock so multiple workers don't stampede GitHub.
try:
fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)