Daemons and Services

From Lazarus wiki
Revision as of 14:30, 22 February 2022 by ArminLinder (talk | contribs) (Bugfix: used wrong Name property name for .service file, added worker thread code sample)
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'.

Note: this wiki page covers Services for Windows and Daemons for Unix (with a string focus on Linux). For macOS support, please refer to macOS daemons and agents.

Prerequisites: Install the LazDaemon package

Before you can start, you must install the LazDaemon package either via "Components" - "Configure installed packages" or by installing the lpk file directly from .../lazarus/components/daemon/lazdaemon.lpk. The package installs some new components and menu items in the IDE.

2022-02-21 14 34 28-LINDER-LAZW7 - VMware Workstation.png

Due to the way packages work in Lazarus, you need to "Save and rebuild the IDE" to install the LazDaemon package.

Creating your first Service/Daemon

After having installed the LazDaemon package, from the "Project" - "New Project" Menu, pick "Daemon (service) application)".

2022-02-17 16 31 12-Create a new project.png

This will automatically create two units, one for a TDaemon descendant ("DaemonUnit"), and one for a TDaemonMapper descendant ("DaemonMapperUnit"), and a main project file ("TestDaemon.lpr"). This is what the scaffolded files look like:

unit DaemonUnit;

{$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 DaemonMapperUnit;

{$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.
Program TestDaemon;

Uses

// {$IFDEF UNIX}{$IFDEF UseCThreads}
//  CThreads,
// {$ENDIF}{$ENDIF}

{$IFDEF UNIX}
  CThreads,
{$ENDIF}
  DaemonApp, lazdaemonapp, daemonmapperunit, DaemonUnit
  { add your units here };

{$R *.res}

begin
  Application.Initialize;
  Application.Run;
end.
  • The .lpr file created contains an additional {$IFDEF UseCThreads} ... {$ENDIF} clause (commented out in the above sample), which keeps daemons from running properly on Linux, unless additional measures are taken to define UseCThreads. I suggest you can safely remove the conditional entirely, like shown in the above .lpr sample.
  • Both the DaemonApp and the DaemonMapper unit have support for the Lazarus Form Editor, though Daemons don't have a GUI. This way you get a familiar GUI to populate the various properties of the daemon and the mapper class. Of course, if you prefer, you can initialize all properties in code as well.
  • Both the DaemonApp and the DaemonMapper unit contain a var definition (var DaemonMapper1: TDaemonMapper1; var Daemon1: TDaemon1;), those are actually never used by the running daemon, remain uninitialized and can be removed. If you have a closer look at how daemon applications work you'll see that the code relies solely on the types (TDaemonMapper1, TDaemon).

The code provide by the TDaemon class does mainly respond to the various service control messages sent by the OS. The TDaemonMapper contains data structures describing the service. Both classes need to be registered, in this code sample this is done in the initialization section of each unit. The main "Application" object for a service-style application is introduced by putting a reference to DaemonApp into the uses secion of each unit. In a daemon application, "Application" provides the complete service framework including advanced features like install and uninstall support, and logging. For the "worker" part of the daemon 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.

Populating the DaemonMapper Class

For the moment let's just fill some basic properties to get things going. Note that the DaemonClassName must exactly match with what you have defined in the DaemonClass unit.

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

Note for cross-platform programmers: In Unix everything is case-sensitive, using lowercase is common use, so you may cause some confusion if you define a mixed-case service name like shown in the above sample.

Also note the WinBindings property, which lets you configure various service properties for use by the Windows Service Manager, like service start type and service user account. It doesn't have any effect on Linux.

Writing the Daemon Methods

TDaemons support the following methods:

OnStart Called when daemon should start. This method must return immediately with OK:=True.
OnStop Called when daemon should stop. This method must return immediately with OK:=True.
OnShutDown Called when daemon should be killed. This method must stop the daemon immediately and return with OK:=True. This is not triggered under Linux. Linux simply kills the daemon.
OnPause Called when daemon should pause. This method must return immediately with OK:=True. Under Linux this is not triggered, instead the kernel stops the whole daemon on STOP.
OnContinue Called when daemon should continue after a pause. This method must return immediately with OK:=True. Under Linux this is not triggered.
OnExecute Do not use this property to implement the working code, use a worker task like shown in the code sample.
BeforeInstall Called before service installation.
AfterInstall Called after (successful) service installation.
BeforeUninstall Called before service de-installation.
AfterUninstall Called after successful service de-installation.

The following code snippets show all the major event handlers necessary to successfully implement a simple daemon. They have been tested on Windows 7/10/11 and several Linux distros (Debian 10.8 and Ubuntu 20.04.3).

The complete project including all the source- and resource files can be downloaded from [Tbd: provide a Git repo, tbd till end of 2/2022]

Some handy helper functions have been offloaded into separate units, so they do not obfuscate the daemon core code:

  • FileLoggerUnit: a thread safe log to file helper. It is required because the contol signal receiver included in the TDaemon application and the service's working code need to multitask, otherwise the daemon will not respond to control signals while the service worker code runs. So in any daemon there are at least two tasks involved, which could create a clash when accessing the log file simultaneously. See the Lazarus wiki Multithreaded Application Tutorial for details about how to serialize accesses to a single resource by using a TRTLCriticalSection to make the code thread-safe. Note that the FileLoggerUnit code does always write the log into the program directory, please make sure the daemon has write access permissions. The code writes one file per day, the filename contains the creation date. The code is also very I/O intensive, if you need a better logger, consider using the LazLogger unit.
  • DaemonWorkerThread: a TThread descendant class to hold the daemon "worker" code. See the Lazarus wiki Multithreaded Application Tutorial for details about the TThread.Execute method containing the inevitable "while not terminated" loop. There is nothing special with a worker thread initiated by a TDaemon, it's just a simple thread like any other.
  • DaemonSystemdInstallerUnit: a unit which tries to provide support for the -install and -uninstall command line parameters supported on Windows for the Linux OS. Adds systemd/systemctl support by writing an appropriate control file to /lib/systemd/system.

=== Installer and Uninstaller for Linux

// --------------------------------
// Installation and De-Installation
// --------------------------------

procedure TDaemon1.DataModuleAfterInstall(Sender: TCustomDaemon);

  var
  isInstalled: boolean = True;
  FilePath: string;

begin
  LogToFile('Daemon installing');
  {$IFDEF UNIX}
  FilePath := GetSystemdControlFilePath(Self.Definition.Name);
  isInstalled := CreateSystemdControlFile(self, FilePath);
  if not isInstalled then
    LogToFile('Error creating systemd control file: ' + FilePath);
  {$ENDIF}
  if isInstalled then
    LogToFile('Daemon installed');
end;

procedure TDaemon1.DataModuleBeforeUnInstall(Sender: TCustomDaemon);
  var
    isUnInstalled: boolean = True;
    FilePath: string;

  begin
    LogToFile('Daemon uninstalling');
    {$IFDEF UNIX}
    FilePath := GetSystemdControlFilePath(Self.Definition.Name);
    isUnInstalled := RemoveSystemdControlFile(FilePath);
    if not isUninstalled then
      LogToFile('Error removing systemd control file: ' + FilePath);
    {$ENDIF}
    if isUninstalled then
      LogToFile('Daemon uninstalled');
  end;

These Unix/Linux-only handlers utilize the routines in DaemonSystemdInstallerUnit to write a systemd control file (.service file) for our service to /lib/systemd/system, so the daemon can be controlled using the systemctl command. On uninstall, the .service file ist deleted.

For Windows these handlers are not required, because Windows service installation and uninstallation is already built into the LazDaemon code.

Note: it was intentionally not tried to adopt the WinBindings data structure found in DaemonMapper1 for use with Linux, instead the basic Linux daemon settings were hard-coded into the DaemonSystemdInstallerUnit to keep Windows and Linux apart where the operating system concept for controlling a service is different. Feel free to read the TDaemonMapper properties used to create the TDeamon application, you'll find all the settings in the "self.Definition" structure, and you can easily adopt some of them to parametrize the .service file.

Daemon Start/Stop Signal handler

// ---------------------
// Start and Stop signal
// ---------------------  

procedure TDaemon1.DataModuleStart(Sender: TCustomDaemon; var OK: Boolean);
begin
  LogToFile(Format('Daemon received start signal, PID:%d', [GetProcessID]));
  // Create a suspended worker thread - see DaemonWorkerThread unit
  FDaemonWorkerThread := TDaemonWorkerThread.Create;
  // Parametrize it
  FDaemonWorkerThread.FreeOnTerminate := False;
  // Start the worker
  FDaemonWorkerThread.Start;
  OK := True;
end;

procedure TDaemon1.DataModuleStop(Sender: TCustomDaemon; var OK: Boolean);
begin
  LogToFile('Daemon received stop signal');
  // stop and terminate the worker
  if assigned(FDaemonWorkerThread) then
  begin
    FDaemonWorkerThread.Terminate;
    // Wait for the thread to terminate.
    FDaemonWorkerThread.WaitFor;
    FreeAndNil(FDaemonWorkerThread);
  end;
  LogToFile('Daemon stopped');
  OK := True;
end;

These handlers deal with the start and stop signals issued by the operating system. If the daemon starts, DataModuleStart fires and we spawn the "worker" thread found in the DaemonWorkerThread unit, see the TDeamonWorkerThread.Execute method to find the actual working code of our daemon. To understand the thread code, refer to the Multithreaded Application Tutorial wiki page and to the documentation of TThread.

Note: to implement the daemon worker code, one might consider to implement a handler for the "execute" method found in TDaemon. This does not seem to work on Windows, TDaemon.Execute does not multitask and thus, while it executes, the daemon stops handling control messages. If you try to stop it using the Service Manager, the daemon will not respond and appear to hang.

Daemon Worker Thread

At the core of every daemon there is a piece of code which does the actual work, I named it the "worker" thread. It needs to be a separate thread, because in the background the daemon is supposed to continue to listen for control messages and process them. Creation and destruction of the thread can be seen on the above samples for TDaemon.DataModuleStart and TDaemon.DataModuleStop handlers.

procedure TDaemonWorkerThread.Execute;

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;

Ths worker doesn't much, it loops until terminated and writes a message into the log every 5 seconds. Note that the inner loop must be layed out in a way so it checks the "Terminated" flag frequently. The flag is set by the TThread.Terminate method which is called to kill a thread, like it can be seern in the above TDaemon.DataModuleStop sample. There is more info about TThreads available in the Multithreaded Application Tutorial wiki.

For your own daemon functionality, replace the code between "..." and "..." with your own.

Daemon/Service Installation

Windows

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

Open a terminal with elevated privileges (run as administrator), navigate to the directory where you compiled your test application into, and try the following commands:

TestDaemon -install install the daemon
sc query TestDaemon check the service status
TestDaemon -uninstall remove the daemon
sc config TestDaemon start=auto configure the service to be started
when the machine boots
sc config TestDaemon start=manual configure the service to be started manually
sc config TestDaemon start=disabled disable the service (cannot be started manually)

Screenshot taken 2/2022 on a Windows 11 machine

After successful installation you can control the service either via the sc command line, or by GUI using the "Services" management console. Recent Windows Versions do also have a basic service control facility on the "Services" tab of the Task Manager.

The most important SC commands to control a service after installation are:

sc start TestDaemon start the service
sc query TestDaemon query the service state
sc stop TestDaemon stop the service

You may also use the Windows service control manager console.

ServiceControlManager.png

Note that, unlike Linux, most Windows service properties, like autostart on boot, can be set from inside the service code too, see the TDaemonMapper1.WinBindings property in the DaemonMapperUnit1 file.

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 ./TestDaemon -install install the service
sudo ./TestDaemon -uninstall uninstall the service
systemctl enable TestDaemon configure the service to be started
when the machine boots
systemctl disable TestDaemon configure the service to not be started
when the machine boots

After successful installation you can control the service using the

systemctl start TestDaemon start the service
systemctl status TestDaemon query the service state
systemctl stop TestDaemon stop the service

OnLinuxWithSystemctl.png

Reading the Log File

This is a sample log file showing the daemon's internals while running, it was created on Windows, but will look exactly the same on Linux.

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

Note that the FileLoggerUnit does always log into the program directory, please make sure the daemon has write access permissions to it.

Debugging your Daemon

Debugging a daemon is not as straightforward as debugging a normal application, since a daemon does not have a GUI, and its run/stop state is usually managed by the operating system. Windows and Linux require different approaches to debugging, especially to the way you initiate a debugging session. Once you have attached the debugger to the daemon and hit your first breakpoint, things work just with any other application, and you may control the debugger directly from your Lazarus GUI and the daemon source code.

As a prerequisite, you need to compile your service/daemon using a "debug" configuration, so debug code gets inserted and a matching debug symbol file (.dbg) file is created.

Windows

The debugging strategy on Windows depends on what portion of the code you need to examine:

- If you need to debug -install or -uninstall, set "Run" - "Run Parameters" to -install or -uninstall, set your breakpoints, and start the code [F9] - If you need to debug the service worker code, start the service via the operating system (SC or Service Manager), determine the Process ID (PID), and then choose "Run" - "Attach to Program". Once attached, the debugger will automatically halt at a predefined temporary breakpoint in ntdll.dll!DbgUserBreakPoint. After setting your breakpoints and watches you start your debugging by running the code using [F9].

2022-02-21 21 25 22-Lazarus IDE v2.2.0 - Daemon application (debugging ...).png

- If you need to debug the create or start handlers, or any other piece in code which might have been executed before you had a chance to "Attach to program" you can make your code wait until a debugger is attached by inserting the following line in your code:

// This code will loop until a debugger is attached
While not IsDebuggerPresent do sleep(10);   // will loop until a debugger is attached
// ... put a breakpoint somewhere after the loop to catch the code as soon as it leaves the loop

2022-02-21 21 36 38-Lazarus IDE v2.2.0 - Daemon application (debugging ...).png

After attaching the debugger and pressing [F9] the daemon will stop right at the breakpoint.

Notes: the -run parameter supported by Linux is not available on Windows. For beening allowed to attach to to a running process you need to run the Lazarus IDE with Administrative Prvilege (elevated), otherwise you will get Error 5 (access denied) when trying to attach to the service. If you are running Lazarus elevated, and nevertheless have difficulties attaching to your service ("Access denied" or "Run" - "Attach to program" is grayed out) make sure you have set the debugger in "Project" - "Project Settings" - "Debugger" to "Gdb (Gnu Debugger)".

Linux

Linux is easier to debug, since you can execute your daemon like every other program from within the Lazarus IDE, if you enter -run into "Command line parameters" in the "Run" - "Run Parameters" menu.

2022-02-22 12 18 20-Debian 10.8 Buster - VMware Workstation.png

Now you can start the daemon using [F9] like any normal application and debug it.

2022-02-22 12 19 41-Debian 10.8 Buster - VMware Workstation.png

Known problems

(tbd 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