kbd_disp_switch (6497B)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | #!/bin/sh # kbd_disp_switch — shared-monitor HDMI input switching, arrival-driven # # Two PCs share one monitor (e.g. HDMI-1 = host A, HDMI-2 = host B) and move # one USB dongle (keyboard/mouse receiver) between them. The display follows # the dongle's ARRIVAL: # attach -> switch to MY_INPUT ... unless the dongle came straight back # from this host (round trip / flap): the display never moved, # so a re-write would only cause a 2-3 s blank (re-handshake). # detach -> record the departure time; push it to the peer host. # # Decision (event-ordering, NOT a time window): # no-op iff my_D > 0 AND peer_D > 0 AND my_D > peer_D # (my departure newer than the peer's last departure -> round trip) # otherwise switch (safe default when the peer is unknown) # # Subcommands (called by udev rule 99-kbd-disp.rules and systemd-run): # attach — udev add: cancel pending decision, start the decision unit, # fire-and-forget peer push # detach — udev remove: record departure, cancel pending decision, push # departure to the peer (synchronous) # decide — systemd-run unit: peer query + event-ordering decision + DDC write # push — systemd-run unit: fire-and-forget peer push # # Why systemd-run? udev kills leftover processes of RUN programs (cgroup # kill — even setsid doesn't help), so backgrounded jobs die. Transient # systemd units survive and are cancellable via systemctl stop. The decision # unit sleeps SETTLE internally (systemd --on-active timers proved unreliable # here — they fired 6-60 s late). # # Config: /etc/kbd_disp.conf # MY_INPUT=1|2 1 -> x0090 (monitor HDMI 1), 2 -> x0091 (HDMI 2) # SETTLE=2 seconds before the decision (dedupes USB events) # PEER_QUERY=user@host (optional) ssh-query peer's last_detach # PEER_STATE_FILE=/path (optional, passive mode) last_detach pushed by peer # PEER_PUSH=user@host (optional) push our last_detach to peer # SSH_KEY=/root/.ssh/kbd_disp dedicated passphrase-less key (udev runs as root) # DDC_CMD='...' DDC write command; %s = input value (see README) # # Default DDC write (LG side-channel — standard VCP 60 is broken on recent # LG monitors; see README "Finding the right DDC command"): # ddcutil setvcp xF4 x0090 --i2c-source-addr=x50 --noverify # HDMI 1 # ddcutil setvcp xF4 x0091 --i2c-source-addr=x50 --noverify # HDMI 2 # Most other monitors: ddcutil setvcp 60 0x0F / 0x10 # # State: /var/lib/kbd_disp/last_detach (epoch of last local dongle departure) # Log: /var/lib/kbd_disp/switch.log export PATH=/usr/local/sbin:/usr/bin:/bin STATE_DIR=/var/lib/kbd_disp LAST_DETACH=$STATE_DIR/last_detach LOG=$STATE_DIR/switch.log UNIT=kbd-disp-decide PUSH_UNIT=kbd-disp-push [ -r /etc/kbd_disp.conf ] || { echo "kbd_disp_switch: missing /etc/kbd_disp.conf" >&2; exit 1; } . /etc/kbd_disp.conf SETTLE=${SETTLE:-2} SSH_KEY=${SSH_KEY:-/root/.ssh/kbd_disp} DDC_CMD=${DDC_CMD:-'ddcutil setvcp xF4 %s --i2c-source-addr=x50 --noverify'} log() { if [ -f "$LOG" ]; then size=$(wc -c < "$LOG") [ "$size" -gt 1048576 ] && mv -f "$LOG" "$LOG.1" fi echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOG" } mkdir -p "$STATE_DIR" 2>/dev/null # ssh helper: non-interactive, bounded, root-run (udev/systemd context) ssh_peer() { # $1 = user@host, $2 = remote command [ -n "$1" ] || return 1 timeout 6 ssh -o BatchMode=yes -o ConnectTimeout=3 \ -o IdentitiesOnly=yes -o IdentityFile="$SSH_KEY" \ -o UserKnownHostsFile=/root/.ssh/known_hosts \ -o StrictHostKeyChecking=accept-new \ "$1" "$2" 2>/dev/null return $? } # cancel a pending decision (new attach or a detach supersedes it) cancel_pending() { systemctl stop "$UNIT.timer" "$UNIT.service" 2>/dev/null } case "$1" in detach) now=$(date +%s) printf '%s\n' "$now" > "$LAST_DETACH" cancel_pending if [ -n "$PEER_PUSH" ]; then if ssh_peer "$PEER_PUSH" "kbd_disp_peer set $now"; then log "detach: pushed departure (t=$now) to $PEER_PUSH" else log "detach: WARN push to $PEER_PUSH failed (peer file may be stale)" fi fi log "detach: departure recorded (t=$now), no switch" exit 0 ;; attach) ;; decide) ;; push) ;; *) echo "usage: $0 {attach|detach|decide|push}" >&2; exit 2 ;; esac case "$MY_INPUT" in 1) VAL=x0090 ;; 2) VAL=x0091 ;; *) log "FATAL: MY_INPUT='$MY_INPUT' (must be 1 or 2)"; exit 1 ;; esac if [ "$1" = "attach" ]; then cancel_pending if systemd-run --no-block --unit="$UNIT" --collect \ /usr/local/sbin/kbd_disp_switch decide 2>/dev/null; then log "attach: scheduled decision in ${SETTLE}s" else log "attach: WARN systemd-run failed (no decision scheduled)" fi if [ -n "$PEER_PUSH" ]; then systemctl stop "$PUSH_UNIT.service" 2>/dev/null systemd-run --no-block --unit="$PUSH_UNIT" --collect \ /usr/local/sbin/kbd_disp_switch push 2>/dev/null fi exit 0 fi if [ "$1" = "push" ]; then now=$(date +%s) if ssh_peer "$PEER_PUSH" "kbd_disp_peer set $now"; then log "push: sent t=$now to $PEER_PUSH" else log "push: WARN failed (t=$now)" fi exit 0 fi # ---- decide (runs as a systemd-run unit) ---- sleep "$SETTLE" || exit 0 # debounce: a detach/duplicate event cancels this unit my_D=$(cat "$LAST_DETACH" 2>/dev/null) peer_D="" if [ -n "$PEER_QUERY" ]; then peer_D=$(ssh_peer "$PEER_QUERY" "kbd_disp_peer get") elif [ -n "$PEER_STATE_FILE" ]; then peer_D=$(cat "$PEER_STATE_FILE" 2>/dev/null) fi case "$peer_D" in ''|*[!0-9]*) peer_D=0 ;; esac case "$my_D" in ''|*[!0-9]*) my_D=0 ;; esac if [ "$my_D" -gt 0 ] && [ "$peer_D" -gt 0 ] && [ "$my_D" -gt "$peer_D" ]; then log "decide: no-op (my departure t=$my_D is newest — round trip/flap, display already on input $MY_INPUT)" exit 0 fi log "decide: real move (peer departure t=${peer_D:-none} newer — or unknown) — switching to input $MY_INPUT" for i in 1 2 3; do cmd=$(printf "$DDC_CMD" "$VAL") out=$(sh -c "$cmd" 2>&1) rc=$? if [ "$rc" -eq 0 ]; then log "decide: switch to input $MY_INPUT OK (attempt $i)" exit 0 fi log "decide: switch attempt $i FAILED rc=$rc: $out" sleep 3 done log "decide: GAVE UP switching to input $MY_INPUT" exit 1 |
