Daemons and Services

From Free Pascal wiki
Revision as of 03:54, 3 February 2022 by Nimral (talk | contribs) (→‎Linux)
Jump to navigationJump to search

English (en) español (es) français (fr) polski (pl) português (pt) русский (ru)

What are daemons, services and agents?

Unix daemons and Windows services are system-wide programs running without user interaction; macOS agents are per user programs (cf system-wide daemons) that may or may not run without user interaction. Although the nomenclature differs, their function is similar: for example www or ftp servers are called daemons under Linux and services under Windows. Because they do not interact with the user directly, they close their stdin, stdout, stderr descriptors at start.

With Free Pascal, Lazarus it is possible to write these daemons/services platform-independent via the Lazarus lazdaemon package. To avoid name conflicts with the Delphi components these classes are called 'daemons'.

Install the LazDaemon package

Before you can start, install the LazDaemon package. Either via Components / Configure installed packages or by opening/installing the lpk file directly: lazarus/components/daemon/lazdaemon.lpk. This package installs some new components and menu items in the IDE:

Under File - New: 3 items appear in the dialog, under the heading: "Daemon (service) applications":

2022-01-31 17 31 50-New ....png

Creating your first Service Daemon

After having installed the LazDaemon package, from the File-New Menu, pick "Daemon (service) application)". This will automatically create two units: one for a TDaemon descendant, and one for a TDaemonMapper descendant. This is what the scaffolded units look like:

unit DaemonUnit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, DaemonApp;
 
type
  TDaemon1 = class(TDaemon)
  private

  public

  end;

var
  Daemon1: TDaemon1;

implementation

procedure RegisterDaemon;
begin
  RegisterDaemonClass(TDaemon1)
end;

{$R *.lfm}

initialization
  RegisterDaemon;
end.
unit DaemonMapperUnit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, DaemonApp;

type
  TDaemonMapper1 = class(TDaemonMapper)
  private

  public

  end;

var
  DaemonMapper1: TDaemonMapper1;

implementation

procedure RegisterMapper;
begin
  RegisterDaemonMapper(TDaemonMapper1)
end;

{$R *.lfm}

initialization
  RegisterMapper;
end.

The TDaemon class, which is a TDataModule descendant, does mainly respond to the various service control messages sent by the OS. For the "worker" part you are supposed to create a thread of its own, see sample code below, don't try to use the execute method provided by the TDaemon class. The TDaemonMapper contains data structures describing the service. Both classes need to be registered, which, in this code sample, is done in the initialization section of each unit. Note that both units have a resource file, which gives you convenient access to all the event handlers supported by those classes from within the Lazarus GUI using the Forms Editor [F12]. The main application component for a service-style application is introduced by putting a reference to DaemonApp into the uses secion of each unit. It provides a complete service framework including advanced features like install and uninstall support, and logging.

Populating the DaemonMapper Class

For the moment let's just fill some basic properties to get things going.

2022-01-31 23 32 52-Settings.png

For later use, note the WinBindings property, which lets you configure various service properties for use by the Windows Service Manager.

Now ... how about testing what we have achieved so far? Sure, our daemon doesn't have any execute routine so far, but configuring the DaemonMapper provides all required infos for installing and uninstalling the service on the operating system. To test, compile your application, and then you need open a command line window with elevated privileges, because normal users are by default not allowed to install Windows services. Navigate to the directory where you compiled your test application into, and try out the following commands (sc ist the built-in command line tool to control Windows services from the command line):

TestDaemon -install install the daemon
sc query TestDaemon check the service status, of course, our service doesn't handle any events yet, so any attempt to start it it runs into an error, but note: the service got known by sc, hence it was installed successfully.
TestDaemon -uninstall remove the daemon, so we can enhance its code for the next try
sc query TestDaemon check whether the service was indeed uninstalled

2022-01-31 23 50 28-Lazarus IDE v2.2.0RC2 - Daemon application.png

Screenshots taken 2/2022 on a Windows 11 machine.

Writing the Daemon Methods

TDaemons support the following methods:

Start
Called when daemon should start. This method must return immediately with True.
Stop
Called when daemon should stop. This method must return immediately with True.
Shutdown
Called when daemon should be killed. This method must stop the daemon immediately and return with True. This is not triggered under Linux. Linux simply kills the daemon.
Pause
Called when daemon should pause. This method must return immediately with True. Under Linux this is not triggered because the kernel stops the whole daemon on STOP and continues it on CONT.
Continue
Called when daemon should continue after a pause. This method must return immediately with True. Under Linux this is not triggered.
Install
Called when daemon is registered as a Windows service. This method should return True on success.
Uninstall
Called when daemon is unregistered as a Windows service. This method should return True on success.
AfterUnInstall
Called after daemon was unregistered as a Windows service. This method should return True on success.
HandleCustomCode
Called when a special signal was sent to the daemon. This method should return True on success.

The following sample code has been tested on Windows (and soon on Linux). You will probably note that it has not been created using the "Daemon (service) application" template, but written from scratch. This is because the forum currently does not allow the upload of archive/package formats. Therefore it is not possible to publish a complete GUI project including all required resource files. Until this issue is resolved, this rather standalone source containing a .lpr and one single unit file is the best option available.

Here is the unit, which contains all the important code:

unit TestDaemonUnit;

// -------------------------------------------------------------------------------
// Demo application on how to use the TDaemon application type
// 2/2022 by arminlinder@armnninlinder.de

// Based on document
// "Taming the daemon: Writing cross-platform service applications in FPC/Lazarus"
// by Michaël Van Canneyt, February 4, 2007
// -------------------------------------------------------------------------------

{$mode objfpc}{$H+}
{$WARN 5043 off : Symbol "$1" is deprecated}
interface

uses
  Classes, SysUtils, DaemonApp, IniFiles;

const
  DAEMON_VERSION = '1.1';       // Just for logging
  DAEMON_NAME = 'TestDaemon';
  DAEMON_CONFIG_FILE_PATH = '/lib/systemd/system/' + DAEMON_NAME + '.service';

type

  { TDaemonThread }

  // This is the "workhorse" of the daemon
  // Execute holds the main work code of the service
  // Do not try the execute method of TDaemon, since it does not multitask,
  // the service thread will stop responding to control messages if looping in the
  // TDaemon execute method. Thus we need a worker thread.

  TDaemonThread = class(TThread)
  private
  public
    procedure Execute; override;  // the worker thread
    constructor Create;
    destructor Destroy; override;
  end;

  { TTestDaemonMapper }

  // This class is providing configuration the data for the daemon

  TTestDaemonMapper = class(TCustomDaemonMapper)
  private

  public
    constructor Create(aOwner: TComponent); override;
  end;

  { TTestDaemon }

  // The main control structure of the daemon
  // Comprising of event handlers for the various TDaemon events
  // supported by TDaemon

  TTestDaemon = class(TCustomDaemon)
    // Events called through -install and -uninstall command line
    function Install: boolean; override;
    function UnInstall: boolean; override;
    // Service start and stop
    // Usually triggered by the service control of the operating system
    function Start: boolean; override;
    function Stop: boolean; override;
    // Service pause and resume
    // Supported by Windows only
    function Pause: boolean; override;
    function Continue: boolean; override;
  private
    FDaemonThread: TDaemonThread; // The worker thread
  end;

implementation

// ---------------------------------------------------------------------
// Quick and dirty write log message to file
// Note: TDaemonApplication has extensive logging capabilities built-in!
// ---------------------------------------------------------------------

procedure LogToFile(aMessage: string);

  function TimeStamped(S: string): string;

    // Return a timestamped copy of a string

  begin
    Result := FormatDateTime('hh:mm:ss', now) + ' ' + S;
  end;

var
  f: Text;
  LogFilePath: string;

begin
  // create a daily log file in the .exe directory
  LogFilePath := IncludeTrailingPathDelimiter(ExtractFilePath(Application.ExeName)) +
    FormatDateTime('YYYYMMDD', now) + '.log';
  AssignFile(f, LogFilePath);
  try
    if FileExists(LogFilePath) then
      Append(f)
    else
    begin
      Rewrite(f);
      writeln(f, TimeStamped('Log created'));
    end;
    Writeln(f, TimeStamped(aMessage));
  finally
    CloseFile(f);
  end;
end;

{ TTestDaemonMapper }

constructor TTestDaemonMapper.Create(aOwner: TComponent);

var
  D: TDaemonDef;

begin
  inherited Create(aOwner);
  D := DaemonDefs.Add as TDaemonDef;
  D.DisplayName := 'Test Daemon ' + DAEMON_VERSION;
  D.Name := DAEMON_NAME;
  D.DaemonClassName := 'TTestDaemon';
  {$IFDEF WINDOWS}
  D.Description := 'A Sample demonstrating how to program a service';
  D.WinBindings.ServiceType := stWin32;
  D.WinBindings.StartType := stManual;   // if you want start on boot: change to stAuto
  {$ENDIF}
  {$IFDEF UNIX}
  D.Description := 'A Sample demonstrating how to program a daemon';
  {$ENDIF}
end;

{ TDaemonThread }

// ----------------------------------------------------------------------
// The thread providing the execute method of the worker thread.
// ----------------------------------------------------------------------

procedure TDaemonThread.Execute;

// This is the main workhorse, the routine which does
// actually hold the service working code

var
  i: integer;

begin
  LogToFile('Daemon worker thread executing');
  while not Terminated do
  begin
    // placeholder, put your actual service code here
    // ...
    LogToFile('Daemon worker thread running');
    // Thread- and CPUload friendly 5s delay loop
    for i := 1 to 50 do
    begin
      if Terminated then break;
      Sleep(100);
    end;
    // ...
    // ----------------------------------------
  end;
  LogToFile('Daemon worker thread terminated');
end;

// Construction and destruction of the worker thread

constructor TDaemonThread.Create;

begin
  // Create a suspended worker thread
  // The actual thread execution will happen if the OS sends a "Start" Signal
  // See OnStart event handler
  inherited Create(True);
  LogToFile('Daemon worker thread created');
end;

destructor TDaemonThread.Destroy;

begin
  // Nothing to do here, just for logging
  LogToFile('Daemon worker thread destroyed');
  inherited Destroy;
end;

{ TTestDaemon }

// ---------------------------------------------------------
// This is the main scaffolding of the thread, containing
// the event handlers responding to control signals from the
// operating system service control facility
// ---------------------------------------------------------

// Installation and uninstallation events
// Windows is handled well by the code inherited from TCustomDaemon
// For Linux, we create a systemd config file in /lib/systemd/system
// See Unix system doc, e.g. at https://www.freedesktop.org/software/systemd/man/systemd.service.html

function TTestDaemon.Install: boolean;

var
  f: TIniFile;
  FilePath: string;

begin
  Result := inherited Install;
  {$IFDEF UNIX}
  FilePath := LowerCase(DAEMON_CONFIG_FILE_PATH);   // mixed case names unusual in Linux
  LogToFile(Format('Creating systemd config file: %s', [FilePath]));
  try
    f := TIniFile.Create(FilePath,[]);
    try
      // We use the cefinition given in the mapper class
      // The mapper class is assigned to the "Definition" property
      f.WriteString('Unit', 'Description', self.Definition.Description);
      f.WriteString('Unit', 'After', 'network.target');
      f.WriteString('Service', 'Type', 'simple');
      f.WriteString('Service', 'ExecStart', Application.ExeName + ' -r');
      f.WriteString('Install', 'WantedBy', 'multi-user.target');

      // Taken from old file sample, IMHO not required
      // f.WriteString('Service', 'TimeoutSec', '25');
      // f.WriteString('Service', 'RemainAfterExit', 'yes');

    except
      on e: Exception do
      begin
        LogToFile(e.Message);
        Result := False;
      end;
    end;
  finally
    f.Free;
  end;
  write('Daemon control file ' + FilePath);
  if result then
    writeln(' successfully created/modified')
  else
    writeln(' create/modify failed');
  {$ENDIF}
  if Result then
    LogToFile(Format('Daemon %s installed', [DAEMON_VERSION]))
  else
    LogToFile(Format('Daemon %s not installed', [DAEMON_VERSION]));
end;

function TTestDaemon.UnInstall: boolean;

var
  FilePath: string;

begin
  Result := inherited Uninstall;
  {$IFDEF UNIX}
  // For Linux, we remove the systemd config file from /lib/systemd/system
  FilePath := LowerCase(DAEMON_CONFIG_FILE_PATH);   // mixed case names unusual in Linux
  if FileExists(FilePath) then
  begin
    LogToFile(Format('Removing systemd config file: %s', [FilePath]));
    result := DeleteFile(FilePath);
    write('Daemon control file ' + FilePath);
    if result then
      writeln(' successfully removed')
    else
      writeln(' remove failed');
  end;
  {$ENDIF}
  if Result then
    LogToFile(Format('Daemon %s uninstalled', [DAEMON_VERSION]))
  else
    LogToFile(Format('Daemon %s not uninstalled', [DAEMON_VERSION]));
end;

// Service pause and continue signals
// Supported on Windows only
// You would hardly find this simple approach to suspend the worker thread too useful
// in a production environment. Simply suspending the worker thread at any time
// will hardly produce a meaningful result

function TTestDaemon.Pause: boolean;

begin
  // Windows only, probably deprecated
  FDaemonThread.Suspend;
  LogToFile('Daemon paused');
  Result := True;
end;

function TTestDaemon.Continue: boolean;

begin
  // Windows only, probably deprecated
  FDaemonThread.Resume;
  LogToFile('Daemon continuing');
  Result := True;
end;

// Service start and stop signals

function TTestDaemon.Start: boolean;

begin
  LogToFile('Daemon received start signal');
  // Create a suspended worker thread
  FDaemonThread := TDaemonThread.Create;
  // Parametrize it
  FDaemonThread.FreeOnTerminate := False;
  // Start the worker
  FDaemonThread.Start;
  Result := True;
end;

function TTestDaemon.Stop: boolean;

begin
  LogToFile('Daemon received stop signal');
  // stop and terminate the worker
  FDaemonThread.Terminate;
  // Wait for the thread to terminate.
  FDaemonThread.WaitFor;
  LogToFile('Daemon stopped');
  Result := True;
end;

end.

And here is the lpr file:

program TestDaemon;

uses
  {$IFDEF UNIX}
  CThreads,
  {$ENDIF}
  daemonapp,
  TestDaemonUnit { add your units here };

begin
  RegisterDaemonClass(TTestDaemon);
  RegisterDaemonMapper(TTestDaemonMapper);
  Application.Title := 'Daemon Test Application';
  Application.Run;
end.

2022-02-01 12 17 27-TestDaemon.png

Service Installation

Windows

You can install the service from any elevated command prompt by starting the executable with the -install parameter.

After successful installation you can control the service either command line (the command is "sc"), or by GUI using the "Services" management console. Note that most service manager properties, like autostart on boot, can be set from inside the service too, see the TTestDaemonMapper.create in the source code, and the various options provided by the TDaemonDef.WinBindings property.

The most important SC commands are:

sc start {service-name} start the service
sc query {service-name} query the service state
sc stop {service-name} stop the service

You may also use the Windows service control manager console.

ServiceControlManager.png

Linux

Unlike for Windows, the TDaemonApp application does not have any support for an automated installation on Linux, since there is a wide variety of service control mechanisms. You can implement your own installation scripts in the install and uninstall handlers of the TCustomDaemon object, like shown for systemd support in the above code sample. If you follow that route, installation and de-installation is essentially the same like under Windows.

You can install and uninstall the service from a terminal by starting the executable with sudo and the -install or -uninstall parameter:

sudo {service-name} -install sudo {service-name} -uninstall

After successful installation you can control the service using the

systemctl start ./{service-name}.service start the service
systemctl status ./{service-name}.service query the service state
systemctl stop ./{service-name}.service stop the service
systemctl enable ./{service-name}.service configure the service to be started when the machine boots
systemctl disable ./{service-name}.service configure the service to not be started when the machine boots

Note that systemctl expects the name of the configuration file (aka "unit"), which usually is the name of the service plus the ".service" ending. Also note that under Unix, like always, everything is case-sensitive, so you may cause confusion if you name a service mixed-case like shown in the above sample.

OnLinuxWithSystemctl.png

[End of work 2/2022]

[Some historical contents - needs to be verified and integrated or removed]

System codepage / UTF-8

A LazDeamon project is working with default, not UTF-8, codepage. The -dDisableUTF8RTL mode has to be activated with Project Options ... -> Compiler Options -> Additions and Overrides -> Use system encoding.

Linux (only for older Debian)

Download, configure, and "Save As" - the sample script located at Web Archive: [1] (The original link is dead for a long time).

  • SVC_ALIAS is the long description of your application
  • SVC_FILENAME is the actual file name of your compiled service application
  • SVC_DIR is the place your you copied the service application
  • SVC_SERVICE_SCRIPT is the final name of the service.sh when you "Save As" the customized debian-service.sh script.

Place your script in the /etc/init.d/ folder

start the service by running "sudo service Name_Of_Your_Script start"

Light bulb  Note: sudo has some variations, e.g.:


sudo -s #
sudo -H #
sudo -i #
sudo su #

sudo sh #

In order to auto-run the service at startup you can try update-rc.d or else will need a third party tool that will do this.

Option 1

sudo update-rc.d Name_Of_Your_Script defaults

Option 2

sudo apt-get install chkconfig
sudo chkconfig --add Name_Of_Your_Script
sudo chkconfig --level 2345 Name_Of_Your_Script on

systemd (Fedora, Debian, SLES12)

Presently, linux flavors are trending away from differing daemon launching and into a unified service model.

Fedora and SuSE Enterprise Linux Server 12 use systemd and with that commands to start/stop services are the same as on debian but there are differences on the configuration files.

  • From the command prompt sudo gedit
  • Copy and Paste
[Unit]
Description=Long description of your application
After=network.target

[Service]
Type=simple
ExecStart=complete_path_and_file_name -r
RemainAfterExit=yes
TimeoutSec=25

[Install]
WantedBy=multi-user.target
  • Edit the following values
    • Description - Long Description of your service application
    • ExecStart - complete-path_and_file_name is the name of your compiled service application with its complete path
  • Save As Dialog
    • Navigate to /lib/systemd/system/
    • Name the file the name_of_your_service.service

See also