Created
December 17, 2025 11:53
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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