mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-08 17:59:56 +00:00
[client/ui-wails] Make /update page faithful to the legacy auto-update dialog
Adds the missing info line ("Your client version is older than the
auto-update version set in Management. Updating client to: <version>.")
and replaces the spinner with the legacy 1-second dot animation
(Updating./.../...). Terminal-state wording now matches the Fyne UI
exactly: 15 min timeout, canceled, and "Update failed: <err>".
Ports mapInstallError from client/ui/update.go so daemon errors that
embed "deadline exceeded" / "canceled" hit the right branch instead of
falling through as a generic failure.
Detects the daemon dropping mid-upgrade (the legacy success signal):
if GetInstallerResult fails for 5s straight, call the new Update.Quit
service method to exit, mirroring app.Quit() in showInstallerResult.
This commit is contained in:
@@ -20,6 +20,17 @@ export function GetInstallerResult(): $CancellablePromise<$models.UpdateResult>
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Quit asks the host application to exit. The /update page calls this once
|
||||
* the daemon-side installer has reported success, mirroring the legacy
|
||||
* Fyne UI's app.Quit() in showInstallerResult. Schedules the actual exit
|
||||
* off the calling goroutine so the JS-side caller's response can return
|
||||
* before the runtime tears down.
|
||||
*/
|
||||
export function Quit(): $CancellablePromise<void> {
|
||||
return $Call.ByID(27817640);
|
||||
}
|
||||
|
||||
export function Trigger(): $CancellablePromise<$models.UpdateResult> {
|
||||
return $Call.ByID(2415339649).then(($result: any) => {
|
||||
return $$createType0($result);
|
||||
|
||||
@@ -1,61 +1,135 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Update as UpdateSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
|
||||
|
||||
const TIMEOUT_MS = 15 * 60 * 1000;
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
// How long the daemon is allowed to be unreachable before we treat it as
|
||||
// "daemon went down for the upgrade, treat as success and quit". Mirrors
|
||||
// the legacy Fyne UI's branch in client/ui/update.go where a connection
|
||||
// failure during polling is taken as the success signal.
|
||||
const DAEMON_DOWN_GRACE_MS = 5000;
|
||||
|
||||
type Phase =
|
||||
| { kind: "running"; dots: number }
|
||||
| { kind: "timeout" }
|
||||
| { kind: "canceled" }
|
||||
| { kind: "failed"; message: string };
|
||||
|
||||
export default function Update() {
|
||||
const [done, setDone] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [phase, setPhase] = useState<Phase>({ kind: "running", dots: 1 });
|
||||
const phaseRef = useRef(phase);
|
||||
phaseRef.current = phase;
|
||||
|
||||
const version = new URLSearchParams(
|
||||
window.location.hash.split("?")[1] ?? "",
|
||||
).get("version");
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
UpdateSvc.Trigger().catch((e) => !cancelled && setError(String(e)));
|
||||
|
||||
const start = Date.now();
|
||||
const timer = setInterval(async () => {
|
||||
let firstUnreachableAt: number | null = null;
|
||||
|
||||
UpdateSvc.Trigger().catch(() => {
|
||||
// The daemon may already be down (installer launched, daemon shutting
|
||||
// down). Don't treat as failure here; the poll loop's daemon-down
|
||||
// detection handles it.
|
||||
});
|
||||
|
||||
const dotTimer = setInterval(() => {
|
||||
if (cancelled) return;
|
||||
setPhase((p) =>
|
||||
p.kind === "running" ? { kind: "running", dots: (p.dots % 3) + 1 } : p,
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
const pollTimer = setInterval(async () => {
|
||||
if (cancelled) return;
|
||||
if (phaseRef.current.kind !== "running") return;
|
||||
|
||||
if (Date.now() - start > TIMEOUT_MS) {
|
||||
setError("Update timed out.");
|
||||
clearInterval(timer);
|
||||
clearInterval(pollTimer);
|
||||
clearInterval(dotTimer);
|
||||
setPhase({ kind: "timeout" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const r = await UpdateSvc.GetInstallerResult();
|
||||
firstUnreachableAt = null;
|
||||
if (r.success) {
|
||||
setDone(true);
|
||||
clearInterval(timer);
|
||||
} else if (r.errorMsg) {
|
||||
setError(r.errorMsg);
|
||||
clearInterval(timer);
|
||||
clearInterval(pollTimer);
|
||||
clearInterval(dotTimer);
|
||||
UpdateSvc.Quit();
|
||||
return;
|
||||
}
|
||||
if (r.errorMsg) {
|
||||
clearInterval(pollTimer);
|
||||
clearInterval(dotTimer);
|
||||
setPhase(mapInstallError(r.errorMsg));
|
||||
}
|
||||
} catch {
|
||||
// installer not finished yet
|
||||
// RPC failed. The daemon often goes away mid-upgrade — treat a
|
||||
// sustained outage as success and quit, matching the legacy UI.
|
||||
const now = Date.now();
|
||||
if (firstUnreachableAt === null) {
|
||||
firstUnreachableAt = now;
|
||||
} else if (now - firstUnreachableAt >= DAEMON_DOWN_GRACE_MS) {
|
||||
clearInterval(pollTimer);
|
||||
clearInterval(dotTimer);
|
||||
UpdateSvc.Quit();
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
}, POLL_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(timer);
|
||||
clearInterval(dotTimer);
|
||||
clearInterval(pollTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const versionLine = version
|
||||
? `Updating client to: ${version}.`
|
||||
: "Updating client.";
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-6">
|
||||
<div className="text-center">
|
||||
{done ? (
|
||||
<h1 className="text-xl font-semibold text-green-500">Update complete</h1>
|
||||
) : error ? (
|
||||
<h1 className="text-xl font-semibold text-red-500">{error}</h1>
|
||||
) : (
|
||||
<>
|
||||
<Loader2 className="mx-auto mb-3 h-8 w-8 animate-spin text-netbird" strokeWidth={1.5} />
|
||||
<h1 className="text-xl font-semibold">Updating…</h1>
|
||||
<p className="mt-1 text-sm text-nb-gray-500">
|
||||
Please don't close this window.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<div className="space-y-3 text-center">
|
||||
<p className="whitespace-pre-line text-sm text-nb-gray-700 dark:text-nb-gray-200">
|
||||
{`Your client version is older than the auto-update version set in Management.\n${versionLine}`}
|
||||
</p>
|
||||
<p className="text-base font-medium">{statusText(phase)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function statusText(p: Phase): string {
|
||||
switch (p.kind) {
|
||||
case "running":
|
||||
return "Updating" + ".".repeat(p.dots);
|
||||
case "timeout":
|
||||
return "Update timed out. Please try again.";
|
||||
case "canceled":
|
||||
return "Update canceled.";
|
||||
case "failed":
|
||||
return "Update failed: " + p.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors mapInstallError in client/ui/update.go. The daemon's installer
|
||||
// surfaces error strings rather than typed errors, so the UI sniffs the
|
||||
// message to decide whether to show the timeout/canceled wording.
|
||||
function mapInstallError(msg: string): Phase {
|
||||
const m = msg.trim().toLowerCase();
|
||||
if (m === "") {
|
||||
return { kind: "failed", message: "unknown update error" };
|
||||
}
|
||||
if (m.includes("deadline exceeded") || m.includes("timeout")) {
|
||||
return { kind: "timeout" };
|
||||
}
|
||||
if (m.includes("canceled") || m.includes("cancelled")) {
|
||||
return { kind: "canceled" };
|
||||
}
|
||||
return { kind: "failed", message: msg };
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
@@ -24,6 +27,18 @@ func NewUpdate(conn DaemonConn) *Update {
|
||||
return &Update{conn: conn}
|
||||
}
|
||||
|
||||
// Quit asks the host application to exit. The /update page calls this once
|
||||
// the daemon-side installer has reported success, mirroring the legacy
|
||||
// Fyne UI's app.Quit() in showInstallerResult. Schedules the actual exit
|
||||
// off the calling goroutine so the JS-side caller's response can return
|
||||
// before the runtime tears down.
|
||||
func (s *Update) Quit() {
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
application.Get().Quit()
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Update) Trigger(ctx context.Context) (UpdateResult, error) {
|
||||
cli, err := s.conn.Client()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user