mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-07 01:26:02 +02:00
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:
1
pluginHolder/management/__init__.py
Normal file
1
pluginHolder/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#
|
||||
1
pluginHolder/management/commands/__init__.py
Normal file
1
pluginHolder/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#
|
||||
102
pluginHolder/management/commands/refresh_plugin_store_cache.py
Normal file
102
pluginHolder/management/commands/refresh_plugin_store_cache.py
Normal 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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user