Fix Synology Admin upgrade without docker.sock via binary rename.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-09-09 01:59:34 +02:00
parent fba94b656c
commit 2532ddf93b
6 changed files with 67 additions and 23 deletions

View file

@ -65,7 +65,7 @@ export default function NodesPage() {
const [editRoots, setEditRoots] = useState<ScanRootRow[]>([]);
const [editBusy, setEditBusy] = useState(false);
const [upgradingId, setUpgradingId] = useState<string | null>(null);
const [targetNodeVersion, setTargetNodeVersion] = useState("1.3.7");
const [targetNodeVersion, setTargetNodeVersion] = useState("1.3.8");
function loadNodes() {
fetch("/api/v1/admin/nodes", { credentials: "include" })

View file

@ -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";

View file

@ -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 \

View file

@ -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

View file

@ -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 {

View file

@ -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 {
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=<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=<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
}