mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-04-30 06:47:54 +02:00
fix: Docker self-update shows pull instructions instead of silently failing
The self-updater extracted files inside the container but Docker restarts from the original image, discarding all changes. Now detects Docker via /.dockerenv and returns pull commands for the user to run on their host. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8135,6 +8135,9 @@ async def system_update(request: Request):
|
||||
status_code=500,
|
||||
media_type="application/json",
|
||||
)
|
||||
# Docker: skip restart — user must pull new images manually
|
||||
if result.get("status") == "docker":
|
||||
return result
|
||||
# Schedule restart AFTER response flushes (2s delay)
|
||||
threading.Timer(2.0, schedule_restart, args=[project_root]).start()
|
||||
return result
|
||||
|
||||
@@ -25,6 +25,21 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
GITHUB_RELEASES_URL = "https://api.github.com/repos/BigBodyCobain/Shadowbroker/releases/latest"
|
||||
GITHUB_RELEASES_PAGE_URL = "https://github.com/BigBodyCobain/Shadowbroker/releases/latest"
|
||||
DOCKER_UPDATE_COMMANDS = (
|
||||
"docker compose pull && docker compose up -d"
|
||||
)
|
||||
|
||||
|
||||
def _is_docker() -> bool:
|
||||
"""Detect if we're running inside a Docker container."""
|
||||
if os.path.isfile("/.dockerenv"):
|
||||
return True
|
||||
try:
|
||||
with open("/proc/1/cgroup", "r") as f:
|
||||
return "docker" in f.read()
|
||||
except (FileNotFoundError, PermissionError):
|
||||
pass
|
||||
return os.environ.get("container") == "docker"
|
||||
_EXPECTED_SHA256 = os.environ.get("MESH_UPDATE_SHA256", "").strip().lower()
|
||||
_ALLOWED_UPDATE_HOSTS = {
|
||||
"api.github.com",
|
||||
@@ -314,12 +329,32 @@ def perform_update(project_root: str) -> dict:
|
||||
Returns a dict with status info on success, or {"status": "error", "message": ...}
|
||||
on failure. Does NOT trigger restart — caller should call schedule_restart()
|
||||
separately after the HTTP response has been sent.
|
||||
|
||||
In Docker, file extraction is skipped because containers run from immutable
|
||||
images. Instead the response tells the frontend to show pull instructions.
|
||||
"""
|
||||
in_docker = _is_docker()
|
||||
temp_dir = tempfile.mkdtemp(prefix="sb_update_")
|
||||
manual_url = GITHUB_RELEASES_PAGE_URL
|
||||
try:
|
||||
zip_path, version, url, release_url = _download_release(temp_dir)
|
||||
manual_url = release_url or manual_url
|
||||
|
||||
if in_docker:
|
||||
logger.info("Docker detected — skipping file extraction")
|
||||
return {
|
||||
"status": "docker",
|
||||
"version": version,
|
||||
"manual_url": manual_url,
|
||||
"release_url": release_url,
|
||||
"download_url": url,
|
||||
"docker_commands": DOCKER_UPDATE_COMMANDS,
|
||||
"message": (
|
||||
f"Version {version} is available. "
|
||||
"Docker containers must be updated by pulling the new images."
|
||||
),
|
||||
}
|
||||
|
||||
_validate_zip_hash(zip_path)
|
||||
backup_path = _backup_current(project_root, temp_dir)
|
||||
copied = _extract_and_copy(zip_path, project_root, temp_dir)
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
X,
|
||||
Terminal,
|
||||
Server,
|
||||
Copy,
|
||||
} from 'lucide-react';
|
||||
import { API_BASE } from '@/lib/api';
|
||||
import { controlPlaneFetch } from '@/lib/controlPlane';
|
||||
@@ -41,7 +42,8 @@ type UpdateStatus =
|
||||
| 'confirming'
|
||||
| 'updating'
|
||||
| 'restarting'
|
||||
| 'update_error';
|
||||
| 'update_error'
|
||||
| 'docker_update';
|
||||
|
||||
const DEFAULT_RELEASES_URL = 'https://github.com/BigBodyCobain/Shadowbroker/releases/latest';
|
||||
|
||||
@@ -63,6 +65,7 @@ export default function TopRightControls({
|
||||
const [latestVersion, setLatestVersion] = useState<string>('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [manualUpdateUrl, setManualUpdateUrl] = useState(DEFAULT_RELEASES_URL);
|
||||
const [dockerCommands, setDockerCommands] = useState('');
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [launcherOpen, setLauncherOpen] = useState(false);
|
||||
@@ -398,10 +401,16 @@ export default function TopRightControls({
|
||||
message?: string;
|
||||
detail?: string;
|
||||
manual_url?: string;
|
||||
docker_commands?: string;
|
||||
};
|
||||
if (typeof data.manual_url === 'string' && data.manual_url.trim().length > 0) {
|
||||
setManualUpdateUrl(data.manual_url);
|
||||
}
|
||||
if (data?.status === 'docker') {
|
||||
setDockerCommands(data.docker_commands || 'docker compose pull && docker compose up -d');
|
||||
setUpdateStatus('docker_update');
|
||||
return;
|
||||
}
|
||||
if (!res.ok || data?.ok === false || data?.status === 'error') {
|
||||
const message = data?.detail || data?.message || 'control_plane_request_failed';
|
||||
const error = new Error(message) as Error & { manualUrl?: string };
|
||||
@@ -516,6 +525,50 @@ export default function TopRightControls({
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── Docker Update Dialog ──
|
||||
const renderDockerDialog = () => (
|
||||
<div className="absolute top-full right-0 mt-2 w-80 z-[9999]">
|
||||
<div className="bg-[var(--bg-primary)]/95 backdrop-blur-sm border border-cyan-800/60 shadow-[0_4px_30px_rgba(0,255,255,0.15)] overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-[var(--border-primary)]">
|
||||
<span className="text-[10px] font-mono tracking-widest text-cyan-400">
|
||||
DOCKER UPDATE — v{latestVersion}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setUpdateStatus('idle')}
|
||||
className="text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-3 flex flex-col gap-2">
|
||||
<p className="text-[9px] font-mono text-[var(--text-muted)] leading-relaxed">
|
||||
Docker containers must be updated by pulling new images.
|
||||
Run this on your host machine:
|
||||
</p>
|
||||
<div className="relative bg-black/40 border border-[var(--border-primary)] p-2 group">
|
||||
<code className="text-[9px] font-mono text-green-400 break-all">{dockerCommands}</code>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(dockerCommands)}
|
||||
className="absolute top-1 right-1 p-1 opacity-0 group-hover:opacity-100 transition-opacity text-[var(--text-muted)] hover:text-cyan-400"
|
||||
title="Copy command"
|
||||
>
|
||||
<Copy size={10} />
|
||||
</button>
|
||||
</div>
|
||||
<a
|
||||
href={manualUpdateUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-[var(--bg-secondary)]/50 border border-[var(--border-primary)] hover:border-[var(--text-muted)] transition-all text-[10px] text-[var(--text-muted)] font-mono tracking-widest"
|
||||
>
|
||||
<ExternalLink size={12} />
|
||||
VIEW RELEASE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const nodeMode = String(nodeStatus?.node_mode || 'participant').trim().toUpperCase();
|
||||
const nodeEnabled = Boolean(nodeStatus?.node_enabled);
|
||||
const syncOutcomeRaw = String(nodeStatus?.sync_runtime?.last_outcome || 'idle')
|
||||
@@ -1026,8 +1079,22 @@ export default function TopRightControls({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Docker update → show pull instructions ── */}
|
||||
{updateStatus === 'docker_update' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setUpdateStatus('docker_update')}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 bg-cyan-500/10 backdrop-blur-sm border border-cyan-500/50 text-[10px] text-cyan-400 font-mono shadow-[0_0_15px_rgba(0,255,255,0.2)]"
|
||||
>
|
||||
<Terminal size={12} className="w-3 h-3" />
|
||||
<span className="tracking-widest">DOCKER UPDATE</span>
|
||||
</button>
|
||||
{renderDockerDialog()}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Default states: idle / checking / uptodate / check-error ── */}
|
||||
{!['available', 'confirming', 'updating', 'restarting', 'update_error'].includes(
|
||||
{!['available', 'confirming', 'updating', 'restarting', 'update_error', 'docker_update'].includes(
|
||||
updateStatus,
|
||||
) && (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user