Daemons and Services/zh CN

From Free Pascal wiki
Jump to navigationJump to search

Template:守护进程和服务

守护进程、服务和代理的概念

Unix 中的“守护进程”和 Windows 中的“服务”是系统级程序,运行时无需用户交互;macOS 中的“代理”是用户级程序(相对系统级的守护进程而言),运行时有无用户交互均有可能。尽管叫法不同,但他们的功能类似:比如 wwwftp 服务在 Linux 下称作守护进程,而在 Windows 下则叫服务。由于不直接与用户交互,因此在启动时会关闭stdin、stdout、stderr 描述符。

在 Free Pascal 和 Lazarus 中,可以利用 Lazarus 的 lazdaemon 包以平台无关的方式编写守护进程/服务。为避免与 Delphi 组件命名冲突,这些类命名为“daemon”。


Light bulb  Note: 本文重点介绍 Windows 和 Linux。如需了解 Lazarus 和 macOS 的“代理”,请参阅macOS 守护进程和代理

用 Lazarus 和可视化组件创建服务/守护进程

Light bulb  Note: 完整源代码在文末提供。

前提:安装 LazDaemon 包

在开始使用 Lazarus IDE 创建守护进程之前,必须通过“软件包”-“安装/卸载软件包”安装 LazDaemon 包,或者直接从.../lazarus/components/daemon/lazdaemon.lpk 安装 lpk 文件。

LazDaemon 包会在 IDE 中安装一些新的组件和菜单。

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

鉴于 Lazarus 软件包的工作方式,安装 LazDaemon 包需要“保存并重新构建IDE”。

创建并使用 Daemon 项目框架

LazDaemon 包安装完成后,从“项目-新建项目”菜单选择“守护进程(服务)应用”。

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

这会自动创建两个单元,一个派生自 TDaemon(DaemonUnit),另一个派生自 TDaemonMapper(DaemonMapperUnit)。以及一个主项目文件(TestDaemon.lpr),需要稍作修改才能使用:

Program TestDaemon;

Uses

// This is the scaffolded code, needs modification like shown below,
// since UseCThreads is usually not definied, and is not needed here
// {$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.
Warning-icon.png

Warning: 自动生成的 .lpr 文件中多了一条 {$IFDEF UseCThreads} ... {$ENDIF} 子句(上述示例中已被注释掉了),据观察,除非另作定义 UseCThreads,否则这个子句无法工作。可整句删除也可如上所示注释掉,或者在 Linux 下编译守护进程时添加 -dUseCThreads 选项。编写 Windows 服务不受影响。

守护进程虽然没有 GUI,但 DaemonApp 和 DaemonMapper 单元都支持 Lazarus 窗体编辑器。这样就可以用熟悉的图形界面来设置守护进程和映射类的各种属性。当然,也可以完全用代码初始化所有属性,如下所示。

自动生成的 DaemonApp 和 DaemonMapper 单元中都有多余的“var”定义(var DaemonMapper1: TDaemonMapper1; var Daemon1: TDaemon1;)。这两个变量在守护进程运行时并未实际用到,依然保持未初始化状态,对他们进行访问会导致致命错误 access violation。只要进一步了解了守护进程应用的内部工作原理,就会发现代码只与类相关(TDaemonMapper1、TDaemon),这两个变量完全可忽略或删除。

TDaemon 类的代码主要用于响应操作系统发送的各种控制消息。TDaemonMapper 包含了描述服务的一些数据结构。这两个类都需要在守护进程应用框架中注册,将在守护进程启动时供内部使用,完成对守护进程的配置。在以下示例中,注册是在单元的“Initialization”部分完成的。

只要在单元的 uses 部分加入 DaemonApp,即可引入守护进程类应用的主“Application”对象。守护进程应用的“Application”提供了完整的服务框架,包括安装和卸载、记录日志之类的高级功能。守护进程的“工作线程”需要自行创建,可参阅后续的示例代码,请勿使用 TDaemon 类的“Execute”方法。

发布 DaemonMapper 类

下面先填充一些基本属性,让程序跑起来。请注意,DaemonClassName 必须与 DaemonClass 单元中定义的类名完全一致。

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

Light bulb  Note: 跨平台程序员请注意:Unix 系统区分大小写,通常使用小写字母,因此如果如上定义了大小写混合的服务名称,可能会引发一些混淆。

不妨简单介绍一下 WinBindings 属性,即可通过 Windows 服务管理器配置各种服务属性,例如启动类型和用户帐户。虽然这个简单的守护应用中并未用到,但对于真正的守护进程应用程序而言,合理设置这些选项非常重要。WinBindings 在 Linux 中无效,请参阅关于 Linux 安装和卸载功能的章节,了解如何在 systemd “单元”(.service)文件中设置选项,以便为 Linux 实现相同的功能。

编写守护进程的方法

TDaemons 支持以下方法:

方法/事件 说明
OnStart 守护进程将启动时调用。本方法必须立即返回并置 OK:=True
OnStop 守护进程将停止时调用。本方法必须立即返回并置 OK:=True
OnShutDown 守护进程将被杀掉时调用,因为系统将要关闭。本方法必须立即停止守护进程,并置 OK:=True返回。本方法在 Linux 系统中不会触发。Linux 会直接杀掉守护进程。
OnPause 守护进程将暂停时调用。本方法必须立即返回并置 OK:=True。本方法在 Linux 系统中不会触发,系统内核会停止整个守护进程并触发 OnStop 事件。
OnContinue - OnExecute 不要用本方法实现工作线程,请参考示例代码实现工作线程的任务。
BeforeInstall 服务即将安装时调用。
AfterInstall 服务安装成功后调用。
BeforeUninstall 服务卸载前调用。
AfterUninstall 服务卸载完成后调用。

以下代码片段展示了成功实现简单守护进程所需的主要事件处理程序。已在 Windows 7/10/11 和几个 Linux 版本(Debian 10.8 和 Ubuntu 20.04.3)中测试通过。

有些便利性辅助函数已剥离至单独的单元中,以防扰乱守护进程的核心代码。

  • FileLoggerUnit:线程安全的记录日志文件的辅助工具。日志是必需记录的,因为 TDaemon 应用的控制信号接收器和服务代码需运行于单独的线程,否则服务代码运行时守护进程将无法响应控制信号。因此,在守护进程中至少涉及两个任务,这两个任务同时访问日志文件时可能会发生冲突。关于如何运用 TRTLCriticalSection 保证代码线程安全,实现对单一资源的串行访问,详情请参阅 Lazarus 维基的多线程应用开发教程。FileLoggerUnit 会将日志文件写入程序所在目录,请确保程序拥有写入权限。这里的代码会每天写一个文件,文件名包含创建日期。如果需要更高级的日志记录工具,可以考虑使用 LazLogger 单元。
  • DaemonWorkerThread:TThread 的派生类,可容纳守护进程的“工作”代码。关于 TThread.Execute 方法,其中不可避免包含了“无限”循环,详情请参阅 Lazarus 维基的多线程应用开发教程。由 TDaemon 启动的工作线程并没有什么特别的,就和其他线程一样。
  • [https://gitlab.com/freepascal.org/lazarus/lazarus/-/tree/main/examples/TDaemon DaemonSystemdInstallerUnit:该单元为 Linux 系统提供 -install 和 -uninstall 命令行参数支持。通过对 /lib/systemd/system 写入相应的控制文件,加入对 systemd/systemctl 的支持。

启动/停止守护进程的信号句柄

// ---------------------
// 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.

Warning-icon.png

Warning: To implement the daemon worker code, one might consider to implement a handler for the "execute" method found in TDaemon. This does not work well, especially not on WIndows. Execute does not run as a separate thread, while it executes the daemon stops handling control messages. The daemon will start and run fine, but if you try to stop/pause/resume it using the Windows Service Manager or the sc command, the daemon will not respond and appear to hang.

关闭守护进程的句柄

This Windows-only handler is called when the daemon is to be stopped because of an operating system shutdown. If we do not handle the event, Windows will simply forcefully kill the daemon process, potentially corrupting open files. For this simple demo service one can simply call the DataModuleStop event handler, executing the same code like a normal service stop. On Linux this handler is not supported, Linux will instead directly call the stop handler.

procedure TDaemon1.DataModuleShutDown(Sender: TCustomDaemon);
// Supported on Windows systems only
begin
  self.Stop;   // On shutdown, we trigger the stop handler. This will do nicely for this demo
  LogToFile('Daemon received shutdown signal');
end;

Linux 下的安装和卸载(systemd)

The built-in install and uninstall support (-install and -uninstall command line parameters) is implemented in LazDaemon applications for Windows only. Here is a simple code sample showing how to make custom handlers for install and uninstall events to add a similiar functionality 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.

If you don't specify a user account in the .service file, systemd will run the daemon as root. Running a daemon as "root" (Linux) or "LocalSystem" (Windows) is, for security reasons, considered bad practice, and can, amongst many other obstacles, hamper debugging the running daemon. Consider to add a line

    [...]
    f.WriteString('Service', 'Type', 'simple');
    f.WriteString('Service', 'User', '...');   // insert a service account here, otherwise the daemon will run as root
    f.WriteString('Service', 'ExecStart', Application.ExeName + ' -r');
    [...]

in the DaemonSystemdInstallerUnit.CreateSystemdControlFile code.

Light bulb  Note: At runtime you'll find all the TDaemonMapper and especially the TDaemonMapper.WinBindings settings in the "self.Definition" and "self.Definition.WinBindings" structures. If you prefer, you may consider using some of them to populate settings in the .service file.

For a comprehensive guide on all settings available in a systemd .service file (sometimes called a systemd "unit file") please see the documentation of your Linux distribution.

工作线程

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 in 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;

This "worker" doesn't do much, it loops until terminated and writes a message into the log every 5 seconds.

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

The inner loop of a TThread.Execute method 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 gracefully end a thread execution, like it can be seen in the above TDaemon.DataModuleStop sample. There is more info about TThreads available in the Multithreaded Application Tutorial wiki.

To keep the sample demo code simple, it does not handle runtime errors in any way. Errors in the logging unit will be dropped silently, all other runtime errors in the daemon will crash the daemon. Please refer to the wiki page about Exceptions to read more about catching and handling exceptions in general, and see the documentation on TThread.HandleException to read more about how to properly catch and forward an exception within the execute method of a worker thread.

Initializing TDaemon Properties in Code

If you prefer not to use the Lazarus IDE and its Forms Editor to populate your TDaemon and TDeamonMapper properties, you can do it all in code as well. If you want to go this route, note that your project must not be based on the LCL/Forms like other applications, but on DaemonApp. Failing this will make the code fail with numerous stream loading errors.

Working with TDaemon (The event driven approach to program a daemon) has already been described in the above sections, the only real difference is that you will assign the event handlers in the application code like so:

TDaemon1 = class(TDaemon)
    {...}
  procedure DataModuleStart(Sender: TCustomDaemon; var OK: Boolean);   // sample event handler event
    {...}
public
  constructor Create(AOwner: TComponent); override;  // overwrite the constructor to initialize the event handlers
end;

{...}

constructor TDaemon1.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  {...}
  onStart := @DataModuleStart;   // assign the sample event handler code
  {...}
end;

Follow the same strategy with TDaemonMapper to populate the Mapper.

Light bulb  Note: If you prefer an OOP based approach using inheritance to customize the daemon- and the mapper class you can do this as well, inherit your own daemon from TCustomDaemon and your own mapper from TCustomDaemonMapper, and overwrite the various virtual functions in the "Protected" section as needed

Here is an excerpt from the DaemonApp unit showing the available properties:

  TCustomDaemon = Class(TDataModule)
  private

    [...]

  Protected
    Function Start : Boolean; virtual;
    Function Stop : Boolean; virtual;
    Function Pause : Boolean; virtual;
    Function Continue : Boolean; virtual;
    Function Execute : Boolean; virtual;
    Function ShutDown : Boolean; virtual;
    Function Install : Boolean; virtual;
    Function UnInstall: boolean; virtual;

    [...]

用代码创建系统服务/守护进程

Light bulb  Note: The complete sample source code for this wiki page is available via the link at the end of this wiki page.

TestDaemonCodeOnly is a demo project utilizing the TCustomDaemon and TCustomDaemonMapper classes, and it works without the Lazarus IDE. It does therefore not need any Lazarus specific helper files like .lpr or .lfm, and can be maintained using any plain text editor. The programming paradigm used is not event-driven like in GUI centric code, but works based on OOP and inheritance. The helper units providing file logging, a daemon worker thread and support for systemd -install and -uninstall for Linux are the same as in the above GUI sample.

Support for -install and -uninstall for Linux is provided by the same DaemonSystemdInstallerUnit as used above. Windows service installation and uninstallation is already built-in in the DaemonApp unit. The TDaemonMapper class provides the WinBindings property to configure the service for Windows, on Linux this property is not used and has no effect. The code provided in the DeamonSystemdInstallerUnit provides similar functionality for Linux/systemd/systemctl.

The -run parameter is not supported on Windows, Windows has its own facilities (the Windows Service Control Manager or the "sc" command line tool) to control services.

The daemon code needs to be implemented in multiple threads, one for handling the daemon control messages and a separate thread for the daemon "worker" code. Just overwriting the TCustomDaemon.Execute method won't work well, since while the daemon loops in the "TDaemon.Execute" method it will not handle control messages. It will therefore appear to hang in the Service Control Manager and refuse to respond to sc commands. The "worker" thread code is identical with what's required in the above GUI sample, and implemented in the DaemonWorkerThread unit.

The absence of a GUI and console requires the use of a logging facility. The TCustomDaemon object provides a logging facility (through the EventLog property), but that is not thread-safe. The FileLoggerUnit provides a simple thread-safe log to file facility. For more sophisticated logging, consider to use the LazLogger unit.

Warning-icon.png

Warning: The logfile created by the FileLoggerUnit is written to the program directory, please ensure that the dameon has write access to it.

For more detailed informations about multithreading and how to make code thread-safe through the use of CriticalSections see the Multithreaded Application Tutorial.

program TestDaemonCodeOnly;

{$mode objfpc}{$H+}

uses
 {$IFDEF UNIX}
  cthreads,
   {$ENDIF}
  Classes,
  SysUtils,
  { you can add units after this }
  DaemonApp,
  FileLoggerUnit,             // Thread safe file logger
  DaemonWorkerThread,         // a TThread descendant to do the daemon's work
  DaemonSystemdInstallerUnit; // -install and -uninstall support for Linux/systemd


// ------------------------------------------------------------------
// TDaemonMapper: This class type defines the settings for the daemon
// ------------------------------------------------------------------

type
  TDaemonMapper1 = class(TCustomDaemonMapper)
  public
    constructor Create(AOwner: TComponent); override;
  end;

  constructor TDaemonMapper1.Create(AOwner: TComponent);

  begin
    inherited Create(AOwner);
    with TDaemonDef(self.DaemonDefs.Add) do
    begin
      DaemonClassName := 'TDaemon1';           // This must exactly match the daemon class
      Name := 'TestDaemonCodeOnly';            // Service name
      DisplayName := 'Test Daemon (CodeOnly)'; // Service display name
      Description := 'Lazarus Daemons and Services Wiki Demo Service (Created in Code only)';
      Options := [doAllowStop, doAllowPause];
      WinBindings.StartType := stManual;  // stBoot, stSystem, stAuto, stManual, stDisabled
      WinBindings.ServiceType := stWin32;
    end;
  end;

// -------------------------------------------------------------------
// TDaemon: This class type definies the daemon task and handles the
//          events triggered by the Windows/Linux Service Manager
// -------------------------------------------------------------------

type

  { TDaemon1 }

  TDaemon1 = class(TCustomDaemon)
  private
    FDaemonWorkerThread: TDaemonWorkerThread;
  public
    function Start: boolean; override;     // start the daemon worker thread
    function Stop: boolean; override;      // stop the daemon worker thread
    function Pause: boolean; override;     // pause the daemon worker thread (Windows only)
    function Continue: boolean; override;  // resume the daemon worker thread (Windows only)
    function ShutDown: boolean; override;  // stop the daemon worker thread because of OS shutdown
    function Install: boolean; override;   // added -install suppport for Linux
    function UnInstall: boolean; override; // added -uninstall suppport for Linux
  end;

  { TDaemon1 }

  // ------------------------------------------------
  // Daemon start and stop signal
  // ------------------------------------------------

  function TDaemon1.Start: boolean;
  begin
    // Create a suspended worker thread - see DaemonWorkerThread unit
    FDaemonWorkerThread := TDaemonWorkerThread.Create;
    // Parametrize it
    FDaemonWorkerThread.FreeOnTerminate := False;
    // Start the worker
    FDaemonWorkerThread.Start;
    LogToFile(Format('TDaemon1: service %s started, PID=%d', [self.Definition.Name, GetProcessID]));
    Result := True;
  end;

  function TDaemon1.Stop: boolean;
  begin
    // stop and terminate the worker
    if assigned(FDaemonWorkerThread) then
    begin
      FDaemonWorkerThread.Terminate;
      // Wait for the thread to terminate.
      FDaemonWorkerThread.WaitFor;
      FreeAndNil(FDaemonWorkerThread);
    end;
    Result := True;
    LogToFile(Format('TDaemon1: service %s stopped', [self.Definition.Name]));
  end;

// ------------------------------------------------
// Daemon pause and continue signal (Windows only)
// ------------------------------------------------

  function TDaemon1.Pause: boolean;
  begin
    FDaemonWorkerThread.Suspend;    // deprecated, yet still working
    LogToFile(Format('TDaemon1: service %s paused', [self.Definition.Name]));
    Result := True;
  end;

  function TDaemon1.Continue: boolean;

  begin
    LogToFile(Format('TDaemon1: service %s continuing', [self.Definition.Name]));
    FDaemonWorkerThread.Resume;    // deprecated, yet still working
    Result := True;
  end;

// --------------------------------------------------------------
// Daemon stop on operating system shutdown signal (Windows only)
// --------------------------------------------------------------

  function TDaemon1.ShutDown: boolean;
  begin
    Result := self.Stop;   // On shutdown, we trigger the stop handler. This will do nicely for this demo
    LogToFile(Format('TDaemon1: service %s shutdown', [self.Definition.Name]));
  end;

// -----------------------------------------------------------------------------------------
// Daemon install and uninstall helpers for Linux, Windows is already built in TCustomDaemon
// -----------------------------------------------------------------------------------------

  function TDaemon1.Install: boolean;

  var
    FilePath: string;

  begin
    Result := False;
    {$IFDEF WINDOWS}
    Result := inherited Install;
    {$ELSE}
      {$IFDEF UNIX}
      FilePath := GetSystemdControlFilePath(Self.Definition.Name);
      LogToFile(Format('TDaemon1: installing control file: %s',[FilePath]));
      Result := CreateSystemdControlFile(self, FilePath);
      if not Result then
        LogToFile('TDaemon1: Error creating systemd control file: ' + FilePath);
      {$ENDIF}
    {$ENDIF}
    LogToFile(Format('TDaemon1: service %s installed: %s', [self.Definition.Name, BoolToStr(Result, 'ok', 'failure')]));
  end;

  function TDaemon1.UnInstall: boolean;

  var
    FilePath: string;

  begin
    Result := False;
    {$IFDEF WINDOWS}
    Result := inherited UnInstall;
    {$ELSE}
      {$IFDEF UNIX}
      FilePath := GetSystemdControlFilePath(Self.Definition.Name);
      Result := RemoveSystemdControlFile(FilePath);
      if not Result then
        LogToFile('TDaemon1: Error removing systemd control file: ' + FilePath);
      {$ENDIF}
    {$ENDIF}
    LogToFile(Format('TDaemon1: service %s uninstalled: %s', [self.Definition.Name, BoolToStr(Result, 'ok', 'failure')]));
  end;

// ---------------------
// Daemon main init code
// ---------------------

begin
  RegisterDaemonClass(TDaemon1);
  RegisterDaemonMapper(TDaemonMapper1);
  Application.Run;
end.

守护进程/系统服务的安装

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:

Command Description
TestDaemon -install install the daemon
sc query TestDaemon check the service status
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)
TestDaemon -uninstall remove the daemon

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:

Command Description
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

Light bulb  Note: Most Windows service properties, like autostart on boot or usage of a specific service account, can be set via the TDaemonMapper1.WinBindings property in the DaemonMapperUnit1 file.

Linux

Unlike for Windows, the TDaemonApp application does not have any support for an automated installation, since under Linux/Unix there is a wide variety of service control subsystems. 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 on Windows.

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

Command Description
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 command

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

OnLinuxWithSystemctl.png

The code in DaemonSystemdInstallerUnit.CreateSystemdControlFile triggered by the -install command line parameter reads the most basic properties of the TDaemonMapper class and writes a simple systemd .service file into the /lib/systemd/system directory to put your daemon under control of systemd. If you prefer to do it manually, you may use any text editor to create your own file following this template:

[Unit]
Description=Long description of your application
After=network.target

[Service]
Type=simple
User=name_of_a_service_user_account
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
    • User - specify an service user account here, if omitted the daemon will run as root, running a daemon as root is strongly discouraged
  • Save the file
    • Navigate to /lib/systemd/system/
    • Name the file the name_of_your_service.service

Many more entries in the .service file created by DaemonSystemdInstallerUnit.CreateSystemdControlFile are available to customize how systemd controls your daemon, please see the the systemd "unit" file reference of your Linux distribution.

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

Warning-icon.png

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

守护进程/系统服务的调试

Debugging a daemon is not as straightforward as debugging a normal application, since a running daemon does usually have neither a GUI nor a console, 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" in the Lazarus GUI 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 command or Windows 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, and the service will be looping somewhere within the worker code. After setting your breakpoints and watches in the worker code, resume your debugging by running the code using [F9].

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

Warning-icon.png

Warning: The "Pause" button in the Lazarus symbol bar does not seem to work when debugging a running process remotely, it will hang the debugger if pressed. Use "Run" [F9] to resume debugging after a breakpoint.

- If you need to debug the create or start handlers, or any other piece in code which might have already 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(100);   // 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.

Light bulb  Note: The -run parameter supported by Linux is not available on Windows. You must use the Windows Service Control Manager to start and stop the service. To attach the debugger to the then 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" properly, most likely you will need "Gdb (Gnu Debugger)".

Linux

Linux is propably somewhat 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. Keep in mind though, that many problems originate from security restrictions enforced by the operating system. Many of those restrictions are not applied when the daemon is run in user context.

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

If you do, for instance because you suspect a problem with service specific restrictions, need to attach the debugger to the already running daemon, you can do so using "Run" - "Attach to Program", if you have configured the daemon to run under a specific user account instead of root. See the chapter above about Linux install and uninstall support on how to add an appropriate option to the .service file.

Unfortunately there isn't such a handy "IsDebuggerPresent" call available like under Windows, but the following trick works nicely.

procedure WaitForDebugger;

var
  SetMeValue:boolean=false;

begin
  // Linux trick: attach the debugger, and change SetMeValue to true
  while not SetMeValue do
     sleep(100); // put a breakpoint here
end;

Put a breakpoint into the line with the "sleep" command. Once the daemon is started using systemctl, execution it will be halted at your breakpoint in the while loop. Now you can attach the debugger to the PID you got from systemctl status, put a watch ([Ctrl-F5]) on the "SetMeValue" variable and then use the Evaluate/Modify context menu to change the value of the "SetMeValue" variable from "false" to "true", and resume program execution ([F9]). The execution will immedately leave the while loop and run to the next breakpoint.

已知问题

与权限相关的问题

While you can do most of the development of the core daemon code under a user account and from within an IDE (Lazarus), the live daemon will most likely run "in the background" under a different context. Nowadays operating systems do, for security reasons, run daemons in more or less restricted environments. Because a daemon does have neither a terminal nor a GUI to show any messages, debugging such issues can be hard. Unfortunately there is no common recipe which resolves all service related problems in all operating systems supported by FPC/Lazarus, as the strategy behind restricting service code is vastly different between Windows, Linux and macOS, and it is even changing between different versions of the same operating system, with the more recent versions trending to be more restrictive than their predecessors.

If you make the transition from running the daemon code under your developer account to "go live" and test the daemon running under a service user account oder under "Localsystem" or "root" (which, btw, is considered bad security practice since many years), take the time to study the security related documentations of your operating system and find out which restrictions and other specialities apply if code runs in the background.

Light bulb  Note: See the section above about debugging daemons running in the background by attaching the debugger to the running code. Make your daemon, once the daemon is started by the operating system, wait until the debugger is attached. From there you can then observe the execution and detect restrictions related exceptions and other security/background operation related nastinesses.

EStreamError 异常:"no streaming method available"

Exception at 0041A30E: EStreamError:
Failed to initialize component class "TDaemon1": No streaming method available.

You have included visual components (which require initalization by a built-in resource stream created by the .lfm file in GUI applications) in a code-only daemon project. If you decide to create the daemon in code, you may neither include the LazDaemon package, nor add the "LazDaemonApp" to the uses section. If you do, your project will compile fine, but if you start the exe you get the above error.

“Stream Read”和“Unkown Properties”错误 ...

... when loading a daemon project through the Lazarus IDE.

Most likely you try to open a TDaemon (GUI component) based project, but you haven't yet installed the LazDaemon package.

Abort all loading errors, and install the LazDaemon package using the Lazarus Package Manager like described above. After Lazarus has re-compiled, the daemon project should re-load and compile without problems.

Image 9.png

Image 10.png

Lazarus IDE suggests to clear invalid properties from the .lpm file

This is another issue which was observed when you try to open a TDaemon (GUI) based project, but you haven't yet installed the LazDaemon package.

Image 11.png

Abort all loading errors, and install the LazDaemon package using the Lazarus Package Manager like described above. After Lazarus has re-compiled itself, the daemon project should re-load and compile without problems.

因为不支持线程导致在 Unix 中无法启动守护进程

The code compiles fine, but if you try to run the daemon using a console and the -run parameter, you get a message saying something like "no thread support compiled in". If you try to start the dameon through systemctl, you won't see any message, it simply won't seem to run. You can use "systemctl status" to see the last error caused by the daemon, this will reveal the same crash infos like starting it using -run.

This binary has no thread support compiled in. Recompile the application with a thread-driver in the program uses clause before other units using thread. Runtime error 232 at ...
...
No heap dump by heaptrc unit Exitcode = 232

Most likely you compiled your project without support for the CThreads library (not required on Windows). See the above project lpr file sample code and the notes about "UseCThreads". The short of it is to go to the .lpr file and remove the {$IFDEF UseCThreads} ... {$ENDIF} around the "uses CThreads" statement, so the CThreads library gets linked into your code on Linux. On Windows this library is not required.

Debugging Issues observed on Windows

The "Pause" button in the Lazarus symbol bar activates, if a breakpoint is hit, but it does not seem to work when debugging a running process remotely, debugging will hang if the button is pressed. Use "Run" [F9] instead to resume debugging after a breakpoint.

To attach the debugger to the running daemon process you need to run the Lazarus IDE with Administrative Prvilege (elevated), otherwise you will get "Error 5 (access denied)" when trying to attach the debugger.

If all debugging selections in the "Run" menu are unavailable (grayed), navigate to "Project" - "Project Options" - "Debugger" and set the "Debugger Backend" to "Gdb [GNU debugger (gdb)]". It has been observed that this setting may get messed up if you copy the project source files from one computer / Lazarus installation over to a different one.

Debugging Issues observed on Linux

Using "Attach to program" to debug a running daemon via process ID (PID) does not seem to work, you get an error "Debugger Error" ... "Attach failed". One reason may be that you haven't specified a user in the service control file, and thus the service runs as "root", which hampers debugging. Besides of running the service using a dedicated service user account, as a workaround, consider to start debugging by loading the daemon from within Lazarus using "Run" - "Run Parameters" and entering the -run command line parameter. This way debugging does work, but note that the code runs in a different context (user instead of system- or service user account), which may change it's behaviour because of permission related issues.

Daemons have no Access to Network- and Cloud Drives (Onedrive, Dropbox)

When choosing the directory for storing the daemon executable (which is by default also used for logging), keep in mind that once the daemon runs in the background it does usually run under the context of a system- or service account. User-specific virtual drives created in one user's account are usually available neither to other user accounts nor to the system account. If you run the daemon from a networked/cloud drive, it is likely that the daemon won't start, since the OS will not be able to find the executable, and that write attempts to the log will fail, since by default the log file is written where the executable is. This may be confusing, since -install and -uninstall will likely work and log fine, because they are usually executed by hand and thus run in your user's context.

Specifying a Service User Account does not work on Linux

If you start the daemon and check which account it runs under, it appears to run as root, despite you have specified a different account in your systemd .service file.

There are (unconfirmed) reports around that (on some versions of Linux?) in the .service file controlling the systemd the "User" entry must appear before the "ExecStart" entry to work properly, like shown in the code samples in the sections about Linux install and uninstall support.

System Codepage / UTF-8

[Old content, needs verfification 3/2022, may be obsolete] 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.

[一些旧的内容:有待验证、合并或删除]

[Old content, needs verfification 3/2022, may be obsolete]

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

See also