Демоны Ада

Часть 2

В прошлой статье мы рассмотрели общий для всех языков алгоритм создания демона. К сожалению выяснилось, что после вызова fork возникает небольшая проблема при использовании компилятора GNAT (хочу заметить что я не ручаюсь за то, что проблема только одна) — при попытке создать свои собственные обработчики системных прерываний программа наглухо зависает. Не знаю кто виноват, важней решить что можно сделать.

Изготавливаем костыль

Попробуем заменить fork полноценным запуском приложения (fork + execv). Вариант первый: сделать внешний костыль, который запускает демона и завершается. Вариант второй — процесс запускает самого себя и завершается, а к параметрам добавляет специальный секретный сигнализатор, например -daemonize. Остановимся на втором варианте.

Вместе с параметром -daemonize нам придется также передать все остальные параметры, указанные при запуске (если они были). Конечно же это не обязательно — но в один прекрасный момент нам захочется через параметры указывать демону какой конфиг читать или что‐то еще, так что приготовимся к этому сразу. Единственное что может потом нервировать — так это постоянно помнить о том, что один из параметров „левый“.

Сделаем функцию, которая соберет все переданные параметры воедино и последним добавит «-daemonize":


          

          
function Get_Daemonize_Args return Argument_List is
   use Ada.Strings.Unbounded;
   Args : Unbounded_String := Null_Unbounded_String;
begin
   if Argument_Count = 0 then
      return Argument_String_To_List ("--daemonize").all;
   else
      for i in 1 .. Argument_Count loop
         Args := Args & To_Unbounded_String (Argument (i) & " ");
      end loop;
      Args := Args & To_Unbounded_String ("--daemonize");
      return Argument_String_To_List (To_String (Args)).all;
   end if;
end Get_Daemonize_Args;

Осталось научить демона полноценно запускать самого себя. Для этого воспользуемся методом Non_Blocking_Spawn из пакета Gnat.OS_Lib


          

          
ID : Process_ID;

if Argument_Count = 0 or else Argument (Argument_Count) /= "--daemonize" then
   ID := Non_Blocking_Spawn (Command_Name, Get_Daemonize_Args);
   if ID = Invalid_Pid then
      Log.Put ("Spawn return Invalid_Pid");  --  Пакет Log будет рассмотрен позже
      OS_Exit (-1);
   end if;
   Log.Put ("Pid is:" & Integer'Image (PID_To_Integer (ID)));
   OS_Exit (0);
end if;

То есть при запуске проверяем передавался ли параметр «-daemonize», если нет — запускаем самого себя уже с ним и завершаем выполнение.

Дальше можно выполнять остальные требования по демонизации процесса.

Вообще, во время работы демона желательно держать открытыми дескрипторы стандартных потоков ввода/вывода, поскольку они могут понадобиться различным функциям и процедурам стандартной библиотеки. Самое простое — после их закрытия (отстыковки от терминала) заново переоткрыть например на /dev/null :


          

          
Stdin  : File_Descriptor := Standin;
Stdout : File_Descriptor := Standout;
Stderr : File_Descriptor := Standerr;

Close (Stdin);
Close (Stdout);
Close (Stderr);
Stdin  := Open_Read_Write ("/dev/null", Text);
Stdout := Open_Read_Write ("/dev/null", Text);
Stderr := Open_Read_Write ("/dev/null", Text);

Желательно вообще не начинать каких‐либо еще операций с файлами до и во время этого блока, только после (для параноидальности можно еще проверить что Stdin действительно стал 0, Stdout = 1, а Stderr = 2).

Обработка прерываний

С помощью обработки сигналов организуются многие вещи, например корректный останов/перезапуск, «передёргивание» лог‐файлов и прочее.

Создадим простой шаблон обработчика прерываний, для этого его желательно (если вообще не необходимо) расположить в отдельном пакете

Спецификация:


          

          
-- File: sig.ads
with Ada.Interrupts;
with Ada.Interrupts.Names;

package SIG is

   SIGTERM : Ada.Interrupts.Interrupt_ID := Ada.Interrupts.Names.SIGTERM;

   protected type P_SIG (Int_ID : Ada.Interrupts.Interrupt_ID) is
      procedure Handler;
      pragma Attach_Handler (Handler, Int_ID);
   end P_SIG;

   type P_SIG_Access is access P_SIG;

end SIG;

Реализация:


          

          
-- File: sig.adb
with Log;

package body SIG is

   protected body P_SIG is
      procedure Handler is
         use Ada.Interrupts;
      begin
         if Int_ID = SIGTERM then
            Log.Put ("Signal:" & Int_ID'Img & ". Fuck Yea!");
         else
            Log.Put ("Signal:" & Int_ID'Img & ".");
         end if;
      end Handler;
   end P_SIG;

end SIG;

Здесь мы использовали вложенный стиль установки обработчика стандартной модели прерываний Ада.

Теперь повесим на него обработку 15‐го сигнала (SIGTERM) и, например, 10‐го (USR1, хотя номер может отличаться в разных системах)


          

          
SIGTERM_Access : SIG.P_SIG_Access;
SIGUSR1_Access : SIG.P_SIG_Access;

SIGTERM_Access := new SIG.P_SIG (SIG.SIGTERM);
SIGUSR1_Access := new SIG.P_SIG (10);

Проверить можно командой kill

$ kill pid

По умолчанию посылается SIGTERM

$ kill -USR1 pid

Логи, syslog

В качестве примера сначала сделаем простой метод для сохранения логов (Log.Put упоминавшийся ранее). В конечном итоге он будет реализован в виде защищенной процедуры, пока приведем реализацию:


          

          
FN : constant String := "/tmp/adaemon.log";

procedure Put (Msg : String) is
   use Ada.Text_IO;

   FD : File_Type;
   Time : Ada.Calendar.Time := Ada.Calendar.Clock;
begin
   if not Ada.Directories.Exists (FN) then
      Create (FD, Out_File, FN);
   else
      Open (FD, Append_File, FN);
   end if;
   Put_Line (FD, Ada.Calendar.Formatting.Image (Time) & " " & Msg);
   Close (FD);
end Put;

Тут все тривиально. Теперь посмотрим как можно отправлять сообщения в системный журнал. Для этого нужно воспользоваться функциями openlog, syslog и closelog, описанными в syslog.h. К сожалению, функция syslog имеет переменное число аргументов и ее нельзя корректно импортировать в Аду (ввиду особенностей ABI архитектуры amd64). Изрядно помучавшись пришлось сделать Си‐шную обертку и импортировать уже ее.

// File: syslog_wrapper.c
#include <syslog.h>

syslog_wrapper (int priority, const char* msg) {
   openlog ("ada_test", LOG_NDELAY|LOG_PID, LOG_DAEMON);
   syslog(priority, "%s", msg);
   closelog();
}

          

          
procedure Syslog (Msg : String) is
   use Interfaces.C;
   use Interfaces.C.Strings;

   procedure Syslog_Wrapper (Priority : int; Msg : chars_ptr);
   pragma Import (C, Syslog_Wrapper, "syslog_wrapper");

begin
   Syslog_Wrapper (6, New_String (Msg));  -- 6 - LOG_INFO
end Syslog;

Echo сервер

В качестве заключительного примера сделаем простой сетевой Echo сервер


          

          
-- File: main.adb

with LOG;
with Gnat.Sockets;
with Ada.Exceptions;
with Ada.IO_Exceptions;

use Gnat.Sockets;

procedure Main is

   Address  : Sock_Addr_Type;
   Server   : Socket_Type;
   Socket   : Socket_Type;
   Channel  : Stream_Access;

begin

   Initialize;
   Create_Socket (Server);
   Set_Socket_Option (Server, Socket_Level, (Reuse_Address, True));
   Bind_Socket (Server, (Family_Inet, Inet_Addr ("127.0.0.1"), 2300));
   Listen_Socket (Server);
   loop
      Accept_Socket (Server, Socket, Address);
      Log.Syslog ("Client connected from " & Image (Address));
      Channel := Stream (Socket);
      declare
      begin
         loop
            Character'Output (Channel, Character'Input (Channel));
         end loop;
      exception
         when Ada.IO_Exceptions.End_Error =>
            null;
      end;
      Close_Socket (Socket);
   end loop;

exception
   when The_Event: others =>
      Log.Put (Ada.Exceptions.Exception_Information (The_Event));
end Main;

Тут без комментариев, после выполнения всех шагов по демонизации просто вызываем Main.

Архив с собранными воедино исходниками можно скачать здесь

Сборка:

$ gcc -c syslog_wrapper.c
$ gnatmake daemon -largs syslog_wrapper.o

После удачной сборки и запуска проверяем работу:

$ telnet 127.0.0.1 2300

Пробуем посылать сигналы демону через kill и проверяем лог файл /tmp/adaemon.log, а также пишется ли что‐нибудь в syslog от демона при подключении (скорее всего это будет /var/log/daemon.log).

PS. Не забудьте вырубить демона через kill -9 :-D


(c) Gavrikov Valeriy ([email protected]), April 2012

Автор: Валерий Гавриков
Дата: 13.04.2012