Сервер без монитора обычно общается с человеком строками терминала, и у такого стиля есть своя честность: каждая команда видна на экране, её можно повторить и вписать в регламент. Но периодически сценарию нужно решение человека, и городить ради одного вопроса текстовое меню в консоли неудобно. Утилита zenity превращает обычный bash-сценарий в программу с графическими окнами: вопрос с кнопками, поле ввода, запрос пароля, выбор файла, календарь и индикатор прогресса вызываются одной строкой. Ответ возвращается в стандартный вывод, отказ даёт ненулевой код возврата, а сама графика может располагаться совсем на другой машине: окно рисует рабочая станция администратора, получающая команды по защищённому соединению. Ниже разобраны механика утилиты, настройка переадресации графической среды и сценарии, которые выручают в реальной работе.
Механика диалогов zenity и передача ответа сценарию через вывод и код возврата
Утилита собрана на графической библиотеке GTK и не требует писать программу с интерфейсом. Каждый диалог это отдельный запуск zenity с набором ключей: окно появляется, человек отвечает, процесс завершается. У сценария есть два канала информации: текст ответа приходит в стандартный вывод, а решение о нажатой кнопке отражает код возврата. Ноль закреплён за основной кнопкой, единица за отменой и закрытием окна, код 5 возвращает сработавший предел времени из ключа --timeout.
ANSWER=$(zenity --entry --title "Имя экземпляра" --text "Укажите короткое имя сервиса")
if [ "$?" -eq 0 ]; then
echo "Принято значение: $ANSWER"
else
echo "Ввод прерван, выходим без изменений"
exit 1
fi
Разделение на текст и код решает коварный случай пустого ввода: пустая строка после основной кнопки и пустая строка после отмены неразличимы, и отличить их способен только код возврата. Есть и тонкость с режимом set -e: присваивание ANSWER=$(zenity --entry ...) завершит сценарий целиком, если человек отменил ввод, потому что код присваивания равен коду утилиты. Когда отмена это нормальная ветка, вызов оборачивают в условие if или временно снимают строгий режим.
Основной набор окон закрывает типовые задачи диалога в сценарии:
- --question рисует вопрос с кнопками, подписи которых меняются ключами --ok-label и --cancel-label;
- --entry и --password дают поле для свободного текста и поле с маскировкой символов;
- --file-selection открывает окно выбора файла или каталога с режимами сохранения и мультивыбора;
- --calendar возвращает дату в формате, заданном ключом --date-format;
- --progress показывает индикатор, который двигают числа из стандартного ввода;
- --notification помещает сообщение в область уведомлений графической среды;
- --list строит таблицу с колонками и режимами одиночного выбора или отметок;
- --forms собирает несколько полей одного окна в единую строку ответа.
Переадресация графической среды по защищённому соединению и переменная DISPLAY
На сервере нет ни монитора, ни графической сессии, и окнам это не мешает. При входе с рабочей станции по SSH с ключом -Y клиент открывает внутри соединения отдельный канал для графических команд: диалог исполняется на сервере, а рисует его машина администратора. Ключ -X включает тот же механизм с дополнительными ограничениями, из-за которых часть приложений работает со сбоями, поэтому -Y применяют для доверенных машин из своего парка.
Настройка сервера сводится к трём шагам. В файле /etc/ssh/sshd_config параметр X11Forwarding получает значение yes, службу перезапускают командой systemctl restart sshd, в ряде дистрибутивов она называется ssh, и устанавливают пакет xauth: без него сервер не оформит права на переадресуемый дисплей и окно не откроется. Проверить результат можно сразу после входа:
echo "$DISPLAY"
zenity --info --title "Проверка" --text "Тоннель графической среды работает"
Переменная окружения DISPLAY получает значение вида localhost:10.0: число перед точкой это номер дисплея, созданного для сессии. Внутри сервера SSH открывает локальный сокет, графические клиенты общаются с ним, как с обычным дисплеем, а весь графический обмен уходит в то же защищённое соединение. Для десятого дисплея служба слушает локальный порт 6010, это видно в выводе утилиты ss. Быструю проверку работающего дисплея делает и утилита xdpyinfo: она печатает сведения об экране или отказывается работать, если переадресация не настроена. Буфер обмена при переадресации общий, текст копируется между окном сервера и приложениями рабочей станции.
Физика процесса простая: сценарий с zenity остаётся серверным процессом, серверу не нужны ни монитор, ни рабочий стол, достаточно графических библиотек самой утилиты. Обратная сторона в привязке к сессии: при обрыве соединения окно исчезает, и сценарий увидит ошибку обращения к дисплею, если не обработает её заранее. Из соображений безопасности переадресацию включают только там, где она нужна: протокол X не изолирует клиентов одного дисплея, программа с доступом к дисплею способна считывать содержимое чужих окон.
Подтверждение опасной операции перед выполнением и запрос пароля без эха в терминал
Самый частый повод для окна в серверном сценарии это действие, которое нельзя отменить. Очистка каталога с кешем, сброс очереди печати, перезапуск рабочего сервиса: такие шаги честнее выполнять после явного согласия, а текст вопроса пишут так, чтобы человек видел последствия до нажатия кнопки.
TARGET="/var/cache/app"
if ! zenity --question --title "Очистка кеша" \
--text "Каталог $TARGET будет удалён целиком. Продолжить?" \
--ok-label "Удалить" --cancel-label "Отмена"; then
echo "Отказ от очистки, выходим"
exit 1
fi
rm -rf "$TARGET"
Второй сюжет про пароли. Ввод пароля в терминале оставляет след и на экране, и в истории оболочки, а окно --password маскирует символы точками и вообще не касается истории. Для sudo принят самый чистый путь: вспомогательный сценарий из двух строк сохраняют в /usr/local/bin/askpass.sh и разрешают исполнение командой chmod 700.
#!/bin/bash
exec zenity --password --title "Права администратора"
Дальше сценарию достаточно экспортировать переменную SUDO_ASKPASS и звать sudo с ключом -A: при запросе пароля программа сама откроет окно, и секрет не появится ни в списке процессов, ни в переменных окружения.
export SUDO_ASKPASS=/usr/local/bin/askpass.sh
sudo -A systemctl restart nginx
Старый способ с промежуточной переменной тоже работает: PASS=$(zenity --password) с передачей в sudo -S через printf. У него есть цена: строка хранится в памяти оболочки, и даже после сброса командой unset какое-то время остаётся в памяти процесса. Когда выбор есть, берут askpass. Ключ --username добавляет в окно пароля поле имени пользователя, и вывод тогда состоит из двух строк, имени и секрета.
Для создания учётной записи с одного экрана удобна форма из нескольких полей. Поля разделяются вертикальной линией, поэтому разбор ответа помещается в одну строку с read:
FORM=$(zenity --forms --title "Новая учётная запись" \
--add-entry "Логин" --add-entry "Полное имя" --add-password "Пароль")
IFS='|' read -r LOGIN FULLNAME PASS <<< "$FORM"
useradd -c "$FULLNAME" "$LOGIN"
printf '%s:%s\n' "$LOGIN" "$PASS" | chpasswd
Выбор файла и каталога в окне диалога вместо ручного набора пути
Опечатка в длинном пути обходится дороже, чем минута на диалог. Ключ --file-selection открывает окно навигации по дереву каталогов, и сценарий получает готовый путь без ручного набора.
DEST=$(zenity --file-selection --directory --title "Каталог для резервной копии" --filename "/srv/")
case "$?" in
0) echo "Каталог выбран: $DEST" ;;
1) echo "Каталог не выбран, остановка" ; exit 1 ;;
esac
Ключ --filename задаёт стартовый каталог окна, --save переводит окно в режим сохранения, а --multiple разрешает отметить несколько файлов: утилита вернёт пути одной строкой через вертикальную линию. Долгую работу сопровождает индикатор: ключ --progress читает стандартный ввод, строка с числом двигает шкалу, строка со знаком решётки меняет подпись под шкалой.
ARCHIVE="$DEST/www-$(date +%F).tar.gz"
(
echo 40
tar -czf "$ARCHIVE" -C /var/www .
echo 90
sha256sum "$ARCHIVE" > "$ARCHIVE.sha256"
echo 100
) | zenity --progress --title "Резервная копия" --text "Идёт упаковка каталога" \
--percentage=0 --auto-close
Когда шкала доходит до сотни, ключ --auto-close закрывает окно сам. Если человеку нужен выбор из фиксированных вариантов, выручает --list: таблица с колонками, одиночный выбор включают --radiolist, режим отметок --checklist, значения при нескольких отметках склеиваются в одну строку.
FORMAT=$(zenity --list --radiolist --title "Формат архива" \
--column "" --column "Вариант" \
TRUE "tar.gz" FALSE "tar.xz" FALSE "zip")
Уведомление о ночном задании и показ окна на экране администратора
Ночная задача по расписанию cron заканчивается в четыре утра, и результат хочется увидеть не в журнале, а на экране. Проще всего случай, когда на самой машине есть графический сеанс с дисплеем :0: офисный терминал с монитором, киоск с автозапуском сеанса, компьютер дежурного. Сценарий, запущенный планировщиком от root, в конце выставляет переменную DISPLAY и вызывает утилиту.
#!/bin/bash
# /usr/local/bin/night-report.sh
/usr/local/bin/make-report.sh > /var/log/night-report.log 2>&1
DISPLAY=":0" zenity --notification --text "Ночной отчёт готов" 2>/dev/null || true
0 2 * * * root /usr/local/bin/night-report.sh
Права на чужой дисплей по умолчанию закрыты. Доступ открывают заранее изнутри сеанса командой xhost +SI:localuser:root, она разрешает локальному root открывать окна на этом экране. Альтернатива это переменная XAUTHORITY с путём к файлу прав пользователя сеанса, но путь зависит от способа входа, поэтому вариант с xhost на практике встречается чаще. Широкую форму xhost + с полным открытием доступа применять не стоит: она снимает проверку прав со всех клиентов дисплея.
Когда администратор вечером заходит на сервер с переадресацией, тот же сценарий покажет окно на его рабочей станции, потому что DISPLAY уже указывает на туннель. Окно существует, пока не закрыто соединение: длинное задание уводят в tmux или nohup, уведомление ставят в самый конец и не завязывают на него успех задания. Сообщения без вопроса дают ключи --info, --warning и --error, у них разные пиктограммы и подписи, механика одна.
Проверка дисплея, таймауты и поведение сценария при отсутствии графической среды
Графический канал это услуга, а не гарантия: утилита может отсутствовать на сервере, дисплей может не существовать, и хороший сценарий встречает обе ситуации спокойно. Перед вызовом окна проверяют саму утилиту и переменную окружения, а при отсутствии графики откатываются на обычный вопрос в терминал.
command -v zenity >/dev/null 2>&1 || { echo "zenity не установлена" >&2; exit 1; }
if [ -n "$DISPLAY" ]; then
NODE=$(zenity --entry --title "Узел обслуживания" --text "Укажите имя узла")
[ "$?" -ne 0 ] && exit 1
else
read -r -p "Укажите имя узла: " NODE
fi
Вторая привычка вежливого сценария это таймаут. Ключ --timeout закрывает окно через заданное число секунд, утилита возвращает код 5, и отсутствие ответа для опасного вопроса безопаснее трактовать как отказ:
zenity --question --text "Запустить пересборку индекса?" --timeout=30
case "$?" in
0) /usr/local/bin/reindex.sh ;;
1) echo "Отказ, пересборка пропущена" ;;
5) echo "Ответа нет, пересборка пропущена" ;;
esac
Зависимости стоит знать заранее: утилита собрана на GTK, менеджер пакетов поставит библиотеки сам, но на минимальном сервере установка потянет заметный объём зависимостей, и это плата за окна. Работающий рабочий стол на сервере при этом не нужен: достаточно установленной утилиты и переменной DISPLAY, которая указывает на доступный дисплей.
Окна в серверных сценариях не заменяют терминал, они закрывают узкую задачу: спросить человека там, где цена ошибки высока. Крепкий сценарий задаёт понятный вопрос, терпеливо ждёт ответа, переносит отсутствие графики и не теряет главного признака серверной работы, повторяемости. Логика остаётся на сервере, окно появляется там, где сидит человек, и в этом разделении весь смысл затеи.