#!/bin/bash
# =============================================================================
# patch-proxmox-vnc-console.sh
#
# Applies or restores the Proxmox VE patch:
# fix #1926 ui: vm console: autodetect novnc or xtermjs
# Source: https://lore.proxmox.com/pve-devel/20250325091854.1051956-1-a.lauterer@proxmox.com/
#
# Usage: bash patch-proxmox-vnc-console.sh
# bash patch-proxmox-vnc-console.sh --status
# =============================================================================
set -euo pipefail
TARGET_FILE="/usr/share/pve-manager/js/pvemanagerlib.js"
BACKUP_FILE="${TARGET_FILE}.orig_backup"
PATCH_MARKER="let activated = false;"
ORIG_MARKER=" var type = me.xtermjs ? 'xtermjs' : 'novnc';"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
info() { echo -e "${CYAN}[INFO]${RESET} $*"; }
success() { echo -e "${GREEN}[OK]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
die() { error "$*"; exit 1; }
check_root() { [[ $EUID -eq 0 ]] || die "This script must be run as root."; }
check_target() { [[ -f "$TARGET_FILE" ]] || die "Target file not found: $TARGET_FILE"; }
detect_state() {
if grep -qF "$PATCH_MARKER" "$TARGET_FILE"; then
echo "patched"
elif grep -qF "$ORIG_MARKER" "$TARGET_FILE"; then
echo "original"
else
echo "unknown"
fi
}
proxmox_version() { pveversion 2>/dev/null | head -1 || echo "unknown version"; }
apply_patch() {
info "Backing up original to: ${BACKUP_FILE}"
cp -p "$TARGET_FILE" "$BACKUP_FILE"
success "Backup created."
info "Applying patch..."
python3 << 'PYEOF'
import sys
path = "/usr/share/pve-manager/js/pvemanagerlib.js"
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
ORIG_MARKER = " var type = me.xtermjs ? 'xtermjs' : 'novnc';"
if ORIG_MARKER not in content:
print("ERROR: original marker not found. Patch already applied or incompatible version.")
sys.exit(1)
OLD_BLOCK = """ var type = me.xtermjs ? 'xtermjs' : 'novnc';
Ext.apply(me, {
layout: {
type: 'vbox',
align: 'stretch',
},
items: [warning, box],
listeners: {
activate: function () {
let sp = Ext.state.Manager.getProvider();
if (Ext.isFunction(me.beforeLoad)) {
me.beforeLoad();
}
let queryDict = {
console: me.consoleType, // kvm, lxc, upgrade or shell
vmid: me.vmid,
node: me.nodename,
cmd: me.cmd,
'cmd-opts': me.cmdOpts,
resize: sp.get('novnc-scaling', 'scale'),
};
queryDict[type] = 1;
PVE.Utils.cleanEmptyObjectKeys(queryDict);
var url = '/?' + Ext.Object.toQueryString(queryDict);
box.load(url);
},
},
});"""
NEW_BLOCK = """ let activated = false;
let configLoaded = false;
let loadConsole = function () {
if (!activated || !configLoaded) {
return;
}
let type = me.xtermjs ? 'xtermjs' : 'novnc';
let sp = Ext.state.Manager.getProvider();
if (Ext.isFunction(me.beforeLoad)) {
me.beforeLoad();
}
let queryDict = {
console: me.consoleType, // kvm, lxc, upgrade or shell
vmid: me.vmid,
node: me.nodename,
cmd: me.cmd,
'cmd-opts': me.cmdOpts,
resize: sp.get('novnc-scaling', 'scale'),
};
queryDict[type] = 1;
PVE.Utils.cleanEmptyObjectKeys(queryDict);
var url = '/?' + Ext.Object.toQueryString(queryDict);
box.load(url);
};
if (me.consoleType === 'kvm') {
Proxmox.Utils.API2Request({
url: `/api2/extjs/nodes/${me.nodename}/qemu/${me.vmid}/config`,
params: { current: '1' },
method: 'GET',
failure: response => Ext.Msg.alert('Error', response.htmlStatus),
success: function ({ result }, options) {
if (result.data.vga?.startsWith('serial')) {
me.xtermjs = true;
}
configLoaded = true;
loadConsole();
},
});
} else {
configLoaded = true;
}
Ext.apply(me, {
layout: {
type: 'vbox',
align: 'stretch',
},
items: [warning, box],
listeners: {
activate: function () {
activated = true;
loadConsole();
},
},
});"""
if OLD_BLOCK not in content:
print("ERROR: target block not found. File format may have changed.")
sys.exit(2)
new_content = content.replace(OLD_BLOCK, NEW_BLOCK, 1)
with open(path, 'w', encoding='utf-8') as f:
f.write(new_content)
print("Patch applied successfully.")
PYEOF
success "Patch applied to $TARGET_FILE"
reload_interface
}
restore_original() {
if [[ ! -f "$BACKUP_FILE" ]]; then
die "No backup found: ${BACKUP_FILE}\nReinstall via: apt install --reinstall pve-manager"
fi
info "Restoring from backup: ${BACKUP_FILE}"
cp -p "$BACKUP_FILE" "$TARGET_FILE"
success "Original file restored."
reload_interface
}
reload_interface() {
info "Reloading pveproxy..."
if systemctl restart pveproxy 2>/dev/null; then
success "pveproxy restarted. Clear your browser cache (Ctrl+Shift+R)."
else
warn "Unable to restart pveproxy. Do it manually: systemctl restart pveproxy"
fi
}
show_menu() {
local state; state=$(detect_state)
local backup_present=false
[[ -f "$BACKUP_FILE" ]] && backup_present=true
echo ""
echo -e "${BOLD}=================================================${RESET}"
echo -e "${BOLD} Patch Proxmox VNC Console — Autodetect xterm ${RESET}"
echo -e "${BOLD}=================================================${RESET}"
echo ""
echo -e " Proxmox: $(proxmox_version)"
echo -e " File: ${TARGET_FILE}"
echo ""
case "$state" in
patched)
echo -e " Status: ${GREEN}✔ PATCHED${RESET} (xterm.js autodetect active)"
$backup_present && echo -e " Backup: ${GREEN}present${RESET}"
echo ""
echo " Options:"
echo " 1) Restore the original file (disable the patch)"
echo " 2) Quit"
read -rp " Your choice [1-2]: " choice
case "$choice" in
1) restore_original ;;
2) info "Cancelled." ;;
*) error "Invalid choice." ;;
esac
;;
original)
echo -e " Status: ${YELLOW}✘ NOT PATCHED${RESET} (standard Proxmox behavior)"
$backup_present && echo -e " Backup: ${GREEN}present${RESET}"
echo ""
echo " This patch automatically detects if the KVM display is"
echo " set to 'serialX' and uses xterm.js instead of noVNC."
echo ""
echo " Options:"
echo " 1) Apply the patch"
if $backup_present; then
echo " 2) Restore from backup"
echo " 3) Quit"
read -rp " Your choice [1-3]: " choice
case "$choice" in
1) apply_patch ;;
2) restore_original ;;
3) info "Cancelled." ;;
*) error "Invalid choice." ;;
esac
else
echo " 2) Quit"
read -rp " Your choice [1-2]: " choice
case "$choice" in
1) apply_patch ;;
2) info "Cancelled." ;;
*) error "Invalid choice." ;;
esac
fi
;;
unknown)
echo -e " Status: ${RED}UNDETERMINED${RESET}"
warn "The file matches neither the original nor the patched version."
warn "A Proxmox update may have occurred."
echo ""
echo " Options:"
echo " 1) Attempt to apply the patch"
if $backup_present; then
echo " 2) Restore from backup"
echo " 3) Quit"
read -rp " Your choice [1-3]: " choice
case "$choice" in
1) apply_patch ;;
2) restore_original ;;
3) info "Cancelled." ;;
*) error "Invalid choice." ;;
esac
else
echo " 2) Quit"
read -rp " Your choice [1-2]: " choice
case "$choice" in
1) apply_patch ;;
2) info "Cancelled." ;;
*) error "Invalid choice." ;;
esac
fi
;;
esac
}
main() {
check_root
check_target
case "${1:-}" in
--status)
echo "Status: $(detect_state)"
[[ -f "$BACKUP_FILE" ]] && echo "Backup: present (${BACKUP_FILE})"
;;
--help|-h)
echo "Usage: $0 [--status | --help]"
echo ""
echo " (no argument) Interactive menu"
echo " --status Show current patch status"
;;
"") show_menu ;;
*) error "Unknown argument: $1"; exit 1 ;;
esac
}
main "$@"