From 2532ddf93bb7d071eb831e772f8f22ca3efc4cf2 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Wed, 9 Sep 2026 01:59:34 +0200 Subject: [PATCH] Fix Synology Admin upgrade without docker.sock via binary rename. Co-authored-by: Cursor --- apps/admin-ui/src/app/nodes/page.tsx | 2 +- apps/master-api/src/nodes/version.ts | 2 +- deploy/docker/Dockerfile.master-api | 2 +- deploy/node/upgrade-node.sh | 2 +- node/media-node/cmd/media-node/main.go | 2 +- node/media-node/internal/control/upgrade.go | 80 ++++++++++++++++----- 6 files changed, 67 insertions(+), 23 deletions(-) diff --git a/apps/admin-ui/src/app/nodes/page.tsx b/apps/admin-ui/src/app/nodes/page.tsx index b0b560c..bd10394 100644 --- a/apps/admin-ui/src/app/nodes/page.tsx +++ b/apps/admin-ui/src/app/nodes/page.tsx @@ -65,7 +65,7 @@ export default function NodesPage() { const [editRoots, setEditRoots] = useState([]); const [editBusy, setEditBusy] = useState(false); const [upgradingId, setUpgradingId] = useState(null); - const [targetNodeVersion, setTargetNodeVersion] = useState("1.3.7"); + const [targetNodeVersion, setTargetNodeVersion] = useState("1.3.8"); function loadNodes() { fetch("/api/v1/admin/nodes", { credentials: "include" }) diff --git a/apps/master-api/src/nodes/version.ts b/apps/master-api/src/nodes/version.ts index e6a167e..a8e26b7 100644 --- a/apps/master-api/src/nodes/version.ts +++ b/apps/master-api/src/nodes/version.ts @@ -1,6 +1,6 @@ /** Target media-node version served from /install/media-node-linux-* */ export const MEDIA_NODE_TARGET_VERSION = - process.env.MEDIA_NODE_VERSION?.trim() || "1.3.7"; + process.env.MEDIA_NODE_VERSION?.trim() || "1.3.8"; /** Minimum node version that handles UPGRADE over WebSocket */ const REMOTE_UPGRADE_MIN_VERSION = "1.3.3"; diff --git a/deploy/docker/Dockerfile.master-api b/deploy/docker/Dockerfile.master-api index e353a7b..2be1d0a 100644 --- a/deploy/docker/Dockerfile.master-api +++ b/deploy/docker/Dockerfile.master-api @@ -4,7 +4,7 @@ WORKDIR /app FROM golang:1.22-alpine AS node-build WORKDIR /src -ARG MEDIA_NODE_VERSION=1.3.7 +ARG MEDIA_NODE_VERSION=1.3.8 RUN apk add --no-cache git ca-certificates COPY node/media-node/ ./ RUN go mod tidy \ diff --git a/deploy/node/upgrade-node.sh b/deploy/node/upgrade-node.sh index 9863b52..84b13e6 100644 --- a/deploy/node/upgrade-node.sh +++ b/deploy/node/upgrade-node.sh @@ -74,7 +74,7 @@ mv "$BIN_PATH.new" "$BIN_PATH" if docker_cmd ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then echo "Container herstarten..." docker_cmd restart "$CONTAINER" >/dev/null - echo "Klaar. Controleer versie in Admin → Nodes (verwacht ${MEDIA_NODE_VERSION:-1.3.7}+)." + echo "Klaar. Controleer versie in Admin → Nodes (verwacht ${MEDIA_NODE_VERSION:-1.3.8}+)." else echo "Waarschuwing: container '$CONTAINER' niet gevonden — binary is wel bijgewerkt." fi diff --git a/node/media-node/cmd/media-node/main.go b/node/media-node/cmd/media-node/main.go index 0f0b05f..dc76f7b 100644 --- a/node/media-node/cmd/media-node/main.go +++ b/node/media-node/cmd/media-node/main.go @@ -24,7 +24,7 @@ import ( "github.com/sthmedia/media-node/internal/streaming" ) -var version = "1.3.7" +var version = "1.3.8" func main() { if len(os.Args) < 2 { diff --git a/node/media-node/internal/control/upgrade.go b/node/media-node/internal/control/upgrade.go index dcca6b7..24d7532 100644 --- a/node/media-node/internal/control/upgrade.go +++ b/node/media-node/internal/control/upgrade.go @@ -69,21 +69,25 @@ func (c *Client) runUpgrade(downloadURL string) error { return fmt.Errorf("chmod staging: %w", err) } - // Prefer in-place install (no Docker API). Needs binary bind-mount :rw - // (install-*.sh) — werkt op Synology waar docker.sock root-only is. - targets := []string{"/usr/local/bin/media-node"} - if binDir := strings.TrimSpace(os.Getenv("MEDIA_NODE_BIN_DIR")); binDir != "" { - targets = append(targets, filepath.Join(binDir, "media-node")) - } + // Prefer directory bind-mount (MEDIA_NODE_BIN_DIR). Replace via .new + rename + // so we never overwrite the running executable (ETXTBSY) and need no docker.sock + // (Synology: sock is root-only). + var errs []string + targets := upgradeBinaryTargets() for _, dest := range targets { - if err := installBinaryFile(stagingFile, dest); err == nil { - log.Printf("UPGRADE: binary geplaatst op %s", dest) - return nil + if err := installBinaryReplace(stagingFile, dest); err != nil { + errs = append(errs, fmt.Sprintf("%s: %v", dest, err)) + continue } + log.Printf("UPGRADE: binary geplaatst op %s", dest) + return nil } if _, err := os.Stat("/var/run/docker.sock"); err != nil { - return fmt.Errorf("geen schrijfrechten op binary-mount en geen docker.sock — SSH: curl -fsSL \"$MASTER/install/upgrade-node.sh\" | bash -s -- --appdata=") + return fmt.Errorf( + "binary niet te vervangen (%s) en geen docker.sock — eenmalig via SSH: curl -fsSL \"$MASTER/install/upgrade-node.sh\" | bash -s -- --appdata=", + strings.Join(errs, "; "), + ) } containerName := strings.TrimSpace(os.Getenv("MEDIA_NODE_CONTAINER_NAME")) @@ -93,7 +97,7 @@ func (c *Client) runUpgrade(downloadURL string) error { binHostDir, dataHostDir, err := dockerContainerBindDirs(containerName) if err != nil { - return fmt.Errorf("%w — op Synology vaak docker.sock permission denied; upgrade via SSH met upgrade-node.sh", err) + return fmt.Errorf("%w — eerdere pogingen: %s", err, strings.Join(errs, "; ")) } stagingHost := filepath.Join(dataHostDir, "upgrade", "media-node") @@ -103,31 +107,69 @@ func (c *Client) runUpgrade(downloadURL string) error { filepath.Dir(stagingHost) + ":/staging:ro", } if err := dockerRunOnce("alpine:3.20", shellCmd, binds); err != nil { - return fmt.Errorf("binary installeren: %w", err) + return fmt.Errorf("binary installeren via docker: %w", err) } return nil } -func installBinaryFile(src, dest string) error { +func upgradeBinaryTargets() []string { + var out []string + if binDir := strings.TrimSpace(os.Getenv("MEDIA_NODE_BIN_DIR")); binDir != "" { + out = append(out, filepath.Join(binDir, "media-node")) + } + // File bind-mount of the running entrypoint — often ETXTBSY; try last. + out = append(out, "/usr/local/bin/media-node") + return out +} + +// installBinaryReplace writes dest.new then replaces dest (unlink+rename). +// Unlink of a running Linux binary is allowed; open(O_WRONLY) on it is not (ETXTBSY). +func installBinaryReplace(src, dest string) error { + if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { + return fmt.Errorf("bin-map: %w", err) + } + + newPath := dest + ".new" + _ = os.Remove(newPath) + in, err := os.Open(src) if err != nil { return err } defer in.Close() - // Direct overwrite: file-bind-mounts hebben vaak geen writable sibling (.new). - out, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC, 0o755) + out, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755) if err != nil { - return err + return fmt.Errorf("schrijven %s: %w", newPath, err) } if _, err := io.Copy(out, in); err != nil { out.Close() - return err + _ = os.Remove(newPath) + return fmt.Errorf("kopiëren: %w", err) } if err := out.Close(); err != nil { + _ = os.Remove(newPath) return err } - return os.Chmod(dest, 0o755) + if err := os.Chmod(newPath, 0o755); err != nil { + _ = os.Remove(newPath) + return err + } + + // Drop old name (ok while process still runs), then put new binary in place. + if err := os.Remove(dest); err != nil && !os.IsNotExist(err) { + // Some file-mounts refuse remove; try rename-over as fallback. + if renErr := os.Rename(newPath, dest); renErr != nil { + _ = os.Remove(newPath) + return fmt.Errorf("vervangen %s: remove=%v rename=%v", dest, err, renErr) + } + return nil + } + if err := os.Rename(newPath, dest); err != nil { + _ = os.Remove(newPath) + return fmt.Errorf("rename naar %s: %w", dest, err) + } + return nil } func downloadFile(url, dest string) error { @@ -185,6 +227,8 @@ func dockerContainerBindDirs(containerName string) (binHostDir, dataHostDir stri switch m.Destination { case "/usr/local/bin/media-node": binHostDir = filepath.Dir(m.Source) + case "/opt/media-node-bin": + binHostDir = m.Source case "/var/lib/media-node": dataHostDir = m.Source }