Показаны сообщения с ярлыком daemon. Показать все сообщения
Показаны сообщения с ярлыком daemon. Показать все сообщения

вторник, 15 декабря 2009 г.

RESTAS-daemon

Ранее я рассказывал, как можно написать демона на "pure lisp" и таким образом обойтись без граблей в виде Screen или detachtty. С тех пор, у меня развелось некоторое колличество подобных демонов для разных сервисов, с общим базовым кодом и незначительными отличиями. Я использовал технику copy/paste, что меня сильно огорчало, да и неудобства заметные - время от времени я правлю базовый код и изменения надо руками разносить по разным файлам (на разных серверах). Желание сделать некую общую схему с повторным использованием у меня было с самого начала, но вот сделать универсальный "демонизатор" не так просто. В итоге, я решил отказаться от универсального решения, и сделать скрипт для запуска как демонов web-приложений на базе RESTAS. Это довольно просто, ибо все эти приложения запускаются однотипно: загрузить нужную asdf-систему и вызвать restas:start-site с именем сайта, определенным с помощью макроса restas:defsite. Получившийся скрипт я назвал restas-daemon.lisp и положил в директорию contrib пакета RESTAS. Теперь запустить демона для web-приложения на базе RESTAS очень просто:
sbcl --noinform --no-userinit --no-sysinit --load /path/to/restas/contrib/restas-daemon.lisp /path/to/daemon.conf COMMAND
Как видно, данный скрипт принимает два параметра: путь к файлу конфигурации демона и имя команды. Поддерживаются следующие команды:
  • start - запуск
  • stop - остановка
  • restart - рестарт
  • kill - убийство, если почему-то демон не хочет останавливаться с помощью stop
  • zap - удалят pid-файл, это может потребоваться, если демон был остановлен как-нибудь грубо
  • nodaemon - в основном тоже, что и start (в том числе происходит смена прав и т.п.), но при этом процесс не делает себе fork и не отключается от терминала, что даёт возможность наблюдать весь его вывод - удобно для отладки демона.
Файл конфигурации демона может содержать определение нескольких переменных, влияющих на поведение демона, вот полный конфиг моего домашнего демона:
(defparameter *name* "homesite")

(defparameter *user* "andrey")

(defparameter *swankport* 9000)

(defparameter *asdf-central-registry*
'(#P"/usr/share/common-lisp/systems/" #P"/home/andrey/development/common-lisp/common-lisp-systems/"))

(defparameter *asdf-load-systems* '(#:homesite))

(defparameter *sites* '((#:homesite nil 80)))
Возможны следующие параметры:
  • *name* - имя демона, в данный момент единственный обязательный параметр
  • *user* - имя пользователя, от которого должен работать демон, по-умолчанию совпадает с именем демона
  • *group* - группа пользователя
  • *fasldir* - путь к каталогу, в который будут складываться fasl-файлы, по-умолчанию определяется из имени демона как (format nil "/var/cache/~A/fasl/" *name*)
  • *pidfile* - pid-файла демона, по-умолчанию (format nil "/var/run/~A/~A.pid" *name* *name*)
  • *swankport* - порт, на котором следует запускать swank, если не указан или nil, то swank не запускается
  • *default-host-redirect* - в RESTAS сайты запускаются с помощью restas:start-site, при этом можно указать имя хоста (аналог виртуальных хостов), данная переменная указывает хост, на который будет производиться редирект, если для хоста запроса нет сайта, например, на lisper.ru можно попасть набрав в браузере www.lisper.ru, lisp.catap.ru или просто прямой адрес, все такие запросы перенаплавляются на lisper.ru
  • *asdf-central-registry* - список директорий, в которых будет производиться поиск систем
  • *asdf-load-systems* - список систем, которые необходимо загрузить при старте демона
  • *sites* - список сайтов, которые необходимо запустить при запуске демона с помощью функции restas:start-site. В качестве сайта может быть указано просто имя, либо список, например '(#:rulisp "lisper.ru" 80). Сайты определяются с помощью restas:defsite - это происходит при загрузке указанных asdf-систем.
Для систем на базе Gentoo есть специальная поддержка. При установке restas с помощью моего форка gentoo-lisp-overlay в каталог /etc/init.d/ добавляется скрипт restas.lo. Теперь, для создания initd-скрипта необходимо создать симлинк на этот файл и создать конфигурационный файл в /etc/conf.d/, например:
cd /etc/init.d/
ln -s restas.lo homesite
emacs /etc/conf.d/homesite.conf
/etc/init.d/homesite start
Схема может быть ещё не совсем устоявшаяся, но уже рабочая :)

воскресенье, 20 сентября 2009 г.

Common Lisp Daemon. Часть 2.

В предыдущей части я рассказал о преодолении основных технических трудностей, возникающих при создании lisp-демонов. Теперь я готов опубликовать код законченного lisp-демона. Полный код можно посмотреть здесь: http://lisper.ru/apps/format/15. Это реальный код, который используется для запуска сайта lisper.ru. Как мне кажется, код достаточно тривиален и по структуре в основном соответствует структуре демонов, написанных на языке C (информацию о принципах разработки демонов на языке С легко найти в сети). Отдельно хотел бы отметить, что данный демон позволяет запускать hunchentoot на 80-ом порту уже после отказа от root-привилегий, что достигается за счёт использования библиотеки libcap (версии 2). При использования данного демона нет необходимости в вспомогательных средствах: GNU Screen, detachtty и т.п. (которые традиционно рекомендуются для решения данной проблемы). Ну и данный код, в том числе, можно рассматривать как пример системного программирования на Common Lisp :) К сожалению, в код пришлось добавить пару платформо-зависимых констант, но я надеюсь при случае написать патч для SBCL, после чего необходимость в этом отпадёт.

Изменив всего несколько строк можно приспособить данный код для запуска другого демона. Но подобная техника, конечно, не есть "хорошо", поэтому я сейчас раздумываю об написании библиотеки, которая позволит создавать демонов в несколько строк кода.

среда, 16 сентября 2009 г.

Common Lisp Daemon. Часть 1.

При развертывании Common Lisp приложения (например, сайта) на сервере (естественно, под управление GNU/Linux) возникает проблема организации lisp-демона, что связано с некоторыми техническими проблемами, которые, вероятно, могут вызвать у начинающих некоторое непонимание (или даже ужас).

Корень этих проблем в том, что обычно lisp процесс не может быть оторван от терминала (причины этого восходят к тем далёким временам, когда компьютеры были большими...) и при закрытии стандартного потока ввода немедленно завершает свою работу.

Для решения этой проблемы наибольшую известность получило решение на базе GNU Screen. Менее известен, но также используется метод основанный на использовании detachtty.

Я пробовал оба этих варианта и совершенно не удовлетворён ни одним из них (хотя и должен заметить, что решение на базе GNU Screen значительно удобней). В конце концов, это просто не нормально, что для решения проблемы демонизации необходимо использовать "не-lisp" инструменты, а итоговое решение очень сильно напоминает хак. С другой стороны, почему бы не реализовать демонизацию средствами самого Common Lisp, что здесь невозможного? Если исходить только из стандарта Common Lisp, то, очевидно, задача действительно нерешаема (ибо разработчики этого самого стандарта кажется совершенно не размышляли на данную тему), но если взять конкретную реализацию? Я попробовал реализовать pure-lisp демонизацию для SBCL и выяснил, что это довольно тривиально.

Итак, что должен делать приличный демон? Обычно это:
  • Отсоединиться от создавшего его родительского процесса (сделать fork)
  • Снизить свои привилегии до минимально возможных (ведь обычно демоны запускаются от имени root)
  • Закрыть стандартные потоки ввода/вывода/ошибок (0, 1, 2)
  • Сигнализировать об успешном запуске (либо сообщить об ошибке)
Все эти действия могут быть реализованы тривиальным образом.

Отсоединение от родительского процесса

Стандартно это делается за счёт системного вызова fork и ничего сложного из себя не представляет. Тут даже и говорить особо нечем:
(unless (= (sb-posix:fork) 0)
(quit))
Реальный код, конечно, будет немного сложнее, но так тоже работает.

Снижение привилегий

Работать от root демону совершенно ни к чему, поэтому:
(defun change-user (name &optional group)
(let ((gid)
(uid))
(when group
(setf gid
(sb-posix:group-gid (sb-posix:getgrnam group))))
(let ((passwd (sb-posix:getpwnam name)))
(unless group
(setf gid
(sb-posix:passwd-gid passwd))
(setf uid
(sb-posix:passwd-uid passwd))))
(sb-posix:setgid gid)
(alien-funcall (extern-alien "initgroups" (function int c-string int)) name gid)
(sb-posix:setuid uid)))

;;; устанавливаем владельцем процесса пользователя rulisp
(change-user "rulisp")
Данный код, правда, содержит очевидный изъян (если вспомнить о saved user id), но для демонстрации принципиальной возможности это не критично.

Переключение на псевдо-терминал

Просто так закрыть стандартный ввод нельзя (это приведёт к завершению процесса), но его можно переключить на псевдо-терминал. При использовании GNU Screen происходит переключение (на псевдо-терминал) всех стандартных потоков, но я использую GNU Screen только для того, чтобы увидеть какие ошибки произошли при запуске демона, т.е. полный сервис, предоставляемый GNU Screen является совершенно избыточным. Поэтому, я считаю достаточным связать стандартный поток ввода с псевдо-терминалом (для предотвращения угрозы завершения процесса, после чего о нём вообще можно забыть), а стандартные потоки вывода и ошибок просто направить в файл (что даст возможность быстро просмотреть как происходила загрузка демона):
(defun switch-to-slave-pseudo-terminal (out)
(flet ((c-bit-or (&rest args)
(reduce #'(lambda (x y) (boole boole-ior x y))
args)))
(let* ((fdm (sb-posix:open #P"/dev/ptmx" sb-posix:O-RDWR))
(slavename (progn
(alien-funcall (extern-alien "grantpt" (function int int)) fdm)
(alien-funcall (extern-alien "unlockpt" (function int int)) fdm)
(alien-funcall (extern-alien "ptsname"
(function c-string int)) fdm)))
(fds (sb-posix:open slavename sb-posix:O-RDONLY))
(log (sb-posix:open out
(c-bit-or sb-posix:O-WRONLY sb-posix:O-CREAT sb-posix:O-TRUNC)
(c-bit-or sb-posix:S-IREAD sb-posix:S-IWRITE sb-posix:S-IROTH))))
(sb-posix:dup2 fds 0)
(sb-posix:dup2 log 1)
(sb-posix:dup2 log 2))))

;;;; переключаем стандартный поток ввода на пседо-терминал,
;;;; а потоки вывода и ошибок направляем в /tmp/log
(switch-to-slave-pseudo-terminal #P"/tmp/log")
До сих пор мне не приходилось работать с псевдотерминалом, так что мог что-нибудь сделать не вполне корректно, но на моей системе работает без проблем.

Сигнализация об успешном запуске демона

После вызова fork получается два процесса, один из которых становится демоном, а другой завершается и, вообще говоря, должен сигнализировать об успешности запуска демона. При использовании решения на основе GNU Screen такого эффекта добиться не получается: узнать об успешности запуска демона можно только по результатам его работы. Для решения этой проблемы можно использовать демона с обратной связью, что требует возможности обработки сигналов. Послать сигнал родительскому приложению очень просто:
(sb-posix:kill (sb-posix:getppid) sb-posix:sigusr1)
Для обработки сигналов в SBCL можно воспользоваться функцией sb-sys:enable-interrupt:
(sb-sys:enable-interrupt sb-posix:sigusr1 #'usr1-signale-handler)
Итого:
Все технические проблемы, возникающие при создании демона, для SBCL решаются довольно тривиальным образом и необходимости в использовании GNU Screen и т.п. средств нет.

P.S. Это были технические детали, а в следующей части я покажу законченное решение, использующее описанные возможности (а также кое-что ещё) для запуска реального "Common Lisp Daemon".