Skip to content

Instantly share code, notes, and snippets.

@georgkreimer
Created December 17, 2025 11:53
Show Gist options
  • Select an option

  • Save georgkreimer/ef4d10817cd7cf36a0586b43c52910d9 to your computer and use it in GitHub Desktop.

Select an option

Save georgkreimer/ef4d10817cd7cf36a0586b43c52910d9 to your computer and use it in GitHub Desktop.
Safe-ish updater/loader for robertklep/dsm7-usb-serial-drivers on Synology DSM.
#!/bin/sh
# synology-usb-serial-upgrade.sh
#
# Safe-ish updater/loader for robertklep/dsm7-usb-serial-drivers on Synology DSM.
# - Detects platform + DSM version; probes repo; falls back to dsm-7.2 when needed
# - Downloads modules with integrity guards (non-empty, not HTML, ELF check, vermagic check if possible)
# - Atomic install via staging + rollback on failure + explicit --rollback mode
# - Concurrency lock
# - Observable logging (file + stdout)
# - Separate modes: --download-only / --install-only / --load-only
# - Modprobe sanity check + load via usb-serial-drivers.sh
# - Safer device perms: prefer 660 + group; fallback to 666 if no suitable group
set -eu
REPO_RAW="https://github.com/robertklep/dsm7-usb-serial-drivers/raw/main"
MODULES="ch341.ko cp210x.ko pl2303.ko rndis_host.ko ti_usb_3410_5052.ko"
RC_SCRIPT_TARGET="/usr/local/etc/rc.d/usb-serial-drivers.sh"
MODULES_DIR="/lib/modules"
LASTGOOD_DIR="${MODULES_DIR}/usb-serial-drivers.lastgood"
LOCKDIR="/tmp/usb-serial-upgrade.lock"
# ---------- logging ----------
choose_logfile() {
if [ -d /var/log ] && [ -w /var/log ]; then
echo "/var/log/usb-serial-upgrade.log"
else
echo "/tmp/usb-serial-upgrade.log"
fi
}
LOGFILE="$(choose_logfile)"
timestamp() { date '+%Y-%m-%d %H:%M:%S%z'; }
log() {
msg="$*"
printf '%s %s\n' "$(timestamp)" "$msg" | tee -a "$LOGFILE" >/dev/null
}
die() {
msg="$*"
printf '%s ERROR: %s\n' "$(timestamp)" "$msg" | tee -a "$LOGFILE" >/dev/null
exit 1
}
# ---------- utils ----------
have() { command -v "$1" >/dev/null 2>&1; }
need_root() {
[ "$(id -u)" -eq 0 ] || die "run as root (Task Scheduler: user=root, or sudo)"
}
acquire_lock() {
if mkdir "$LOCKDIR" 2>/dev/null; then
trap 'rm -rf "$LOCKDIR" >/dev/null 2>&1 || true' EXIT INT TERM
return 0
fi
die "another instance is running (lock: $LOCKDIR)"
}
sha256_file() {
if have sha256sum; then
sha256sum "$1" | awk '{print $1}'
elif have openssl; then
openssl dgst -sha256 "$1" | awk '{print $2}'
else
die "need sha256sum or openssl"
fi
}
download() {
url="$1"; out="$2"
if have curl; then
curl -fsSL "$url" -o "$out"
elif have wget; then
wget -qO "$out" "$url"
else
die "need curl or wget"
fi
}
probe_url_exists() {
url="$1"
tmp="$(mktemp -p /tmp probe.XXXXXX)" || exit 1
if download "$url" "$tmp" 2>/dev/null; then
if [ -s "$tmp" ]; then
rm -f "$tmp"
return 0
fi
fi
rm -f "$tmp"
return 1
}
# ---------- system detection ----------
get_platform() {
grep -E '^platform_name=' /etc/synoinfo.conf | sed -E 's/.*"([^"]+)".*/\1/'
}
get_dsm_major_minor() {
f="/etc.defaults/VERSION"
[ -r "$f" ] || f="/etc/VERSION"
major="$(grep -E '^majorversion=' "$f" | head -n1 | cut -d= -f2 | tr -d '"' || true)"
minor="$(grep -E '^minorversion=' "$f" | head -n1 | cut -d= -f2 | tr -d '"' || true)"
[ -n "$major" ] && [ -n "$minor" ] || die "could not parse DSM version from $f"
printf '%s %s\n' "$major" "$minor"
}
choose_dsm_folder() {
platform="$1"
major="$2"
minor="$3"
# Prefer exact match; if missing, probe fallback set (covers "7.3 uses 7.2 libs" case).
candidates="dsm-${major}.${minor}"
if [ "$major" = "7" ]; then
candidates="$candidates dsm-7.2 dsm-7.1 dsm-7.0"
fi
for d in $candidates; do
test_url="$REPO_RAW/modules/$platform/$d/cp210x.ko"
if probe_url_exists "$test_url"; then
echo "$d"
return 0
fi
done
die "no matching driver folder found in repo for platform=$platform dsm=${major}.${minor}"
}
# ---------- validation ----------
is_probably_html() {
f="$1"
head -c 256 "$f" 2>/dev/null | tr '\n' ' ' | grep -qiE '<!doctype html|<html|<head|<body'
}
is_elf() {
f="$1"
# check 0x7f 45 4c 46
magic="$(dd if="$f" bs=4 count=1 2>/dev/null | od -An -t x1 2>/dev/null | tr -d ' \n' || true)"
[ "$magic" = "7f454c46" ]
}
modinfo_bin() {
if [ -x /sbin/modinfo ]; then echo /sbin/modinfo; return 0; fi
if have modinfo; then command -v modinfo; return 0; fi
echo ""
}
vermagic_ok_or_skip() {
ko="$1"
mp="$(modinfo_bin)"
if [ -z "$mp" ]; then
log "modinfo not found; skipping vermagic check for $ko"
return 0
fi
vm="$("$mp" -F vermagic "$ko" 2>/dev/null || true)"
if [ -z "$vm" ]; then
log "modinfo vermagic unavailable; skipping vermagic check for $ko"
return 0
fi
kr="$(uname -r)"
case "$vm" in
"$kr"*) return 0 ;;
*"$kr"*) return 0 ;;
*) die "vermagic mismatch for $(basename "$ko"): vermagic='$vm' kernel='$kr'" ;;
esac
}
validate_downloaded_module() {
f="$1"
[ -s "$f" ] || die "downloaded file empty: $f"
if is_probably_html "$f"; then
die "download looks like HTML (bad URL or GitHub error page): $f"
fi
if ! is_elf "$f"; then
die "download is not an ELF file: $f"
fi
vermagic_ok_or_skip "$f"
}
# ---------- install / rollback ----------
install_or_update_rc_script() {
tmp="$(mktemp -p /tmp usb-serial-drivers.sh.XXXXXX)"
download "$REPO_RAW/usb-serial-drivers.sh" "$tmp"
[ -s "$tmp" ] || die "downloaded rc script is empty"
if is_probably_html "$tmp"; then
die "rc script download looks like HTML"
fi
if [ -f "$RC_SCRIPT_TARGET" ]; then
oldhash="$(sha256_file "$RC_SCRIPT_TARGET")"
newhash="$(sha256_file "$tmp")"
if [ "$oldhash" = "$newhash" ]; then
log "rc script unchanged: $RC_SCRIPT_TARGET"
rm -f "$tmp"
return 0
fi
bkdir="/usr/local/etc/rc.d/usb-serial-drivers.backup"
mkdir -p "$bkdir"
cp -f "$RC_SCRIPT_TARGET" "$bkdir/usb-serial-drivers.sh.$(date +%Y%m%d-%H%M%S)"
fi
cp -f "$tmp" "$RC_SCRIPT_TARGET"
chown root:root "$RC_SCRIPT_TARGET" >/dev/null 2>&1 || true
chmod 755 "$RC_SCRIPT_TARGET"
rm -f "$tmp"
log "installed/updated: $RC_SCRIPT_TARGET"
}
rollback_lastgood() {
[ -d "$LASTGOOD_DIR" ] || die "no lastgood backup directory at $LASTGOOD_DIR"
log "rollback: restoring modules from $LASTGOOD_DIR"
for m in $MODULES; do
src="$LASTGOOD_DIR/$m"
dst="$MODULES_DIR/$m"
if [ -f "$src" ]; then
cp -f "$src" "$dst"
chown root:root "$dst" >/dev/null 2>&1 || true
chmod 644 "$dst" || true
log "rollback restored: $dst"
else
log "rollback missing in lastgood (skipping): $src"
fi
done
log "rollback complete"
}
atomic_install_modules() {
stagedir="$1"
mkdir -p "$LASTGOOD_DIR"
# Stage new files already validated in $stagedir/modules
# Determine which will change
changed_list=""
for m in $MODULES; do
src="$stagedir/modules/$m"
dst="$MODULES_DIR/$m"
if [ -f "$dst" ]; then
oldhash="$(sha256_file "$dst")"
newhash="$(sha256_file "$src")"
if [ "$oldhash" = "$newhash" ]; then
log "unchanged: $dst"
continue
fi
fi
changed_list="$changed_list $m"
done
if [ -z "${changed_list# }" ]; then
log "no module changes to install"
return 0
fi
log "modules to update:$changed_list"
# Backup current to lastgood (only for files that will change)
for m in $changed_list; do
dst="$MODULES_DIR/$m"
if [ -f "$dst" ]; then
cp -f "$dst" "$LASTGOOD_DIR/$m"
chmod 644 "$LASTGOOD_DIR/$m" || true
fi
done
# Install atomically: copy to .staging then rename into place
instaged="${MODULES_DIR}/.staging-usb-serial.$(date +%Y%m%d-%H%M%S)"
mkdir -p "$instaged"
updated=""
cleanup_staging() { rm -rf "$instaged" >/dev/null 2>&1 || true; }
trap 'cleanup_staging' RETURN
for m in $changed_list; do
src="$stagedir/modules/$m"
tmpdst="$instaged/$m"
cp -f "$src" "$tmpdst"
chown root:root "$tmpdst" >/dev/null 2>&1 || true
chmod 644 "$tmpdst"
done
for m in $changed_list; do
tmpdst="$instaged/$m"
dst="$MODULES_DIR/$m"
if mv -f "$tmpdst" "$dst"; then
updated="$updated $m"
log "installed: $dst"
else
log "install failed on $dst; initiating rollback"
rollback_lastgood
die "install failed; rolled back"
fi
done
cleanup_staging
trap - RETURN
log "module install complete"
}
# ---------- load / permissions ----------
modprobe_bin() {
if [ -x /sbin/modprobe ]; then echo /sbin/modprobe; return 0; fi
if have modprobe; then command -v modprobe; return 0; fi
echo ""
}
try_modprobe() {
m="$1"
mp="$(modprobe_bin)"
if [ -z "$mp" ]; then
log "modprobe not found; skipping modprobe $m"
return 0
fi
if "$mp" "$m" 2>/dev/null; then
log "modprobe ok: $m"
else
log "modprobe failed (may be unavailable): $m"
fi
}
is_loaded() { grep -q "^$1 " /proc/modules 2>/dev/null; }
report_loaded() {
for m in "$@"; do
if is_loaded "$m"; then
log "loaded: $m"
else
log "not loaded: $m"
fi
done
}
load_drivers() {
# built-ins sanity check
try_modprobe usbserial
try_modprobe ftdi_sio
try_modprobe cdc_acm
report_loaded usbserial ftdi_sio cdc_acm
# external repo loader
if [ -x "$RC_SCRIPT_TARGET" ]; then
"$RC_SCRIPT_TARGET" start || true
else
die "rc script not found/executable: $RC_SCRIPT_TARGET"
fi
}
pick_serial_group() {
# prefer dialout/uucp if present; else empty
if have getent; then
getent group dialout >/dev/null 2>&1 && { echo dialout; return 0; }
getent group uucp >/dev/null 2>&1 && { echo uucp; return 0; }
getent group tty >/dev/null 2>&1 && { echo tty; return 0; }
else
# busybox fallback
grep -q '^dialout:' /etc/group 2>/dev/null && { echo dialout; return 0; }
grep -q '^uucp:' /etc/group 2>/dev/null && { echo uucp; return 0; }
grep -q '^tty:' /etc/group 2>/dev/null && { echo tty; return 0; }
fi
echo ""
}
fix_device_perms() {
grp="$(pick_serial_group)"
for dev in /dev/ttyUSB*; do
[ -e "$dev" ] || continue
if [ -n "$grp" ]; then
chgrp "$grp" "$dev" 2>/dev/null || true
chmod 660 "$dev" 2>/dev/null || true
log "device perms: $dev -> root:$grp 660"
else
chmod 666 "$dev" 2>/dev/null || true
log "device perms: $dev -> 666 (no suitable group found)"
fi
done
}
# ---------- modes ----------
MODE="full"
case "${1:-}" in
--download-only) MODE="download" ;;
--install-only) MODE="install" ;;
--load-only) MODE="load" ;;
--rollback) MODE="rollback" ;;
"" ) ;;
* ) die "unknown argument: $1 (use: --download-only|--install-only|--load-only|--rollback)" ;;
esac
main() {
need_root
acquire_lock
log "----- start (mode=$MODE) -----"
log "kernel: $(uname -a)"
if [ "$MODE" = "rollback" ]; then
rollback_lastgood
load_drivers
fix_device_perms
log "----- done (rollback) -----"
return 0
fi
platform="$(get_platform)"
set -- $(get_dsm_major_minor); major="$1"; minor="$2"
dsm_folder="$(choose_dsm_folder "$platform" "$major" "$minor")"
log "platform=$platform dsm=${major}.${minor} -> using repo folder: $dsm_folder"
workdir="$(mktemp -d -p /tmp usb-serial-drivers.XXXXXX)"
trap 'rm -rf "$workdir" >/dev/null 2>&1 || true' INT TERM
mkdir -p "$workdir/modules"
base="$REPO_RAW/modules/$platform/$dsm_folder"
log "repo base: $base"
# download + validate
for m in $MODULES; do
out="$workdir/modules/$m"
download "$base/$m" "$out"
validate_downloaded_module "$out"
log "download ok: $m (sha256=$(sha256_file "$out"))"
done
install_or_update_rc_script
if [ "$MODE" = "download" ]; then
log "download-only complete (kept in: $workdir)"
log "NOTE: this temp dir may be removed by the system; rerun without --download-only to install"
log "----- done (download-only) -----"
return 0
fi
# install
atomic_install_modules "$workdir"
if [ "$MODE" = "install" ]; then
log "----- done (install-only) -----"
return 0
fi
# load + perms
load_drivers
fix_device_perms
log "----- done -----"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment