xtool/contrib/mORMot/SQLite3/mORMotService.pas

2798 lines
96 KiB
ObjectPascal

/// daemon managment classes for mORMot, including low-level Win NT Service
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit mORMotService;
{
This file is part of Synopse mORMot framework.
Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez
Synopse Informatique - https://synopse.info
*** BEGIN LICENSE BLOCK *****
Version: MPL 1.1/GPL 2.0/LGPL 2.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the License.
The Original Code is Synopse mORMot framework.
The Initial Developer of the Original Code is Arnaud Bouchez.
Portions created by the Initial Developer are Copyright (C) 2022
the Initial Developer. All Rights Reserved.
Contributor(s):
- Eric Grange
- Leander007
- Maciej Izak (hnb)
Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
***** END LICENSE BLOCK *****
}
interface
{$I Synopse.inc}
uses
{$ifdef MSWINDOWS}
Windows,
Messages,
{$else}
{$ifdef FPC}
SynFPCLinux,
BaseUnix,
Unix,
{$else}
Types,
LibC, // Kylix
{$endif FPC}
{$endif MSWINDOWS}
Classes,
SysUtils,
{$ifndef LVCL}
Contnrs,
{$endif}
SynCommons,
SynTable,
SynLog,
SynCrypto, // for executable MD5/SHA256 hashes
mORMot; // for TSynJsonFileSettings (i.e. JSON serialization)
{$ifdef MSWINDOWS}
{ *** some minimal Windows API definitions, replacing WinSvc.pas missing for FPC }
const
CM_SERVICE_CONTROL_CODE = WM_USER+1000;
SERVICE_QUERY_CONFIG = $0001;
SERVICE_CHANGE_CONFIG = $0002;
SERVICE_QUERY_STATUS = $0004;
SERVICE_ENUMERATE_DEPENDENTS = $0008;
SERVICE_START = $0010;
SERVICE_STOP = $0020;
SERVICE_PAUSE_CONTINUE = $0040;
SERVICE_INTERROGATE = $0080;
SERVICE_USER_DEFINED_CONTROL = $0100;
SERVICE_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED or
SERVICE_QUERY_CONFIG or
SERVICE_CHANGE_CONFIG or
SERVICE_QUERY_STATUS or
SERVICE_ENUMERATE_DEPENDENTS or
SERVICE_START or
SERVICE_STOP or
SERVICE_PAUSE_CONTINUE or
SERVICE_INTERROGATE or
SERVICE_USER_DEFINED_CONTROL;
SC_MANAGER_CONNECT = $0001;
SC_MANAGER_CREATE_SERVICE = $0002;
SC_MANAGER_ENUMERATE_SERVICE = $0004;
SC_MANAGER_LOCK = $0008;
SC_MANAGER_QUERY_LOCK_STATUS = $0010;
SC_MANAGER_MODIFY_BOOT_CONFIG = $0020;
SC_MANAGER_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED or
SC_MANAGER_CONNECT or
SC_MANAGER_CREATE_SERVICE or
SC_MANAGER_ENUMERATE_SERVICE or
SC_MANAGER_LOCK or
SC_MANAGER_QUERY_LOCK_STATUS or
SC_MANAGER_MODIFY_BOOT_CONFIG;
SERVICE_CONFIG_DESCRIPTION = $0001;
SERVICE_WIN32_OWN_PROCESS = $00000010;
SERVICE_WIN32_SHARE_PROCESS = $00000020;
SERVICE_INTERACTIVE_PROCESS = $00000100;
SERVICE_BOOT_START = $00000000;
SERVICE_SYSTEM_START = $00000001;
SERVICE_AUTO_START = $00000002;
SERVICE_DEMAND_START = $00000003;
SERVICE_DISABLED = $00000004;
SERVICE_ERROR_IGNORE = $00000000;
SERVICE_ERROR_NORMAL = $00000001;
SERVICE_ERROR_SEVERE = $00000002;
SERVICE_ERROR_CRITICAL = $00000003;
SERVICE_CONTROL_STOP = $00000001;
SERVICE_CONTROL_PAUSE = $00000002;
SERVICE_CONTROL_CONTINUE = $00000003;
SERVICE_CONTROL_INTERROGATE = $00000004;
SERVICE_CONTROL_SHUTDOWN = $00000005;
SERVICE_STOPPED = $00000001;
SERVICE_START_PENDING = $00000002;
SERVICE_STOP_PENDING = $00000003;
SERVICE_RUNNING = $00000004;
SERVICE_CONTINUE_PENDING = $00000005;
SERVICE_PAUSE_PENDING = $00000006;
SERVICE_PAUSED = $00000007;
type
PServiceStatus = ^TServiceStatus;
TServiceStatus = object
public
dwServiceType: DWORD;
dwCurrentState: DWORD;
dwControlsAccepted: DWORD;
dwWin32ExitCode: DWORD;
dwServiceSpecificExitCode: DWORD;
dwCheckPoint: DWORD;
dwWaitHint: DWORD;
end;
PServiceStatusProcess = ^TServiceStatusProcess;
TServiceStatusProcess = object(TServiceStatus)
public
dwProcessId: DWORD;
dwServiceFlags: DWORD;
end;
SC_HANDLE = THandle;
SERVICE_STATUS_HANDLE = DWORD;
TServiceTableEntry = record
lpServiceName: PChar;
lpServiceProc: procedure(ArgCount: DWORD; Args: PPChar); stdcall;
end;
PServiceTableEntry = ^TServiceTableEntry;
{$Z4}
SC_STATUS_TYPE = (SC_STATUS_PROCESS_INFO);
{$Z1}
function OpenSCManager(lpMachineName, lpDatabaseName: PChar;
dwDesiredAccess: DWORD): SC_HANDLE; stdcall; external advapi32
name 'OpenSCManager'+{$ifdef UNICODE}'W'{$else}'A'{$endif};
function ChangeServiceConfig2(hService: SC_HANDLE; dwsInfoLevel: DWORD;
lpInfo: Pointer): BOOL; stdcall; external advapi32 name 'ChangeServiceConfig2W';
function StartService(hService: SC_HANDLE; dwNumServiceArgs: DWORD;
lpServiceArgVectors: Pointer): BOOL; stdcall; external advapi32
name 'StartService'+{$ifdef UNICODE}'W'{$else}'A'{$endif};
function CreateService(hSCManager: SC_HANDLE; lpServiceName, lpDisplayName: PChar;
dwDesiredAccess, dwServiceType, dwStartType, dwErrorControl: DWORD;
lpBinaryPathName, lpLoadOrderGroup: PChar; lpdwTagId: LPDWORD; lpDependencies,
lpServiceStartName, lpPassword: PChar): SC_HANDLE; stdcall; external advapi32
name 'CreateService'+{$ifdef UNICODE}'W'{$else}'A'{$endif};
function OpenService(hSCManager: SC_HANDLE; lpServiceName: PChar;
dwDesiredAccess: DWORD): SC_HANDLE; stdcall; external advapi32
name 'OpenService'+{$ifdef UNICODE}'W'{$else}'A'{$endif};
function DeleteService(hService: SC_HANDLE): BOOL; stdcall; external advapi32;
function CloseServiceHandle(hSCObject: SC_HANDLE): BOOL; stdcall; external advapi32;
function QueryServiceStatus(hService: SC_HANDLE;
var lpServiceStatus: TServiceStatus): BOOL; stdcall; external advapi32;
function QueryServiceStatusEx(hService: SC_HANDLE;
InfoLevel: SC_STATUS_TYPE; lpBuffer: Pointer; cbBufSize: DWORD;
var pcbBytesNeeded: DWORD): BOOL; stdcall; external advapi32;
function ControlService(hService: SC_HANDLE; dwControl: DWORD;
var lpServiceStatus: TServiceStatus): BOOL; stdcall; external advapi32;
function SetServiceStatus(hServiceStatus: SERVICE_STATUS_HANDLE;
var lpServiceStatus: TServiceStatus): BOOL; stdcall; external advapi32;
function RegisterServiceCtrlHandler(lpServiceName: PChar;
lpHandlerProc: TFarProc): SERVICE_STATUS_HANDLE; stdcall; external advapi32
name 'RegisterServiceCtrlHandler'+{$ifdef UNICODE}'W'{$else}'A'{$endif};
function StartServiceCtrlDispatcher(
lpServiceStartTable: PServiceTableEntry): BOOL; stdcall; external advapi32
name 'StartServiceCtrlDispatcher'+{$ifdef UNICODE}'W'{$else}'A'{$endif};
{ *** high level classes to define and manage Windows Services }
var
/// you can set this global variable to TSynLog or TSQLLog to enable logging
// - default is nil, i.e. disabling logging, since it may interfere with the
// logging process of the service itself
ServiceLog: TSynLogClass;
type
/// all possible states of the service
TServiceState =
(ssNotInstalled, ssStopped, ssStarting, ssStopping, ssRunning,
ssResuming, ssPausing, ssPaused, ssErrorRetrievingState);
/// TServiceControler class is intended to create a new service instance or
// to maintain (that is start, stop, pause, resume...) an existing service
// - to provide the service itself, use the TService class
TServiceController = class
protected
FSCHandle: THandle;
FHandle: THandle;
FStatus: TServiceStatus;
FName: RawUTF8;
private
function GetStatus: TServiceStatus;
function GetState: TServiceState;
public
/// Creates a new service and allows to control it and/or its configuration
// - TargetComputer - set it to empty string if local computer is the target.
// - DatabaseName - set it to empty string if the default database is supposed
// ('ServicesActive').
// - Name - name of a service.
// - DisplayName - display name of a service.
// - Path - a path to binary (executable) of the service created.
// - OrderGroup - an order group name (unnecessary)
// - Dependencies - string containing a list with names of services, which must
// start before (every name should be separated with #0, entire
// list should be separated with #0#0. Or, an empty string can be
// passed if there is no dependancy).
// - Username - login name. For service type SERVICE_WIN32_OWN_PROCESS, the
// account name in the form of "DomainName\Username"; If the account
// belongs to the built-in domain, ".\Username" can be specified;
// Services of type SERVICE_WIN32_SHARE_PROCESS are not allowed to
// specify an account other than LocalSystem. If '' is specified, the
// service will be logged on as the 'LocalSystem' account, in which
// case, the Password parameter must be empty too.
// - Password - a password for login name. If the service type is
// SERVICE_KERNEL_DRIVER or SERVICE_FILE_SYSTEM_DRIVER,
// this parameter is ignored.
// - DesiredAccess - a combination of following flags:
// SERVICE_ALL_ACCESS (default value), SERVICE_CHANGE_CONFIG,
// SERVICE_ENUMERATE_DEPENDENTS, SERVICE_INTERROGATE, SERVICE_PAUSE_CONTINUE,
// SERVICE_QUERY_CONFIG, SERVICE_QUERY_STATUS, SERVICE_START, SERVICE_STOP,
// SERVICE_USER_DEFINED_CONTROL
// - ServiceType - a set of following flags:
// SERVICE_WIN32_OWN_PROCESS (default value, which specifies a Win32 service
// that runs in its own process), SERVICE_WIN32_SHARE_PROCESS,
// SERVICE_KERNEL_DRIVER, SERVICE_FILE_SYSTEM_DRIVER,
// SERVICE_INTERACTIVE_PROCESS (default value, which enables a Win32 service
// process to interact with the desktop)
// - StartType - one of following values:
// SERVICE_BOOT_START, SERVICE_SYSTEM_START,
// SERVICE_AUTO_START (which specifies a device driver or service started by
// the service control manager automatically during system startup),
// SERVICE_DEMAND_START (default value, which specifies a service started by
// a service control manager when a process calls the StartService function,
// that is the TServiceController.Start method), SERVICE_DISABLED
// - ErrorControl - one of following:
// SERVICE_ERROR_IGNORE, SERVICE_ERROR_NORMAL (default value, by which
// the startup program logs the error and displays a message but continues
// the startup operation), SERVICE_ERROR_SEVERE,
// SERVICE_ERROR_CRITICAL
constructor CreateNewService(const TargetComputer, DatabaseName,
Name, DisplayName, Path: string;
const OrderGroup: string = ''; const Dependencies: string = '';
const Username: string = ''; const Password: string = '';
DesiredAccess: DWORD = SERVICE_ALL_ACCESS;
ServiceType: DWORD = SERVICE_WIN32_OWN_PROCESS or SERVICE_INTERACTIVE_PROCESS;
StartType: DWORD = SERVICE_DEMAND_START; ErrorControl: DWORD = SERVICE_ERROR_NORMAL);
/// wrapper around CreateNewService() to install the current executable as service
class function Install(const Name,DisplayName,Description: string;
AutoStart: boolean; ExeName: TFileName=''; Dependencies: string=''): TServiceState;
/// Opens an existing service, in order to control it or its configuration
// from your application. Parameters (strings are unicode-ready since Delphi 2009):
// - TargetComputer - set it to empty string if local computer is the target.
// - DatabaseName - set it to empty string if the default database is supposed
// ('ServicesActive').
// - Name - name of a service.
// - DesiredAccess - a combination of following flags:
// SERVICE_ALL_ACCESS, SERVICE_CHANGE_CONFIG, SERVICE_ENUMERATE_DEPENDENTS,
// SERVICE_INTERROGATE, SERVICE_PAUSE_CONTINUE, SERVICE_QUERY_CONFIG,
// SERVICE_QUERY_STATUS, SERVICE_START, SERVICE_STOP, SERVICE_USER_DEFINED_CONTROL
constructor CreateOpenService(const TargetComputer, DataBaseName, Name: String;
DesiredAccess: DWORD = SERVICE_ALL_ACCESS);
/// release memory and handles
destructor Destroy; override;
/// Handle of SC manager
property SCHandle: THandle read FSCHandle;
/// Handle of service opened or created
// - its value is 0 if something failed in any Create*() method
property Handle: THandle read FHandle;
/// Retrieve the Current status of the service
property Status: TServiceStatus read GetStatus;
/// Retrieve the Current state of the service
property State: TServiceState read GetState;
/// Requests the service to stop
function Stop: boolean;
/// Requests the service to pause
function Pause: boolean;
/// Requests the paused service to resume
function Resume: boolean;
/// Requests the service to update immediately its current status information
// to the service control manager
function Refresh: boolean;
/// Request the service to shutdown
// - this function always return false
function Shutdown: boolean;
/// Removes service from the system, i.e. close the Service
function Delete: boolean;
/// starts the execution of a service with some specified arguments
// - this version expect PChar pointers, either AnsiString (for FPC and old
// Delphi compiler), either UnicodeString (till Delphi 2009)
function Start(const Args: array of PChar): boolean;
/// try to define the description text of this service
procedure SetDescription(const Description: string);
/// this class method will check the command line parameters, and will let
// control the service according to it
// - MyServiceSetup.exe /install will install the service
// - MyServiceSetup.exe /start will start the service
// - MyServiceSetup.exe /stop will stop the service
// - MyServiceSetup.exe /uninstall will uninstall the service
// - so that you can write in the main block of your .dpr:
// !CheckParameters('MyService.exe',HTTPSERVICENAME,HTTPSERVICEDISPLAYNAME);
// - if ExeFileName='', it will install the current executable
// - optional Description and Dependencies text may be specified
class procedure CheckParameters(const ExeFileName: TFileName;
const ServiceName,DisplayName,Description: string; const Dependencies: string='');
end;
{$M+}
TService = class;
{$M-}
/// callback procedure for Windows Service Controller
TServiceControlHandler = procedure(CtrlCode: DWORD); stdcall;
/// event triggered for Control handler
TServiceControlEvent = procedure(Sender: TService; Code: DWORD) of object;
/// event triggered to implement the Service functionality
TServiceEvent = procedure(Sender: TService) of object;
/// TService is the class used to implement a service provided by an application
TService = class
protected
fSName: String;
fDName: String;
fStartType: DWORD;
fServiceType: DWORD;
fData: DWORD;
fControlHandler: TServiceControlHandler;
fOnControl: TServiceControlEvent;
fOnInterrogate: TServiceEvent;
fOnPause: TServiceEvent;
fOnShutdown: TServiceEvent;
fOnStart: TServiceEvent;
fOnExecute: TServiceEvent;
fOnResume: TServiceEvent;
fOnStop: TServiceEvent;
fStatusRec: TServiceStatus;
fArgsList: array of string;
fJumper: PByteArray;
fStatusHandle: THandle;
function GetArgCount: Integer;
function GetArgs(Idx: Integer): String;
function GetInstalled: boolean;
procedure SetStatus(const Value: TServiceStatus);
procedure CtrlHandle(Code: DWORD);
function GetControlHandler: TServiceControlHandler;
procedure SetControlHandler(const Value: TServiceControlHandler);
public
/// this method is the main service entrance, from the OS point of view
// - it will call OnControl/OnStop/OnPause/OnResume/OnShutdown events
// - and report the service status to the system (via ReportStatus method)
procedure DoCtrlHandle(Code: DWORD); virtual;
/// Creates the service
// - the service is added to the internal registered services
// - main application must call the global ServicesRun procedure to actually
// start the services
// - caller must free the TService instance when it's no longer used
constructor Create(const aServiceName, aDisplayName: String); reintroduce; virtual;
/// free memory and release handles
destructor Destroy; override;
/// Reports new status to the system
function ReportStatus(dwState, dwExitCode, dwWait: DWORD): BOOL;
/// Installs the service in the database
// - return true on success
// - create a local TServiceController with the current executable file,
// with the supplied command line parameters
function Install(const Params: string=''): boolean;
/// Removes the service from database
// - uses a local TServiceController with the current Service Name
procedure Remove;
/// Starts the service
// - uses a local TServiceController with the current Service Name
procedure Start;
/// Stops the service
// - uses a local TServiceController with the current Service Name
procedure Stop;
/// this is the main method, in which the Service should implement its run
procedure Execute; virtual;
/// Number of arguments passed to the service by the service controler
property ArgCount: Integer read GetArgCount;
/// List of arguments passed to the service by the service controler
property Args[Idx: Integer]: String read GetArgs;
/// Any data You wish to associate with the service object
property Data: DWORD read FData write FData;
/// Whether service is installed in DataBase
// - uses a local TServiceController to check if the current Service Name exists
property Installed: boolean read GetInstalled;
/// Current service status
// - To report new status to the system, assign another
// value to this record, or use ReportStatus method (preferred)
property Status: TServiceStatus read fStatusRec write SetStatus;
/// Callback handler for Windows Service Controller
// - if handler is not set, then auto generated handler calls DoCtrlHandle
// (note that this auto-generated stubb is... not working yet - so you should
// either set your own procedure to this property, or use TServiceSingle)
// - a typical control handler may be defined as such:
// ! var MyGlobalService: TService;
// !
// ! procedure MyServiceControlHandler(Opcode: LongWord); stdcall;
// ! begin
// ! if MyGlobalService<>nil then
// ! MyGlobalService.DoCtrlHandle(Opcode);
// ! end;
// !
// ! ...
// ! MyGlobalService := TService.Create(...
// ! MyGlobalService.ControlHandler := MyServiceControlHandler;
property ControlHandler: TServiceControlHandler
read GetControlHandler write SetControlHandler;
/// Start event is executed before the main service thread (i.e. in the Execute method)
property OnStart: TServiceEvent read fOnStart write fOnStart;
/// custom Execute event
// - launched in the main service thread (i.e. in the Execute method)
property OnExecute: TServiceEvent read fOnExecute write fOnExecute;
/// custom event triggered when a Control Code is received from Windows
property OnControl: TServiceControlEvent read fOnControl write fOnControl;
/// custom event triggered when the service is stopped
property OnStop: TServiceEvent read fOnStop write fOnStop;
/// custom event triggered when the service is paused
property OnPause: TServiceEvent read fOnPause write fOnPause;
/// custom event triggered when the service is resumed
property OnResume: TServiceEvent read fOnResume write fOnResume;
/// custom event triggered when the service receive an Interrogate
property OnInterrogate: TServiceEvent read fOnInterrogate write fOnInterrogate;
/// custom event triggered when the service is shut down
property OnShutdown: TServiceEvent read fOnShutdown write fOnShutdown;
published
/// Name of the service. Must be unique
property ServiceName: String read fSName;
/// Display name of the service
property DisplayName: String read fDName write fDName;
/// Type of service
property ServiceType: DWORD read fServiceType write fServiceType;
/// Type of start of service
property StartType: DWORD read fStartType write fStartType;
end;
/// inherit from this service if your application has a single service
// - note that TService jumper does not work well - so use this instead
TServiceSingle = class(TService)
public
/// will set a global function as service controller
constructor Create(const aServiceName, aDisplayName: String); override;
/// will release the global service controller
destructor Destroy; override;
end;
var
/// the internal list of Services handled by this unit
// - not to be accessed directly: create TService instances, and they will
// be added/registered to this list
// - then run the global ServicesRun procedure
// - every TService instance is to be freed by the main application, when
// it's no more used
Services: TSynList = nil;
/// the main TService instance running
ServiceSingle: TServiceSingle = nil;
/// launch the registered Services execution
// - the registered list of service provided by the aplication is sent
// to the operating system
// - returns TRUE on success
// - returns FALSE on error (to get extended information, call GetLastError)
function ServicesRun: boolean;
/// convert the Control Code retrieved from Windows into a service state
// enumeration item
function CurrentStateToServiceState(CurrentState: DWORD): TServiceState;
/// return the ready to be displayed text of a TServiceState value
function ServiceStateText(State: TServiceState): string;
/// return service PID
function GetServicePid(const aServiceName: string): DWORD;
/// kill Windows process
function KillProcess(pid: DWORD; waitseconds: integer = 30): boolean;
{$else}
/// low-level function able to properly run or fork the current process
// then execute the start/stop methods of a TSynDaemon / TDDDDaemon instance
// - fork will create a local /run/[ProgramName]-[ProgramPathHash].pid file name
procedure RunUntilSigTerminated(daemon: TObject; dofork: boolean;
const start, stop: TThreadMethod; log: TSynLog = nil; const servicename: string = '');
/// kill a process previously created by RunUntilSigTerminated(dofork=true)
// - will lookup a local /run/[ProgramName]-[ProgramPathHash].pid file name to
// retrieve the actual PID to be killed, then send a SIGTERM, and wait
// waitseconds for the .pid file to disapear
// - returns true on success, false on error (e.g. no valid .pid file or
// the file didn't disappear, which may mean that the daemon is broken)
function RunUntilSigTerminatedForKill(waitseconds: integer = 30): boolean;
/// local .pid file name as created by RunUntilSigTerminated(dofork=true)
function RunUntilSigTerminatedPidFile: TFileName;
var
/// once SynDaemonIntercept has been called, this global variable
// contains the SIGQUIT / SIGTERM / SIGINT received signal
SynDaemonTerminated: integer;
/// enable low-level interception of executable stop signals
// - any SIGQUIT / SIGTERM / SIGINT signal will set appropriately the global
// SynDaemonTerminated variable, with an optional logged entry to log
// - as called e.g. by RunUntilSigTerminated()
// - you can call this method several times with no issue
procedure SynDaemonIntercept(log: TSynLog=nil);
{$endif MSWINDOWS}
type
/// command line patterns recognized by ParseCommandArgs()
TParseCommand = (
pcHasRedirection, pcHasSubCommand, pcHasParenthesis,
pcHasJobControl, pcHasWildcard, pcHasShellVariable,
pcUnbalancedSingleQuote, pcUnbalancedDoubleQuote,
pcTooManyArguments, pcInvalidCommand, pcHasEndingBackSlash);
TParseCommands = set of TParseCommand;
PParseCommands = ^TParseCommands;
/// used to store references of arguments recognized by ParseCommandArgs()
TParseCommandsArgs = array[0..31] of PAnsiChar;
PParseCommandsArgs = ^TParseCommandsArgs;
const
/// identifies some bash-specific processing
PARSECOMMAND_BASH = [pcHasRedirection .. pcHasShellVariable];
/// identifies obvious invalid content
PARSECOMMAND_ERROR = [pcUnbalancedSingleQuote .. pcHasEndingBackSlash];
/// low-level parsing of a RunCommand() execution command
// - parse and fills argv^[0..argc^-1] with corresponding arguments, after
// un-escaping and un-quoting if applicable, using temp^ to store the content
// - if argv=nil, do only the parsing, not the argument extraction - could be
// used for fast validation of the command line syntax
// - you can force arguments OS flavor using the posix parameter - note that
// Windows parsing is not consistent by itself (e.g. double quoting or
// escaping depends on the actual executable called) so returned flags
// should be considered as indicative only with posix=false
function ParseCommandArgs(const cmd: RawUTF8; argv: PParseCommandsArgs = nil;
argc: PInteger = nil; temp: PRawUTF8 = nil;
posix: boolean = {$ifdef MSWINDOWS}false{$else}true{$endif}): TParseCommands;
function ToText(cmd: TParseCommands): shortstring; overload;
{$ifdef HASINLINE}inline;{$endif}
/// like SysUtils.ExecuteProcess, but allowing not to wait for the process to finish
// - optional env value follows 'n1=v1'#0'n2=v2'#0'n3=v3'#0#0 Windows layout
function RunProcess(const path, arg1: TFileName; waitfor: boolean;
const arg2: TFileName=''; const arg3: TFileName=''; const arg4: TFileName='';
const arg5: TFileName=''; const env: TFileName=''; envaddexisting: boolean=false): integer;
/// like fpSystem, but cross-platform
// - under POSIX, calls bash only if needed, after ParseCommandArgs() analysis
// - under Windows (especially Windows 10), creating a process can be dead slow
// https://randomascii.wordpress.com/2019/04/21/on2-in-createprocess
function RunCommand(const cmd: TFileName; waitfor: boolean;
const env: TFileName=''; envaddexisting: boolean=false;
parsed: PParseCommands=nil): integer;
{ *** cross-plaform high-level services/daemons }
type
/// abstract parent containing information able to initialize a TSynDaemon class
// - will handle persistence as JSON local files
// - you may consider using TDDDAppSettingsAbstract from dddInfraSettings
TSynDaemonSettings = class(TSynJsonFileSettings)
protected
fServiceName: string;
fServiceDisplayName: string;
fLog: TSynLogInfos;
fLogPath: TFileName;
fLogRotateFileCount: integer;
fLogClass: TSynLogClass;
fServiceDependencies: string;
public
/// initialize and set the default settings
constructor Create; override;
/// define the log information into the supplied TSynLog class
// - if you don't call this method, the logging won't be initiated
// - is to be called typically in the overriden Create constructor of the
// associated TSynDaemon class, just after "inherited Create"
procedure SetLog(aLogClass: TSynLogClass);
/// returns user-friendly description of the service, including version
// information and company copyright (if available)
function ServiceDescription: string;
/// read-only access to the TSynLog class, if SetLog() has been called
property LogClass: TSynLogClass read fLogClass;
/// optional service dependencies
// - not published by default: could be defined if needed, or e.g. set in
// overriden constructor
// - several depending services may be set by appending #0 between names
property ServiceDependencies: string read fServiceDependencies write fServiceDependencies;
published
/// the service name, as used internally by Windows or the TSynDaemon class
// - default is the executable name
property ServiceName: string read fServiceName write fServiceName;
/// the service name, as displayed by Windows or at the console level
// - default is the executable name
property ServiceDisplayName: string read fServiceDisplayName write fServiceDisplayName;
/// if not void, will enable the logs (default is LOG_STACKTRACE)
property Log: TSynLogInfos read fLog write fLog;
/// allow to customize where the logs should be written
property LogPath: TFileName read fLogPath write fLogPath;
/// how many files will be rotated (default is 2)
property LogRotateFileCount: integer read fLogRotateFileCount write fLogRotateFileCount;
end;
/// meta-class of TSynDaemon settings information
TSynDaemonSettingsClass = class of TSynDaemonSettings;
/// abstract parent to implements a daemon/service
// - inherit from this abstract class and override Start and Stop methods
// - you may consider using TDDDAdministratedDaemon from dddInfraApps
TSynDaemon = class(TSynPersistent)
protected
fConsoleMode: boolean;
fWorkFolderName: TFileName;
fSettings: TSynDaemonSettings;
function CustomCommandLineSyntax: string; virtual;
{$ifdef MSWINDOWS}
procedure DoStart(Sender: TService);
procedure DoStop(Sender: TService);
{$endif}
public
/// initialize the daemon, creating the associated settings
// - TSynDaemonSettings instance will be owned and freed by the daemon
// - any non supplied folder name will be replaced by a default value
// (executable folder under Windows, or /etc /var/log on Linux)
constructor Create(aSettingsClass: TSynDaemonSettingsClass;
const aWorkFolder, aSettingsFolder, aLogFolder: TFileName;
const aSettingsExt: TFileName = '.settings';
const aSettingsName: TFileName = ''); reintroduce;
/// main entry point of the daemon, to process the command line switches
// - aAutoStart is used only under Windows
procedure CommandLine(aAutoStart: boolean=true);
/// inherited class should override this abstract method with proper process
procedure Start; virtual; abstract;
/// inherited class should override this abstract method with proper process
// - should do nothing if the daemon was already stopped
procedure Stop; virtual; abstract;
/// call Stop, finalize the instance, and its settings
destructor Destroy; override;
published
/// if this instance was run as /console or /verb
property ConsoleMode: boolean read fConsoleMode;
/// the settings associated with this daemon
// - will be allocated in Create constructor, and released in Destroy
property Settings: TSynDaemonSettings read fSettings;
end;
{$ifdef MSWINDOWS}
/// Enum synchronized with WinAPI
// - see https://docs.microsoft.com/en-us/windows/desktop/secauthz/privilege-constants
TWinSystemPrivilege = (wspCreateToken, wspAssignPrimaryToken, wspLockMemory,
wspIncreaseQuota, wspUnsolicitedInput, wspMachineAccount, wspTCP, wspSecurity,
wspTakeOwnership, wspLoadDriver, wspSystemProfile, wspSystemTime, wspProfSingleProcess,
wspIncBasePriority, wspCreatePageFile, wspCreatePermanent, wspBackup, wspRestore,
wspShutdown, wspDebug, wspAudit, wspSystemEnvironment, wspChangeNotify,
wspRemoteShutdown, wspUndock, wspSyncAgent, wspEnableDelegation, wspManageVolume,
wspImpersonate, wspCreateGlobal, wspTrustedCredmanAccess, wspRelabel,
wspIncWorkingSet, wspTimeZone, wspCreateSymbolicLink);
TWinSystemPrivileges = set of TWinSystemPrivilege;
TPrivilegeTokenType = (pttProcess, pttThread);
/// object dedicated to management of available privileges for Windows platform
// - not all available privileges are active for process
// - for usage of more advanced WinAPI, explicit enabling of privilege is
// sometimes needed
TSynWindowsPrivileges = object
private
fAvailable: TWinSystemPrivileges;
fEnabled: TWinSystemPrivileges;
fDefEnabled: TWinSystemPrivileges;
function SetPrivilege(aPrivilege: Pointer; aEnablePrivilege: boolean): boolean;
procedure LoadPrivileges;
public
/// handle to privileges token
Token: THandle;
/// initialize the object dedicated to management of available privileges
// - aTokenPrivilege can be used for current process or current thread
procedure Init(aTokenPrivilege: TPrivilegeTokenType = pttProcess);
/// finalize the object and relese Token handle
// - aRestoreInitiallyEnabled parameter can be used to restore initially
// state of enabled privileges
procedure Done(aRestoreInitiallyEnabled: boolean = true);
/// enable privilege
// - if aPrivilege is already enabled return true, if operation is not
// possible (required privilege doesn't exist or API error) return false
function Enable(aPrivilege: TWinSystemPrivilege): boolean;
/// disable privilege
// - if aPrivilege is already disabled return true, if operation is not
// possible (required privilege doesn't exist or API error) return false
function Disable(aPrivilege: TWinSystemPrivilege): boolean;
/// set of available privileges for current process/thread
property Available: TWinSystemPrivileges read fAvailable;
/// set of enabled privileges for current process/thread
property Enabled: TWinSystemPrivileges read fEnabled;
end;
TWinProcessAvailableInfos = set of (wpaiPID, wpaiBasic, wpaiPEB, wpaiCommandLine, wpaiImagePath);
PWinProcessInfo = ^TWinProcessInfo;
TWinProcessInfo = record
AvailableInfo: TWinProcessAvailableInfos;
PID: Cardinal;
ParentPID: Cardinal;
SessionID: Cardinal;
PEBBaseAddress: Pointer;
AffinityMask: Cardinal;
BasePriority: LongInt;
ExitStatus: LongInt;
BeingDebugged: Byte;
ImagePath: SynUnicode;
CommandLine: SynUnicode;
end;
TWinProcessInfoDynArray = array of TWinProcessInfo;
procedure GetProcessInfo(aPid: Cardinal; out aInfo: TWinProcessInfo); overload;
procedure GetProcessInfo(const aPidList: TCardinalDynArray; out aInfo: TWinProcessInfoDynArray); overload;
{$endif MSWINDOWS}
const
/// text identifier typically used before command line switches
// - equals '/' on Windows, and '--' on POSIX systems
CMDLINESWITCH = {$ifdef MSWINDOWS}'/'{$else}'--'{$endif};
implementation
{$ifdef MSWINDOWS}
{ TServiceController }
constructor TServiceController.CreateNewService(const TargetComputer,
DatabaseName,Name,DisplayName,Path,OrderGroup,Dependencies,Username,Password: String;
DesiredAccess,ServiceType,StartType,ErrorControl: DWORD);
var Exe: TFileName;
backupError: cardinal;
begin
inherited Create;
if Path='' then begin
ServiceLog.Add.Log(sllError,'CreateNewService("%","%") with Path=""',
[Name,DisplayName]);
Exit;
end;
if TargetComputer='' then
if GetDriveType(pointer(ExtractFileDrive(Path)))=DRIVE_REMOTE then begin
Exe := ExpandUNCFileName(Path);
if (copy(Exe,1,12)<>'\\localhost\') or (Exe[14]<>'$') then begin
ServiceLog.Add.Log(sllError,'CreateNewService("%","%") on remote drive: Path="%" is %',
[Name,DisplayName,Path,Exe]);
Exit;
end;
system.delete(Exe,1,12); // \\localhost\c$\... -> c:\...
Exe[2] := ':';
end else
Exe := Path;
StringToUTF8(Name,FName);
FSCHandle := OpenSCManager(pointer(TargetComputer), pointer(DatabaseName),
SC_MANAGER_ALL_ACCESS);
if FSCHandle=0 then begin
backupError := GetLastError;
ServiceLog.Add.Log(sllLastError,'OpenSCManager(''%'',''%'') for [%]',
[TargetComputer,DatabaseName,FName]);
SetLastError(backupError);
Exit;
end;
FHandle := CreateService(FSCHandle, pointer(Name), pointer(DisplayName),
DesiredAccess, ServiceType, StartType, ErrorControl, pointer(Exe),
pointer(OrderGroup), nil, pointer(Dependencies),
pointer(Username), pointer(Password));
if FHandle=0 then begin
backupError := GetLastError;
ServiceLog.Add.Log(sllLastError,'CreateService("%","%","%")',[Name,DisplayName,Path]);
SetLastError(backupError);
end;
end;
constructor TServiceController.CreateOpenService(const TargetComputer,
DataBaseName, Name: String; DesiredAccess: DWORD);
var backupError: cardinal;
begin
inherited Create;
StringToUTF8(Name,FName);
FSCHandle := OpenSCManager(pointer(TargetComputer), pointer(DatabaseName),
GENERIC_READ);
if FSCHandle = 0 then begin
backupError := GetLastError;
ServiceLog.Add.Log(sllLastError,'OpenSCManager(''%'',''%'') for [%]',
[TargetComputer,DatabaseName,FName]);
SetLastError(backupError);
Exit;
end;
FHandle := OpenService(FSCHandle, pointer(Name), DesiredAccess);
if FHandle=0 then begin
backupError := GetLastError;
ServiceLog.Add.Log(sllLastError,'OpenService("%")',[Name]);
SetLastError(backupError);
end;
end;
function TServiceController.Delete: boolean;
begin
Result := FALSE;
if FHandle <> 0 then
if DeleteService(FHandle) then begin
Result := CloseServiceHandle(FHandle);
FHandle := 0;
end
else ServiceLog.Add.Log(sllLastError,'DeleteService("%")',[FName]);
end;
destructor TServiceController.Destroy;
begin
if FHandle <> 0 then
CloseServiceHandle(FHandle);
if FSCHandle <> 0 then
CloseServiceHandle(FSCHandle);
inherited;
end;
function TServiceController.GetState: TServiceState;
begin
if (self=nil) or (FSCHandle=0) or (FHandle=0) then
result := ssNotInstalled else
result := CurrentStateToServiceState(Status.dwCurrentState);
ServiceLog.Add.Log(sllTrace,FName,TypeInfo(TServiceState),result,self);
end;
function TServiceController.GetStatus: TServiceStatus;
begin
FillChar(FStatus, Sizeof(FStatus), 0);
QueryServiceStatus(FHandle, FStatus);
Result := FStatus;
end;
function TServiceController.Pause: boolean;
begin
Result := ControlService(FHandle, SERVICE_CONTROL_PAUSE, FStatus);
end;
function TServiceController.Refresh: boolean;
begin
Result := ControlService(FHandle, SERVICE_CONTROL_INTERROGATE, FStatus);
end;
function TServiceController.Resume: boolean;
begin
Result := ControlService(FHandle, SERVICE_CONTROL_CONTINUE, FStatus);
end;
function TServiceController.Shutdown: boolean;
begin
Result := ControlService(FHandle, SERVICE_CONTROL_SHUTDOWN, FStatus);
end;
function TServiceController.Start(const Args: array of PChar): boolean;
begin
if length(Args)=0 then
Result := StartService(FHandle, 0, nil) else
Result := StartService(FHandle, length(Args), @Args[0]);
end;
function TServiceController.Stop: boolean;
begin
Result := ControlService(FHandle, SERVICE_CONTROL_STOP, FStatus);
end;
procedure TServiceController.SetDescription(const Description: string);
var desc: SynUnicode;
begin
if Description='' then
exit;
StringToSynUnicode(Description, desc);
ChangeServiceConfig2(FHandle, SERVICE_CONFIG_DESCRIPTION, @desc);
end;
class procedure TServiceController.CheckParameters(const ExeFileName: TFileName;
const ServiceName, DisplayName, Description, Dependencies: string);
var param: string;
i: integer;
procedure ShowError(const Msg: RawUTF8);
begin
ServiceLog.Add.Log(sllLastError,'During % for %',[Msg,param]);
if not IsConsole then
exit;
{$I-} // ignore if no console has been allocated
writeln(ServiceName,': Error "',Msg,'" for ',param);
ioresult;
{$I+}
end;
begin
for i := 1 to ParamCount do begin
param := SysUtils.LowerCase(paramstr(i));
ServiceLog.Add.Log(sllInfo,'Controling % with command [%]',[ServiceName,param]);
if param='/install' then
TServiceController.Install(
ServiceName,DisplayName,Description,true,ExeFileName,Dependencies) else
with TServiceController.CreateOpenService('','',ServiceName) do
try
if State=ssErrorRetrievingState then
ShowError('State') else
if param='/uninstall' then begin
if not Stop then
ShowError('Stop');
if not Delete then
ShowError('Delete');
end else
if param='/stop' then begin
if not Stop then
ShowError('Stop');
end else
if param='/start' then begin
if not Start([]) then
ShowError('Start');
end;
finally
Free;
end;
end;
end;
class function TServiceController.Install(const Name, DisplayName,
Description: string; AutoStart: boolean; ExeName: TFileName; Dependencies: string): TServiceState;
var ctrl: TServiceController;
start: DWORD;
begin
if AutoStart then
start := SERVICE_AUTO_START else
start := SERVICE_DEMAND_START;
if ExeName='' then
ExeName := ExeVersion.ProgramFileName;
ctrl := TServiceController.CreateNewService('','',Name,DisplayName,ExeName,
'',Dependencies,'','',SERVICE_ALL_ACCESS,SERVICE_WIN32_OWN_PROCESS,start);
try
result := ctrl.State;
if result<>ssNotInstalled then
ctrl.SetDescription(Description);
finally
ctrl.Free;
end;
end;
{ TService }
function FindServiceIndex(const Name: String): integer;
begin
if Services<>nil then
for result := 0 to Services.Count-1 do
if TService(Services.List[result]).ServiceName=Name then
exit;
result := -1;
end;
constructor TService.Create(const aServiceName, aDisplayName: String);
begin
if FindServiceIndex(aServiceName)>=0 then
raise EServiceException.CreateUTF8('%.Create: Attempt to install a service ' +
'with duplicated name: %', [self, aServiceName]);
fSName := aServiceName;
fDName := aDisplayName;
if aDisplayName = '' then
fDName := aServiceName;
if Services=nil then
GarbageCollectorFreeAndNil(Services,TSynList.Create);
Services.Add(self);
fServiceType := SERVICE_WIN32_OWN_PROCESS or SERVICE_INTERACTIVE_PROCESS;
fStartType := SERVICE_AUTO_START;
fStatusRec.dwServiceType := fServiceType;
fStatusRec.dwCurrentState := SERVICE_STOPPED;
fStatusRec.dwControlsAccepted := 31;
fStatusRec.dwWin32ExitCode := NO_ERROR;
ServiceLog.Add.Log(sllInfo,'Create: % (%) running as [%]',
[ServiceName,aDisplayName,ExeVersion.ProgramFullSpec],self);
end;
procedure TService.CtrlHandle(Code: DWORD);
begin
DoCtrlHandle(Code);
end;
destructor TService.Destroy;
var i: integer;
begin
if fsName<>'' then begin
i := FindServiceIndex(fsName);
if i<0 then
raise EServiceException.CreateUTF8('%.Destroy: Cannot find service % to remove',
[self, fsName]);
Services.Delete(i);
if fJumper<>nil then
VirtualFree(fJumper, 0, MEM_RELEASE);
end;
inherited Destroy;
end;
procedure TService.DoCtrlHandle(Code: DWORD);
var log: ISynLog;
begin
log := ServiceLog.Enter(self, 'DoCtrlHandle');
if log<>nil then
log.Log(sllInfo,'%: command % received from OS',[ServiceName,Code],self);
try
case Code of
SERVICE_CONTROL_STOP: begin
ReportStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
try
if Assigned(fOnStop) then
fOnStop(Self);
ReportStatus(SERVICE_STOPPED, NO_ERROR, 0);
except
ReportStatus(SERVICE_STOPPED, ERROR_CAN_NOT_COMPLETE, 0);
end;
end;
SERVICE_CONTROL_PAUSE: begin
ReportStatus(SERVICE_PAUSE_PENDING, NO_ERROR, 0);
try
if Assigned(fOnPause) then
fOnPause(Self);
ReportStatus(SERVICE_PAUSED, NO_ERROR, 0)
except
ReportStatus(SERVICE_PAUSED, ERROR_CAN_NOT_COMPLETE, 0)
end;
end;
SERVICE_CONTROL_CONTINUE: begin
ReportStatus(SERVICE_CONTINUE_PENDING, NO_ERROR, 0);
try
if Assigned(fOnResume) then
fOnResume(Self);
ReportStatus(SERVICE_RUNNING, NO_ERROR, 0);
except
ReportStatus(SERVICE_RUNNING, ERROR_CAN_NOT_COMPLETE, 0);
end;
end;
SERVICE_CONTROL_SHUTDOWN: begin
if Assigned(fOnShutdown) then
fOnShutdown(Self);
Code := 0;
end;
SERVICE_CONTROL_INTERROGATE: begin
SetServiceStatus(FStatusHandle, fStatusRec);
if Assigned(fOnInterrogate) then
fOnInterrogate(Self);
end;
end;
if Assigned(fOnControl) then
fOnControl(Self, Code);
except
end;
end;
procedure TService.Execute;
begin
try
if Assigned(fOnStart) then
fOnStart(@Self);
ReportStatus(SERVICE_RUNNING, NO_ERROR, 0);
if Assigned(fOnExecute) then
fOnExecute(@Self);
except
ReportStatus(SERVICE_RUNNING, ERROR_CAN_NOT_COMPLETE, 0);
end;
end;
function TService.GetArgCount: Integer;
begin
result := length(FArgsList);
end;
function TService.GetArgs(Idx: Integer): String;
begin
if cardinal(Idx)>cardinal(high(FArgsList)) then
result := '' else // avoid GPF
result := FArgsList[Idx];
end;
{$ifdef CPUX86}
{.$define X86JUMPER} // this preliminary version is buggy so disabled
// a single service per excecutable is fine enough for our daemons
// also for proper Delphi 10.4 compilation with no hint
{$endif CPUX86}
{$ifdef X86JUMPER}
procedure JumpToService;
asm
pop eax
mov eax, [eax] // retrieve TService self value
mov edx, [esp+4]
call TService.CtrlHandle
ret 4
end;
{$endif X86JUMPER}
function TService.GetControlHandler: TServiceControlHandler;
{$ifdef X86JUMPER}
var AfterCallAddr: Pointer;
Offset: Integer;
{$endif X86JUMPER}
begin
Result := fControlHandler;
if not Assigned(Result) then
ServiceLog.Add.Log(sllError,'%.GetControlHandler with fControlHandler=nil: '+
'use TServiceSingle or set a custom ControlHandler',[self]);
{$ifdef X86JUMPER}
if not Assigned(Result) then
begin
raise EServiceException.Create('Automated jumper generation is not working: '+
'use TServiceSingle or set a custom ControlHandler');
if fJumper=nil then begin
fJumper := VirtualAlloc(nil, 5+sizeof(Pointer), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if fJumper=nil then
raise EServiceException.CreateUTF8('Cannot allocate memory for service jump gate: %',
[fSName]);
AfterCallAddr := Pointer(PtrUInt(fJumper)+5);
Offset := PtrUInt(@JumpToService)-PtrUInt(AfterCallAddr);
fJumper[0] := $E8; // call opcode
PInteger(@fJumper[1])^ := Offset; // points to JumpToService
PPtrUInt(@fJumper[5])^ := PtrUInt(self); // will be set as EAX=self
end;
Result := Pointer(fJumper);
end;
{$endif X86JUMPER}
end;
function TService.GetInstalled: boolean;
begin
with TServiceController.CreateOpenService('','',fSName,SERVICE_QUERY_STATUS) do
try
result := Handle<>0;
finally
Free;
end;
end;
function TService.Install(const Params: string): boolean;
var schService: SC_HANDLE;
schSCManager: SC_HANDLE;
ServicePath: TFileName;
begin
result := false;
if installed then
exit;
ServicePath := ExeVersion.ProgramFileName;
if Params<>'' then
ServicePath := ServicePath+' '+Params;
schSCManager := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS);
if (schSCManager>0) then begin
schService := CreateService(schSCManager,
pointer(fSName), pointer(fDName), SERVICE_ALL_ACCESS,
fServiceType, fStartType, SERVICE_ERROR_NORMAL,
pointer(ServicePath), nil, nil, nil, nil, nil);
if (schService>0) then begin
result := true;
CloseServiceHandle(schService);
end;
end;
end;
procedure TService.Remove;
begin
with TServiceController.CreateOpenService('','',fSName,SERVICE_ALL_ACCESS) do
try
if Handle=0 then exit;
Stop;
Delete;
finally
Free;
end;
end;
function TService.ReportStatus(dwState, dwExitCode, dwWait: DWORD): BOOL;
var status: string;
begin
status := ServiceStateText(CurrentStateToServiceState(dwState));
ServiceLog.Add.Log(sllInfo,'% ReportStatus(%,%,%)',
[ServiceName,status,dwExitCode,dwWait],self);
if dwState = SERVICE_START_PENDING then
fStatusRec.dwControlsAccepted := 0 else
fStatusRec.dwControlsAccepted := 31;
fStatusRec.dwCurrentState := dwState;
fStatusRec.dwWin32ExitCode := dwExitCode;
fStatusRec.dwWaitHint := dwWait;
if (dwState = SERVICE_RUNNING) or (dwState = SERVICE_STOPPED) then
fStatusRec.dwCheckPoint := 0 else
inc(fStatusRec.dwCheckPoint);
result := SetServiceStatus(FStatusHandle, fStatusRec);
if not result then
ServiceLog.Add.Log(sllLastError,'% ReportStatus(%,%,%)',
[ServiceName,status,dwExitCode,dwWait],self);
end;
procedure TService.SetControlHandler(const Value: TServiceControlHandler);
begin
fControlHandler := Value;
if fJumper<>nil then
VirtualFree(fJumper, 0, MEM_RELEASE);
end;
procedure TService.SetStatus(const Value: TServiceStatus);
begin
fStatusRec := Value;
if FStatusHandle <> 0 then
SetServiceStatus(FStatusHandle, fStatusRec);
end;
procedure TService.Start;
begin
with TServiceController.CreateOpenService('','',fSName,SERVICE_ALL_ACCESS) do
try
Start([]);
finally
Free;
end;
end;
procedure TService.Stop;
begin
with TServiceController.CreateOpenService('','',fSName,SERVICE_ALL_ACCESS) do
try
Stop;
finally
Free;
end;
end;
function CurrentStateToServiceState(CurrentState: DWORD): TServiceState;
begin
case CurrentState of
SERVICE_STOPPED: result := ssStopped;
SERVICE_START_PENDING: result := ssStarting;
SERVICE_STOP_PENDING: result := ssStopping;
SERVICE_RUNNING: result := ssRunning;
SERVICE_CONTINUE_PENDING: result := ssResuming;
SERVICE_PAUSE_PENDING: result := ssPausing;
SERVICE_PAUSED: result := ssPaused;
else result := ssNotInstalled; // e.g. SERVICE_CONTROL_SHUTDOWN
end;
end;
function ServiceStateText(State: TServiceState): string;
var P: PShortString;
begin
P := GetEnumName(TypeInfo(TServiceState),ord(State));
result := string(copy(P^,3,length(P^)-2));
end;
function GetServicePid(const aServiceName: string): DWORD;
var
ssp: TServiceStatusProcess;
scm: THandle;
svc: THandle;
size: DWORD;
begin
result := 0;
scm := OpenSCManager(nil, nil, SC_MANAGER_CONNECT);
if scm <> 0 then
try
svc := OpenService(scm, pointer(aServiceName), SERVICE_QUERY_STATUS);
if svc <> 0 then
try
if QueryServiceStatusEx(svc, SC_STATUS_PROCESS_INFO, @ssp, SizeOf(TServiceStatusProcess), size) then
result := ssp.dwProcessId
else
ServiceLog.Add.Log(sllLastError);
finally
CloseServiceHandle(svc);
end;
finally
CloseServiceHandle(scm);
end;
end;
function KillProcess(pid: DWORD; waitseconds: integer): boolean;
var
ph: THandle;
begin
ph := OpenProcess(PROCESS_TERMINATE or SYNCHRONIZE, false, pid);
result := ph <> 0;
if result then begin
try
result := TerminateProcess(ph, 0) and (WaitForSingleObject(ph, waitseconds * 1000) <> WAIT_TIMEOUT);
finally
CloseHandle(ph);
end;
end;
end;
{ function that a service process specifies as the entry point function
of a particular service. The function can have any application-defined name
- Args points to an array of pointers that point to null-terminated
argument strings. The first argument in the array is the name of the service,
and subsequent arguments are any strings passed to the service by the process
that called the StartService function to start the service. }
procedure ServiceProc(ArgCount: DWORD; Args: PPChar); stdcall;
var i: integer;
Srv: TService;
begin
i := FindServiceIndex(Args^);
if i<0 then
exit; // avoid any GPF
Srv := Services.Items[i];
for i := 1 to ArgCount-1 do begin
Inc(Args);
SetLength(Srv.FArgsList, length(Srv.FArgsList)+1);
Srv.FArgsList[high(Srv.FArgsList)] := Args^;
end;
Srv.FStatusHandle := RegisterServiceCtrlHandler(
pointer(Srv.fSName), @Srv.ControlHandler);
if Srv.FStatusHandle = 0 then begin
Srv.ReportStatus(SERVICE_STOPPED, GetLastError, 0);
Exit;
end;
Srv.ReportStatus(SERVICE_START_PENDING, 0, 0);
Srv.Execute;
end;
function ServicesRun: boolean;
var S: array of TServiceTableEntry;
service: TService;
i: integer;
{$ifndef NOEXCEPTIONINTERCEPT}
dummy: TSynLog;
{$endif}
begin
if (Services=nil) or (Services.Count=0) then begin
result := false;
exit;
end;
for i := 0 to Services.Count-1 do begin
service := Services.List[i];
if not assigned(service.fControlHandler) then
raise EServiceException.CreateUTF8('%.ControlHandler=nil (ServiceName="%"): '+
'use TServiceSingle or set a custom ControlHandler',[service,service.ServiceName]);
end;
SetLength(S,Services.Count+1); // +1 so that the latest entry is nil
for i := 0 to Services.Count-1 do begin
S[i].lpServiceName := pointer(TService(Services.List[i]).ServiceName);
S[i].lpServiceProc := ServiceProc;
end;
{$ifndef NOEXCEPTIONINTERCEPT}
dummy := GlobalCurrentHandleExceptionSynLog;
GlobalCurrentHandleExceptionSynLog := nil; // don't log any EExternalException
try
{$endif}
result := StartServiceCtrlDispatcher(pointer(S));
{$ifndef NOEXCEPTIONINTERCEPT}
finally
GlobalCurrentHandleExceptionSynLog := dummy;
end;
{$endif}
end;
{ TServiceSingle }
procedure SingleServiceControlHandler(Opcode: LongWord); stdcall;
begin
if ServiceSingle<>nil then
ServiceSingle.DoCtrlHandle(Opcode);
end;
constructor TServiceSingle.Create(const aServiceName,
aDisplayName: String);
begin
inherited Create(aServiceName,aDisplayName);
if ServiceSingle<>nil then
raise EServiceException.Create('Only one TServiceSingle is allowed at a time');
ServiceSingle := self;
ControlHandler := SingleServiceControlHandler;
end;
destructor TServiceSingle.Destroy;
begin
try
inherited;
finally
ServiceSingle := nil;
end;
end;
// redefined here so that we can share code with FPC and Delphi
function CreateProcessW(lpApplicationName: PWideChar; lpCommandLine: PWideChar;
lpProcessAttributes, lpThreadAttributes: PSecurityAttributes;
bInheritHandles: BOOL; dwCreationFlags: DWORD; lpEnvironment: Pointer;
lpCurrentDirectory: PWideChar; const lpStartupInfo: TStartupInfo;
out lpProcessInformation: TProcessInformation): BOOL; stdcall; external kernel32;
function GetExitCodeProcess(hProcess: THandle; out lpExitCode: DWORD): BOOL; stdcall;
external kernel32;
function RunProcess(const path, arg1: TFileName; waitfor: boolean;
const arg2,arg3,arg4,arg5,env: TFileName; envaddexisting: boolean): integer;
begin
result := RunCommand(FormatString('"%" % % % % %', [path, arg1, arg2, arg3, arg4, arg5]),
waitfor, env, envaddexisting);
end;
var
EnvironmentCache: SynUnicode;
function RunCommand(const cmd: TFileName; waitfor: boolean;
const env: TFileName; envaddexisting: boolean; parsed: PParseCommands): integer;
var
startupinfo: TStartupInfo; // _STARTUPINFOW or _STARTUPINFOA is equal here
processinfo: TProcessInformation;
path: TFileName;
wcmd, wenv, wpath: SynUnicode;
e, p: PWideChar;
exitcode: DWORD;
i: integer;
begin
// https://support.microsoft.com/en-us/help/175986/info-understanding-createprocess-and-command-line-arguments
result := -1;
if cmd = '' then
exit;
// CreateProcess can alter the strings -> use local SynUnicode
StringToSynUnicode(cmd, wcmd);
if cmd[1] = '"' then begin
path := copy(cmd, 2, maxInt);
i := Pos('"', path);
if i = 0 then
exit;
SetLength(path, i - 1); // unquote "path" string
end
else begin
i := Pos(' ', cmd);
if i <= 1 then
exit;
path := copy(cmd, 1, i - 1);
end;
path := ExtractFilePath(path);
if path = '' then
path := ExeVersion.ProgramFilePath;
StringToSynUnicode(path, wpath);
if env <> '' then begin
StringToSynUnicode(env, wenv);
if envaddexisting then begin
GlobalLock;
if EnvironmentCache = '' then begin
e := GetEnvironmentStringsW;
p := e;
while p^ <> #0 do
inc(p, StrLenW(p) + 1); // go to name=value#0 pairs end
SetString(EnvironmentCache, e, (PtrUInt(p) - PtrUInt(e)) shr 1);
FreeEnvironmentStringsW(e);
end;
wenv := EnvironmentCache + wenv;
GlobalUnLock;
end;
end;
FillCharFast(startupinfo, SizeOf(startupinfo), 0);
startupinfo.cb := SizeOf(startupinfo);
FillCharFast(processinfo, SizeOf(processinfo), 0);
// https://docs.microsoft.com/pl-pl/windows/desktop/ProcThread/process-creation-flags
if CreateProcessW(nil, pointer(wcmd), nil, nil, false, CREATE_UNICODE_ENVIRONMENT or
CREATE_DEFAULT_ERROR_MODE or DETACHED_PROCESS or CREATE_NEW_PROCESS_GROUP,
pointer(wenv), pointer(wpath), startupinfo, processinfo) then begin
if waitfor then
if WaitForSingleObject(processinfo.hProcess,INFINITE) = WAIT_FAILED then
result := -GetLastError
else if not GetExitCodeProcess(processinfo.hProcess, exitcode) then
result := -GetLastError
else
result := exitcode
else
result := 0;
CloseHandle(processinfo.hProcess);
CloseHandle(processinfo.hThread);
end else
result := -GetLastError;
end;
{$else} // Linux/POSIX signal interception
var
SynDaemonIntercepted: boolean;
SynDaemonInterceptLog: TSynLogClass;
{$ifdef FPC}
procedure DoShutDown(Sig: Longint; Info: PSigInfo; Context: PSigContext); cdecl;
var level: TSynLogInfo;
log: TSynLog;
si_code: integer;
text: TShort4;
begin // code below has no memory (re)allocation
if SynDaemonInterceptLog <> nil then begin
log := SynDaemonInterceptLog.Add;
case Sig of
SIGQUIT: text := 'QUIT';
SIGTERM: text := 'TERM';
SIGINT: text := 'INT';
SIGABRT: text := 'ABRT';
else text := UInt3DigitsToShort(Sig);
end;
if Sig = SIGTERM then // polite quit
level := sllInfo else
level := sllExceptionOS;
if Info=nil then
si_code := 0 else
si_code := Info^.si_code;
log.Writer.CustomOptions := log.Writer.CustomOptions + [twoFlushToStreamNoAutoResize];
log.Log(level, 'SynDaemonIntercepted received SIG%=% si_code=%', [text, Sig, si_code]);
log.Flush({flushtodisk=}Sig <> SIGTERM); // ensure all log is safely written
end;
SynDaemonTerminated := Sig;
end;
{$else}
procedure DoShutDown(Sig: integer); cdecl;
begin
SynDaemonTerminated := Sig;
end;
{$endif FPC}
procedure SynDaemonIntercept(log: TSynLog);
var
saOld, saNew: {$ifdef FPC}SigactionRec{$else}TSigAction{$endif};
begin // note: SIGFPE/SIGSEGV/SIGBUS/SIGILL are handled by the RTL
if SynDaemonIntercepted then
exit;
SynDaemonIntercepted := true;
SynDaemonInterceptLog := log.LogClass;
FillCharFast(saNew, SizeOf(saNew), 0);
{$ifdef FPC}
saNew.sa_handler := @DoShutDown;
fpSigaction(SIGQUIT, @saNew, @saOld);
fpSigaction(SIGTERM, @saNew, @saOld);
fpSigaction(SIGINT, @saNew, @saOld);
fpSigaction(SIGABRT, @saNew, @saOld);
{$else} // Kylix
saNew.__sigaction_handler := @DoShutDown;
sigaction(SIGQUIT, @saNew, @saOld);
sigaction(SIGTERM, @saNew, @saOld);
sigaction(SIGINT, @saNew, @saOld);
sigaction(SIGABRT, @saNew, @saOld);
{$endif}
end;
function RunUntilSigTerminatedPidFile: TFileName;
begin
result := FormatString('%.%.pid', [ExeVersion.ProgramFilePath, ExeVersion.ProgramName]);
end;
function RunUntilSigTerminatedForKill(waitseconds: integer): boolean;
var
pid: PtrInt;
pidfilename: TFileName;
tix: Int64;
begin
result := false;
pidfilename := RunUntilSigTerminatedPidFile;
pid := GetInteger(pointer(StringFromFile(pidfilename)));
if pid <= 0 then
exit;
{$ifdef FPC}
if fpkill(pid, SIGTERM) <> 0 then // polite quit
if fpgeterrno<>ESysESRCH then
{$else} // Kylix
if kill(pid, SIGTERM) <> 0 then
if errno<>ESRCH then
{$endif}
exit else // no such process -> try to delete the .pid file
if DeleteFile(pidfilename) then begin
result := true; // process crashed or hard reboot -> nothing to kill
exit;
end;
if waitseconds <= 0 then begin
result := true;
exit;
end;
tix := GetTickCount64 + waitseconds * 1000;
repeat // RunUntilSigTerminated() below should delete the .pid file
sleep(100);
if not FileExists(pidfilename) then
result := true;
until result or (GetTickCount64 > tix);
if not result then
{$ifdef FPC}fpkill{$else}kill{$endif}(pid, SIGKILL); // finesse
end;
procedure CleanAfterFork;
begin
{$ifdef FPC}fpUMask{$else}umask{$endif}(0); // reset file mask
chdir('/'); // avoid locking current directory
Close(input);
AssignFile(input, '/dev/null');
ReWrite(input);
Close(output);
AssignFile(output, '/dev/null');
ReWrite(output);
{$ifdef FPC}Close{$else}__close{$endif}(stderr);
end;
procedure RunUntilSigTerminated(daemon: TObject; dofork: boolean;
const start, stop: TThreadMethod; log: TSynLog; const servicename: string);
var
pid, sid: {$ifdef FPC}TPID{$else}pid_t{$endif};
pidfilename: TFileName;
const
TXT: array[boolean] of string[4] = ('run', 'fork');
begin
SynDaemonIntercept(log);
if dofork then begin
pidfilename := RunUntilSigTerminatedPidFile;
pid := GetInteger(pointer(StringFromFile(pidfilename)));
if pid > 0 then
if ({$ifdef FPC}fpkill{$else}kill{$endif}(pid, 0) = 0) or not DeleteFile(pidfilename) then
raise EServiceException.CreateUTF8('%.CommandLine Fork failed: % is already forked as pid=%',
[daemon, ExeVersion.ProgramName, pid]);
pid := {$ifdef FPC}fpFork{$else}fork{$endif};
if pid < 0 then
raise EServiceException.CreateUTF8('%.CommandLine Fork failed', [daemon]);
if pid > 0 then // main program - just terminate
exit;
// clean forked instance
sid := {$ifdef FPC}fpSetSID{$else}setsid{$endif};
if sid < 0 then // new session (process group) created?
raise EServiceException.CreateUTF8('%.CommandLine SetSID failed', [daemon]);
CleanAfterFork;
// create local .[ExeVersion.ProgramName].pid file
pid := {$ifdef FPC}fpgetpid{$else}getpid{$endif};
FileFromString(Int64ToUtf8(pid), pidfilename);
end;
try
if log <> nil then
log.Log(sllNewRun, 'Start % /% %', [serviceName, TXT[dofork],
ExeVersion.Version.DetailedOrVoid], daemon);
start;
while SynDaemonTerminated = 0 do
if GetCurrentThreadID = MainThreadID then
CheckSynchronize(100) else
Sleep(100);
finally
if log <> nil then
log.Log(sllNewRun, 'Stop /% from Sig=%', [TXT[dofork], SynDaemonTerminated], daemon);
try
stop;
finally
if dofork and (pidfilename <> '') then begin
DeleteFile(pidfilename);
if log <> nil then
log.Log(sllTrace, 'RunUntilSigTerminated: deleted file %', [pidfilename]);
end;
end;
end;
end;
{$ifndef FPC} // Kylix doesn't have a proper WaitProcess
function WaitProcess(pid: pid_t): pid_t;
var r: pid_t;
s: integer;
begin
repeat
r := WaitPid(pid, @s, 0);
if (r = -1) and (errno = EINTR) then
r := 0;
until r <> 0;
if r < 0 then // WaitPid() failed
result := -1 else
if WIFEXITED(s) then // returns the exit status code
result := WEXITSTATUS(s) else
result := -abs(s); // ensure returns a negative value for other errors
end;
{$endif FPC}
function RunInternal(args: PPAnsiChar; waitfor: boolean;
const env: TFileName; envaddexisting: boolean): integer;
var
pid: {$ifdef FPC}TPID{$else}pid_t{$endif};
e: array[0..511] of PAnsiChar; // max 512 environment variables
envpp: PPAnsiChar;
P: PAnsiChar;
n: PtrInt;
begin
{$ifdef FPC}
{$if (defined(BSD) or defined(SUNOS)) and defined(FPC_USE_LIBC)}
pid := FpvFork;
{$else}
pid := FpFork;
{$ifend}
{$else}
pid := fork; // Kylix
{$endif FPC}
if pid < 0 then begin
result := -1; // fork failed
exit;
end;
if pid = 0 then begin // we are in child process -> switch to new executable
if not waitfor then
CleanAfterFork; // don't share the same console
envpp := envp;
if env <> '' then begin
n := 0;
result := {$ifdef FPC}-ESysE2BIG{$else}-7{$endif};
if envaddexisting and (envpp <> nil) then begin
while envpp^ <> nil do begin
if PosChar(envpp^, #10) = nil then begin // filter simple variables
if n = high(e) - 1 then
exit;
e[n] := envpp^;
inc(n);
end;
inc(envpp);
end;
end;
P := pointer(env); // env follows Windows layout 'n1=v1'#0'n2=v2'#0#0
while P^ <> #0 do begin
if n = high(e) - 1 then
exit;
e[n] := P; // makes POSIX compatible
inc(n);
inc(P, StrLen(P) + 1);
end;
e[n] := nil; // end with null
envpp := @e;
end;
{$ifdef FPC}
FpExecve(args^, args, envpp);
FpExit(127);
{$else}
execve(args^, args, envpp);
_exit(127);
{$endif}
end;
if waitfor then begin
result := WaitProcess(pid);
if result = 127 then
result := -result; // execv() failed in child process
end else
result := 0; // fork success (don't wait for the child process to fail)
end;
function RunProcess(const path, arg1: TFileName; waitfor: boolean;
const arg2,arg3,arg4,arg5,env: TFileName; envaddexisting: boolean): integer;
var
a: array[0..6] of PAnsiChar; // assume no UNICODE on BSD, i.e. as TFileName
begin
a[0] := pointer(path);
a[1] := pointer(arg1);
a[2] := pointer(arg2);
a[3] := pointer(arg3);
a[4] := pointer(arg4);
a[5] := pointer(arg5);
a[6] := nil; // end with null
result := RunInternal(@a, waitfor, env, envaddexisting);
end;
function RunCommand(const cmd: TFileName; waitfor: boolean;
const env: TFileName; envaddexisting: boolean;
parsed: PParseCommands): integer;
var
temp: RawUTF8;
err: TParseCommands;
a: TParseCommandsArgs;
begin
err := ParseCommandArgs(cmd, @a, nil, @temp);
if parsed <> nil then
parsed^ := err;
if err = [] then
// no need to spawn the shell for simple commands
result := RunInternal(a, waitfor, env, envaddexisting)
else if err * PARSECOMMAND_ERROR <> [] then
// no system call for clearly invalid command line
result := {$ifdef FPCLINUXNOTBSD}-ESysELIBBAD{$else}-80{$endif}
else begin // execute complex commands via the shell
a[0] := '/bin/sh';
a[1] := '-c';
a[2] := pointer(cmd);
a[3] := nil;
result := RunInternal(@a, waitfor, env, envaddexisting);
end;
end;
{$endif MSWINDOWS}
function ToText(cmd: TParseCommands): shortstring;
begin
if cmd = [] then
result[0] := #0
else
GetSetNameShort(TypeInfo(TParseCommands), cmd, result, {trim=}true);
end;
function ParseCommandArgs(const cmd: RawUTF8; argv: PParseCommandsArgs;
argc: PInteger; temp: PRawUTF8; posix: boolean): TParseCommands;
var
n: PtrInt;
state: set of (sWhite, sInArg, sInSQ, sInDQ, sSpecial, sBslash);
c: AnsiChar;
D, P: PAnsiChar;
begin
result := [pcInvalidCommand];
if argv <> nil then
argv[0] := nil;
if argc <> nil then
argc^ := 0;
if cmd = '' then
exit;
if argv = nil then
D := nil
else begin
if temp = nil then
exit;
SetLength(temp^, length(cmd));
D := pointer(temp^);
end;
state := [];
n := 0;
P := pointer(cmd);
repeat
c := P^;
if D <> nil then
D^ := c;
inc(P);
case c of
#0: begin
if sInSQ in state then
include(result, pcUnbalancedSingleQuote);
if sInDQ in state then
include(result, pcUnbalancedDoubleQuote);
exclude(result, pcInvalidCommand);
if argv <> nil then
argv[n] := nil;
if argc <> nil then
argc^ := n;
exit;
end;
#1 .. ' ': begin
if state = [sInArg] then begin
state := [];
if D <> nil then begin
D^ := #0;
inc(D);
end;
continue;
end;
if state * [sInSQ, sInDQ] = [] then
continue;
end;
'\':
if posix and (state * [sInSQ, sBslash] = []) then
if sInDQ in state then begin
case P^ of
'"', '\', '$', '`': begin
include(state, sBslash);
continue;
end;
end;
end else if P^ = #0 then begin
include(result, pcHasEndingBackSlash);
exit;
end else begin
if D <> nil then
D^ := P^;
inc(P);
end;
'^':
if not posix and (state * [sInSQ, sInDQ, sBslash] = []) then
if PWord(P)^ = $0a0d then begin
inc(P, 2);
continue;
end
else if P^ = #0 then begin
include(result, pcHasEndingBackSlash);
exit;
end else begin
if D <> nil then
D^ := P^;
inc(P);
end;
'''':
if posix and not(sInDQ in state) then
if sInSQ in state then begin
exclude(state, sInSQ);
continue;
end else if state = [] then begin
if argv <> nil then begin
argv[n] := D;
inc(n);
if n = high(argv^) then
exit;
end;
state := [sInSQ, sInArg];
continue;
end else if state = [sInArg] then begin
state := [sInSQ, sInArg];
continue;
end;
'"':
if not(sInSQ in state) then
if sInDQ in state then begin
exclude(state, sInDQ);
continue;
end else if state = [] then begin
if argv <> nil then begin
argv[n] := D;
inc(n);
if n = high(argv^) then
exit;
end;
state := [sInDQ, sInArg];
continue;
end else if state = [sInArg] then begin
state := [sInDQ, sInArg];
continue;
end;
'|', '<', '>':
if state * [sInSQ, sInDQ] = [] then
include(result, pcHasRedirection);
'&', ';':
if posix and (state * [sInSQ, sInDQ] = []) then begin
include(state, sSpecial);
include(result, pcHasJobControl);
end;
'`':
if posix and (state * [sInSQ, sBslash] = []) then
include(result, pcHasSubCommand);
'(', ')':
if posix and (state * [sInSQ, sInDQ] = []) then
include(result, pcHasParenthesis);
'$':
if posix and (state * [sInSQ, sBslash] = []) then
if p^ = '(' then
include(result, pcHasSubCommand)
else
include(result, pcHasShellVariable);
'*', '?':
if posix and (state * [sInSQ, sInDQ] = []) then
include(result, pcHasWildcard);
end;
exclude(state, sBslash);
if state = [] then begin
if argv <> nil then begin
argv[n] := D;
inc(n);
if n = high(argv^) then
exit;
end;
state := [sInArg];
end;
if D <> nil then
inc(D);
until false;
end;
{ *** cross-plaform high-level services }
{ TSynDaemonSettings }
constructor TSynDaemonSettings.Create;
begin
inherited Create;
fLog := LOG_STACKTRACE + [sllNewRun];
fLogRotateFileCount := 2;
fServiceName := UTF8ToString(ExeVersion.ProgramName);
fServiceDisplayName := fServiceName;
end;
function TSynDaemonSettings.ServiceDescription: string;
var
versionnumber: string;
begin
result := ServiceDisplayName;
with ExeVersion.Version do begin
versionnumber := DetailedOrVoid;
if versionnumber <> '' then
result := result + ' ' + versionnumber;
if CompanyName <> '' then
result := FormatString('% - (c)% %', [result, BuildYear, CompanyName]);
end;
end;
procedure TSynDaemonSettings.SetLog(aLogClass: TSynLogClass);
begin
if (self <> nil) and (Log <> []) and (aLogClass <> nil) then
with aLogClass.Family do begin
DestinationPath := LogPath;
PerThreadLog := ptIdentifiedInOnFile; // ease multi-threaded server debug
RotateFileCount := LogRotateFileCount;
if RotateFileCount > 0 then begin
RotateFileSizeKB := 20 * 1024; // rotate by 20 MB logs
FileExistsAction := acAppend; // as expected in rotation mode
end
else
HighResolutionTimestamp := true;
Level := Log;
fLogClass := aLogClass;
end;
end;
{ TSynDaemon }
constructor TSynDaemon.Create(aSettingsClass: TSynDaemonSettingsClass;
const aWorkFolder, aSettingsFolder, aLogFolder, aSettingsExt, aSettingsName: TFileName);
var
fn: TFileName;
begin
inherited Create;
if aWorkFolder = '' then
fWorkFolderName := ExeVersion.ProgramFilePath
else
fWorkFolderName := EnsureDirectoryExists(aWorkFolder, true);
if aSettingsClass = nil then
aSettingsClass := TSynDaemonSettings;
fSettings := aSettingsClass.Create;
fn := aSettingsFolder;
if fn = '' then
fn := {$ifdef MSWINDOWS}fWorkFolderName{$else}'/etc/'{$endif};
fn := EnsureDirectoryExists(fn);
if aSettingsName = '' then
fn := fn + UTF8ToString(ExeVersion.ProgramName)
else
fn := fn + aSettingsName;
fSettings.LoadFromFile(fn + aSettingsExt);
if fSettings.LogPath = '' then
if aLogFolder = '' then
fSettings.LogPath := {$ifdef MSWINDOWS}fWorkFolderName{$else}GetSystemPath(spLog){$endif}
else
fSettings.LogPath := EnsureDirectoryExists(aLogFolder);
end;
destructor TSynDaemon.Destroy;
begin
if fSettings <> nil then
fSettings.SaveIfNeeded;
Stop;
inherited Destroy;
FreeAndNil(fSettings);
end;
{$ifdef MSWINDOWS}
procedure TSynDaemon.DoStart(Sender: TService);
begin
Start;
end;
procedure TSynDaemon.DoStop(Sender: TService);
begin
Stop;
end;
{$endif MSWINDOWS}
function TSynDaemon.CustomCommandLineSyntax: string;
begin
result := '';
end;
{$I-}
type
TExecuteCommandLineCmd = (
cNone, cVersion, cVerbose, cStart, cStop, cState, cSilentKill,
cHelp, cInstall, cRun, cFork, cUninstall, cConsole, cKill);
procedure TSynDaemon.CommandLine(aAutoStart: boolean);
const CMD_CHR: array[cHelp .. cKill] of AnsiChar = ('H', 'I', 'R', 'F', 'U', 'C', 'K');
var
cmd, c: TExecuteCommandLineCmd;
p: PUTF8Char;
ch: AnsiChar;
param: RawUTF8;
exe: RawByteString;
log: TSynLog;
{$ifdef MSWINDOWS}
service: TServiceSingle;
ctrl: TServiceController;
{$endif MSWINDOWS}
procedure WriteCopyright;
var
msg, name, copyright: string;
i: integer;
begin
msg := fSettings.ServiceDescription;
i := Pos(' - ', msg);
if i = 0 then
name := msg
else begin
name := copy(msg, 1, i - 1);
copyright := copy(msg, i + 3, 1000);
end;
TextColor(ccLightGreen);
writeln(' ', name);
writeln(StringOfChar('-', length(name) + 2));
TextColor(ccGreen);
if copyright <> '' then
writeln(' ', copyright);
writeln;
TextColor(ccLightGray);
end;
procedure Syntax;
var
spaces, custom: string;
begin
WriteCopyright;
writeln('Try with one of the switches:');
spaces := StringOfChar(' ', length(ExeVersion.ProgramName) + 4);
{$ifdef MSWINDOWS}
writeln(' ', ExeVersion.ProgramName, ' /console -c /verbose /help -h /version');
writeln(spaces, '/install /uninstall /start /stop /state');
{$else}
writeln(' ./', ExeVersion.ProgramName, ' --console -c --verbose --help -h --version');
writeln(spaces, '--run -r --fork -f --kill -k');
{$endif MSWINDOWS}
custom := CustomCommandLineSyntax;
if custom <> '' then
writeln(spaces, custom);
end;
function cmdText: RawUTF8;
begin
result := GetEnumNameTrimed(TypeInfo(TExecuteCommandLineCmd), cmd);
end;
procedure Show(Success: Boolean);
var
msg: RawUTF8;
error: integer;
begin
WriteCopyright;
if Success then begin
msg := 'Successfully executed';
TextColor(ccWhite);
end
else begin
error := GetLastError;
msg := FormatUTF8('Error % [%] occured with',
[error, StringToUTF8(SysErrorMessage(error))]);
TextColor(ccLightRed);
ExitCode := 1; // notify error to caller batch
end;
msg := FormatUTF8('% [%] (%) on Service ''%''',
[msg, param, cmdText, fSettings.ServiceName]);
writeln(UTF8ToConsole(msg));
TextColor(ccLightGray);
log.Log(sllDebug, 'CommandLine: %', [msg], self);
end;
begin
if (self = nil) or (fSettings = nil) then
exit;
log := nil;
param := trim(StringToUTF8(paramstr(1)));
cmd := cNone;
if (param <> '') and (param[1] in ['/', '-']) then begin
p := @param[2];
if p^ = '-' then
inc(p); // allow e.g. --fork switch (idem to /f -f /fork -fork)
ch := NormToUpper[p^];
for c := low(CMD_CHR) to high(CMD_CHR) do
if CMD_CHR[c] = ch then begin
cmd := c;
break;
end;
if cmd = cNone then
byte(cmd) := ord(cVersion) +
IdemPCharArray(p, ['VERS', 'VERB', 'START', 'STOP', 'STAT', 'SILENTK']);
end;
try
case cmd of
cHelp:
Syntax;
cVersion: begin
WriteCopyright;
exe := StringFromFile(ExeVersion.ProgramFileName);
writeln(' ', fSettings.ServiceName,
#13#10' Size: ', length(exe), ' bytes (', KB(exe), ')' +
#13#10' Build date: ', ExeVersion.Version.BuildDateTimeString,
#13#10' MD5: ', MD5(exe),
#13#10' SHA256: ', SHA256(exe));
if ExeVersion.Version.Version32 <> 0 then
writeln(' Version: ', ExeVersion.Version.Detailed);
end;
cConsole, cVerbose: begin
WriteCopyright;
writeln('Launched in ', cmdText, ' mode'#10);
TextColor(ccLightGray);
log := fSettings.fLogClass.Add;
if (cmd = cVerbose) and (log <> nil) then begin
log.Family.Level := LOG_VERBOSE;
log.Family.EchoToConsole := LOG_VERBOSE;
end;
try
log.Log(sllNewRun, 'Start % /% %', [fSettings.ServiceName,cmdText,
ExeVersion.Version.DetailedOrVoid], self);
fConsoleMode := true;
Start;
writeln('Press [Enter] to quit');
ioresult;
readln;
writeln('Shutting down server');
finally
ioresult;
log.Log(sllNewRun, 'Stop /%', [cmdText], self);
Stop;
end;
end;
{$ifdef MSWINDOWS} // implement the daemon as a Windows Service
else if fSettings.ServiceName = '' then
if cmd = cNone then
Syntax
else begin
TextColor(ccLightRed);
writeln('No ServiceName specified - please fix the settings');
end
else
case cmd of
cNone:
if param = '' then begin // executed as a background service
service := TServiceSingle.Create(
fSettings.ServiceName, fSettings.ServiceDisplayName);
try
service.OnStart := DoStart;
service.OnStop := DoStop;
service.OnShutdown := DoStop; // sometimes, is called without Stop
if ServicesRun then // blocking until service shutdown
Show(true)
else if GetLastError = 1063 then
Syntax
else
Show(false);
finally
service.Free;
end;
end
else
Syntax;
cInstall:
with fSettings do
Show(TServiceController.Install(ServiceName, ServiceDisplayName,
ServiceDescription, aAutoStart, '', ServiceDependencies) <> ssNotInstalled);
cStart, cStop, cUninstall, cState: begin
ctrl := TServiceController.CreateOpenService('', '', fSettings.ServiceName);
try
case cmd of
cStart:
Show(ctrl.Start([]));
cStop:
Show(ctrl.Stop);
cUninstall:
begin
ctrl.Stop;
Show(ctrl.Delete);
end;
cState:
writeln(fSettings.ServiceName, ' State=', ServiceStateText(ctrl.State));
end;
finally
ctrl.Free;
end;
end;
else
Syntax;
end;
{$else}
cRun, cFork:
RunUntilSigTerminated(self,(cmd=cFork),Start,Stop,fSettings.fLogClass.Add,fSettings.ServiceName);
cKill, cSilentKill:
if RunUntilSigTerminatedForKill then begin
if cmd <> cSilentKill then
writeln('Forked process ', ExeVersion.ProgramName, ' killed successfully');
end
else
raise EServiceException.Create('No forked process found to be killed');
else
Syntax;
{$endif MSWINDOWS}
end;
except
on E: Exception do begin
if cmd <> cSilentKill then
ConsoleShowFatalException(E, true);
ExitCode := 1; // indicates error
end;
end;
if cmd <> cSilentKill then
TextColor(ccLightGray);
ioresult;
end;
{$I+}
{$ifdef MSWINDOWS}
const
SE_CREATE_TOKEN_NAME = 'SeCreateTokenPrivilege';
SE_ASSIGNPRIMARYTOKEN_NAME = 'SeAssignPrimaryTokenPrivilege';
SE_LOCK_MEMORY_NAME = 'SeLockMemoryPrivilege';
SE_INCREASE_QUOTA_NAME = 'SeIncreaseQuotaPrivilege';
SE_UNSOLICITED_INPUT_NAME = 'SeUnsolicitedInputPrivilege';
SE_MACHINE_ACCOUNT_NAME = 'SeMachineAccountPrivilege';
SE_TCB_NAME = 'SeTcbPrivilege';
SE_SECURITY_NAME = 'SeSecurityPrivilege';
SE_TAKE_OWNERSHIP_NAME = 'SeTakeOwnershipPrivilege';
SE_LOAD_DRIVER_NAME = 'SeLoadDriverPrivilege';
SE_SYSTEM_PROFILE_NAME = 'SeSystemProfilePrivilege';
SE_SYSTEMTIME_NAME = 'SeSystemtimePrivilege';
SE_PROF_SINGLE_PROCESS_NAME = 'SeProfileSingleProcessPrivilege';
SE_INC_BASE_PRIORITY_NAME = 'SeIncreaseBasePriorityPrivilege';
SE_CREATE_PAGEFILE_NAME = 'SeCreatePagefilePrivilege';
SE_CREATE_PERMANENT_NAME = 'SeCreatePermanentPrivilege';
SE_BACKUP_NAME = 'SeBackupPrivilege';
SE_RESTORE_NAME = 'SeRestorePrivilege';
SE_SHUTDOWN_NAME = 'SeShutdownPrivilege';
SE_DEBUG_NAME = 'SeDebugPrivilege';
SE_AUDIT_NAME = 'SeAuditPrivilege';
SE_SYSTEM_ENVIRONMENT_NAME = 'SeSystemEnvironmentPrivilege';
SE_CHANGE_NOTIFY_NAME = 'SeChangeNotifyPrivilege';
SE_REMOTE_SHUTDOWN_NAME = 'SeRemoteShutdownPrivilege';
SE_UNDOCK_NAME = 'SeUndockPrivilege';
SE_SYNC_AGENT_NAME = 'SeSyncAgentPrivilege';
SE_ENABLE_DELEGATION_NAME = 'SeEnableDelegationPrivilege';
SE_MANAGE_VOLUME_NAME = 'SeManageVolumePrivilege';
SE_IMPERSONATE_NAME = 'SeImpersonatePrivilege';
SE_CREATE_GLOBAL_NAME = 'SeCreateGlobalPrivilege';
SE_TRUSTED_CREDMAN_ACCESS_NAME = 'SeTrustedCredManAccessPrivilege';
SE_RELABEL_NAME = 'SeRelabelPrivilege';
SE_INC_WORKING_SET_NAME = 'SeIncreaseWorkingSetPrivilege';
SE_TIME_ZONE_NAME = 'SeTimeZonePrivilege';
SE_CREATE_SYMBOLIC_LINK_NAME = 'SeCreateSymbolicLinkPrivilege';
MAX_SE_NAME_LENGTH = 31;
WinSystemPrivilegeToSE_NAME: array[TWinSystemPrivilege] of string = (
SE_CREATE_TOKEN_NAME,
SE_ASSIGNPRIMARYTOKEN_NAME,
SE_LOCK_MEMORY_NAME,
SE_INCREASE_QUOTA_NAME,
SE_UNSOLICITED_INPUT_NAME,
SE_MACHINE_ACCOUNT_NAME,
SE_TCB_NAME,
SE_SECURITY_NAME,
SE_TAKE_OWNERSHIP_NAME,
SE_LOAD_DRIVER_NAME,
SE_SYSTEM_PROFILE_NAME,
SE_SYSTEMTIME_NAME,
SE_PROF_SINGLE_PROCESS_NAME,
SE_INC_BASE_PRIORITY_NAME,
SE_CREATE_PAGEFILE_NAME,
SE_CREATE_PERMANENT_NAME,
SE_BACKUP_NAME,
SE_RESTORE_NAME,
SE_SHUTDOWN_NAME,
SE_DEBUG_NAME,
SE_AUDIT_NAME,
SE_SYSTEM_ENVIRONMENT_NAME,
SE_CHANGE_NOTIFY_NAME,
SE_REMOTE_SHUTDOWN_NAME,
SE_UNDOCK_NAME,
SE_SYNC_AGENT_NAME,
SE_ENABLE_DELEGATION_NAME,
SE_MANAGE_VOLUME_NAME,
SE_IMPERSONATE_NAME,
SE_CREATE_GLOBAL_NAME,
SE_TRUSTED_CREDMAN_ACCESS_NAME,
SE_RELABEL_NAME,
SE_INC_WORKING_SET_NAME,
SE_TIME_ZONE_NAME,
SE_CREATE_SYMBOLIC_LINK_NAME
);
type
PTOKEN_PRIVILEGES = ^TOKEN_PRIVILEGES;
TOKEN_PRIVILEGES = packed record
PrivilegeCount : DWORD;
Privileges : array[0..0] of LUID_AND_ATTRIBUTES;
end;
function OpenProcessToken(ProcessHandle: THandle; DesiredAccess: DWORD;
var TokenHandle: THandle): BOOL; stdcall; external advapi32 name 'OpenProcessToken';
function LookupPrivilegeValue(lpSystemName, lpName: PChar;
var lpLuid: TLargeInteger): BOOL; stdcall; external advapi32
name {$ifdef UNICODE}'LookupPrivilegeValueW'{$else}'LookupPrivilegeValueA'{$endif};
function LookupPrivilegeNameA(lpSystemName: LPCSTR; var lpLuid: TLargeInteger;
lpName: LPCSTR; var cbName: DWORD): BOOL; stdcall; external advapi32 name 'LookupPrivilegeNameA';
function AdjustTokenPrivileges(TokenHandle: THandle; DisableAllPrivileges: BOOL;
const NewState: TOKEN_PRIVILEGES; BufferLength: DWORD;
PreviousState: PTokenPrivileges; ReturnLength: PDWORD): BOOL; stdcall; external advapi32
name 'AdjustTokenPrivileges';
{ TSynWindowsPrivileges }
procedure TSynWindowsPrivileges.Init(aTokenPrivilege: TPrivilegeTokenType);
var
access: Cardinal;
begin
Token := 0;
fAvailable := [];
fEnabled := [];
fDefEnabled := [];
access := TOKEN_QUERY or TOKEN_ADJUST_PRIVILEGES;
if aTokenPrivilege = pttProcess then begin
if not OpenProcessToken(GetCurrentProcess, access, Token) then
raise ESynException.Create('TSynWindowsPrivileges cannot open process token');
end
else if not OpenThreadToken(GetCurrentThread, access, false, Token) then
if GetLastError = ERROR_NO_TOKEN then begin
if not ImpersonateSelf(SecurityImpersonation) or
not OpenThreadToken(GetCurrentThread, access, false, Token) then
raise ESynException.Create('TSynWindowsPrivileges cannot open thread token');
end else
raise ESynException.Create('TSynWindowsPrivileges cannot open thread token');
LoadPrivileges;
end;
procedure TSynWindowsPrivileges.Done(aRestoreInitiallyEnabled: boolean = true);
var
i: TWinSystemPrivilege;
new: TWinSystemPrivileges;
begin
if aRestoreInitiallyEnabled then begin
new := fEnabled-fDefEnabled;
for i := low(TWinSystemPrivilege) to high(TWinSystemPrivilege) do
if i in new then
Disable(i);
end;
CloseHandle(Token);
end;
function TSynWindowsPrivileges.Enable(aPrivilege: TWinSystemPrivilege): boolean;
begin
result := aPrivilege in fEnabled;
if result or not (aPrivilege in fAvailable) or not SetPrivilege(Pointer(WinSystemPrivilegeToSE_NAME[aPrivilege]), true) then
exit;
Include(fEnabled, aPrivilege);
result := true;
end;
function TSynWindowsPrivileges.Disable(aPrivilege: TWinSystemPrivilege
): boolean;
begin
result := not (aPrivilege in fEnabled);
if result or not (aPrivilege in fAvailable) or not SetPrivilege(Pointer(WinSystemPrivilegeToSE_NAME[aPrivilege]), false) then
exit;
Exclude(fEnabled, aPrivilege);
result := true;
end;
procedure TSynWindowsPrivileges.LoadPrivileges;
const
UPCASE_SE_NAMES: array[TWinSystemPrivilege] of PAnsiChar = (
'SECREATETOKENPRIVILEGE','SEASSIGNPRIMARYTOKENPRIVILEGE','SELOCKMEMORYPRIVILEGE',
'SEINCREASEQUOTAPRIVILEGE','SEUNSOLICITEDINPUTPRIVILEGE','SEMACHINEACCOUNTPRIVILEGE',
'SETCBPRIVILEGE','SESECURITYPRIVILEGE','SETAKEOWNERSHIPPRIVILEGE',
'SELOADDRIVERPRIVILEGE','SESYSTEMPROFILEPRIVILEGE','SESYSTEMTIMEPRIVILEGE',
'SEPROFILESINGLEPROCESSPRIVILEGE','SEINCREASEBASEPRIORITYPRIVILEGE',
'SECREATEPAGEFILEPRIVILEGE','SECREATEPERMANENTPRIVILEGE','SEBACKUPPRIVILEGE',
'SERESTOREPRIVILEGE','SESHUTDOWNPRIVILEGE','SEDEBUGPRIVILEGE','SEAUDITPRIVILEGE',
'SESYSTEMENVIRONMENTPRIVILEGE','SECHANGENOTIFYPRIVILEGE','SEREMOTESHUTDOWNPRIVILEGE',
'SEUNDOCKPRIVILEGE','SESYNCAGENTPRIVILEGE','SEENABLEDELEGATIONPRIVILEGE',
'SEMANAGEVOLUMEPRIVILEGE','SEIMPERSONATEPRIVILEGE','SECREATEGLOBALPRIVILEGE',
'SETRUSTEDCREDMANACCESSPRIVILEGE','SERELABELPRIVILEGE','SEINCREASEWORKINGSETPRIVILEGE',
'SETIMEZONEPRIVILEGE','SECREATESYMBOLICLINKPRIVILEGE');
var
buf: TSynTempBuffer;
tp: PTOKEN_PRIVILEGES;
len: Cardinal;
i: integer;
name: AnsiString;
enumval: integer;
begin
if Token = 0 then
raise ESynException.Create('TSynWindowsPrivileges: invalid privileges token');
buf.Init;
try
len := 0;
if not GetTokenInformation(Token, TokenPrivileges, buf.buf, buf.len, len) then
if GetLastError <> ERROR_INSUFFICIENT_BUFFER then
raise ESynException.Create('TSynWindowsPrivileges cannot get token information')
else begin
buf.Done;
buf.Init(len);
if not GetTokenInformation(Token, TokenPrivileges, buf.buf, buf.len, len) then
raise ESynException.Create('TSynWindowsPrivileges cannot get token information')
end;
tp := buf.buf;
SetLength(name, MAX_SE_NAME_LENGTH);
for i := 0 to tp.PrivilegeCount-1 do
begin
len := Length(name);
if not LookupPrivilegeNameA(nil,tp.Privileges[i].Luid,@name[1],len) then
if GetLastError <> ERROR_INSUFFICIENT_BUFFER then
raise ESynException.CreateUTF8('TSynWindowsPrivileges cannot lookup privilege name for Luid (%)',
[PInt64(@tp.Privileges[i].Luid)^]) // PInt64() to avoid URW699 on Delphi 6
else begin
SetLength(name, len);
if not LookupPrivilegeNameA(nil,tp.Privileges[i].Luid,@name[1],len) then
raise ESynException.CreateUTF8('TSynWindowsPrivileges cannot lookup privilege name for Luid (%)',
[PInt64(@tp.Privileges[i].Luid)^])
end;
enumval := IdemPCharArray(@name[1], UPCASE_SE_NAMES);
if (enumval >= ord(low(TWinSystemPrivilege))) and (enumval <= ord(high(TWinSystemPrivilege))) then begin
Include(fAvailable, TWinSystemPrivilege(enumval));
if tp.Privileges[i].Attributes and SE_PRIVILEGE_ENABLED <> 0 then
Include(fDefEnabled, TWinSystemPrivilege(enumval));
end;
end;
fEnabled := fDefEnabled;
finally
buf.Done;
end;
end;
function TSynWindowsPrivileges.SetPrivilege(aPrivilege: Pointer;
aEnablePrivilege: boolean): boolean;
var
tp: TOKEN_PRIVILEGES;
id: TLargeInteger;
tpprev: TOKEN_PRIVILEGES;
cbprev: DWORD;
begin
result := false;
cbprev := sizeof(TOKEN_PRIVILEGES);
if not LookupPrivilegeValue(nil, aPrivilege, id) then
exit;
tp.PrivilegeCount := 1;
tp.Privileges[0].Luid := PInt64(@id)^;
tp.Privileges[0].Attributes := 0;
AdjustTokenPrivileges(Token, false, tp, sizeof(TOKEN_PRIVILEGES), @tpprev, @cbprev);
if GetLastError <> ERROR_SUCCESS then
exit;
tpprev.PrivilegeCount := 1;
tpprev.Privileges[0].Luid := PInt64(@id)^;
with tpprev.Privileges[0] do
if aEnablePrivilege then
Attributes := Attributes or SE_PRIVILEGE_ENABLED
else
Attributes := Attributes xor (SE_PRIVILEGE_ENABLED and Attributes);
AdjustTokenPrivileges(Token, false, tpprev, cbprev, nil, nil);
if GetLastError <> ERROR_SUCCESS then
exit;
result := true;
end;
const
ntdll = 'NTDLL.DLL';
type
_PPS_POST_PROCESS_INIT_ROUTINE = ULONG;
PUNICODE_STRING = ^UNICODE_STRING;
UNICODE_STRING = packed record
Length: word;
MaximumLength: word;
{$ifdef CPUX64}
_align: array[0..3] of byte;
{$endif}
Buffer: PWideChar;
end;
PMS_PEB_LDR_DATA = ^MS_PEB_LDR_DATA;
MS_PEB_LDR_DATA = packed record
Reserved1: array[0..7] of BYTE;
Reserved2: array[0..2] of pointer;
InMemoryOrderModuleList: LIST_ENTRY;
end;
PMS_RTL_USER_PROCESS_PARAMETERS = ^MS_RTL_USER_PROCESS_PARAMETERS;
MS_RTL_USER_PROCESS_PARAMETERS = packed record
Reserved1: array[0..15] of BYTE;
Reserved2: array[0..9] of pointer;
ImagePathName: UNICODE_STRING;
CommandLine: UNICODE_STRING ;
end;
PMS_PEB = ^MS_PEB;
MS_PEB = packed record
Reserved1: array[0..1] of BYTE;
BeingDebugged: BYTE;
Reserved2: array[0..0] of BYTE;
{$ifdef CPUX64}
_align1: array[0..3] of byte;
{$endif}
Reserved3: array[0..1] of pointer;
Ldr: PMS_PEB_LDR_DATA;
ProcessParameters: PMS_RTL_USER_PROCESS_PARAMETERS;
Reserved4: array[0..103] of BYTE;
Reserved5: array[0..51] of pointer;
PostProcessInitRoutine: _PPS_POST_PROCESS_INIT_ROUTINE; // for sure not pointer, otherwise SessionId is broken
Reserved6: array[0..127] of BYTE;
{$ifdef CPUX64}
_align2: array[0..3] of byte;
{$endif}
Reserved7: array[0..0] of pointer;
SessionId: ULONG;
{$ifdef CPUX64}
_align3: array[0..3] of byte;
{$endif}
end;
PMS_PROCESS_BASIC_INFORMATION = ^MS_PROCESS_BASIC_INFORMATION;
MS_PROCESS_BASIC_INFORMATION = packed record
ExitStatus: Longint;
{$ifdef CPUX64}
_align1: array[0..3] of byte;
{$endif}
PebBaseAddress: PMS_PEB;
AffinityMask: PtrUInt;
BasePriority: Longint;
{$ifdef CPUX64}
_align2: array[0..3] of byte;
{$endif}
UniqueProcessId: PtrUInt;
InheritedFromUniqueProcessId: PtrUInt;
end;
{$Z4}
PROCESSINFOCLASS = (ProcessBasicInformation = 0, ProcessDebugPort = 7,
ProcessWow64Information = 26, ProcessImageFileName = 27,
ProcessBreakOnTermination = 29, ProcessSubsystemInformation = 75);
{$Z1}
NTSTATUS = LongInt;
function NtQueryInformationProcess(ProcessHandle: THandle;
ProcessInformationClass: PROCESSINFOCLASS; ProcessInformation: pointer;
ProcessInformationLength: ULONG; ReturnLength: PULONG): NTSTATUS; stdcall;
external ntdll name 'NtQueryInformationProcess';
function ReadProcessMemory(hProcess: THandle; const lpBaseAddress: Pointer; lpBuffer: Pointer;
nSize: PTRUINT; var lpNumberOfBytesRead: PTRUINT): BOOL; external 'kernel32' name 'ReadProcessMemory';
function InternalGetProcessInfo(aPID: DWORD; out aInfo: TWinProcessInfo): boolean;
var
bytesread: PtrUInt;
sizeneeded: DWORD;
pbi: PMS_PROCESS_BASIC_INFORMATION;
peb: MS_PEB;
peb_upp: MS_RTL_USER_PROCESS_PARAMETERS;
prochandle: THandle;
buf: TSynTempBuffer;
begin
result := false;
with aInfo do begin
AvailableInfo := [];
PID := 0;
ParentPID := 0;
SessionID := 0;
PEBBaseAddress := nil;
AffinityMask := 0;
BasePriority := 0;
ExitStatus := 0;
BeingDebugged := 0;
ImagePath := '';
CommandLine := '';
end;
if APID = 0 then
exit;
prochandle := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, FALSE, aPid);
if prochandle = INVALID_HANDLE_VALUE then
exit;
Include(aInfo.AvailableInfo, wpaiPID);
aInfo.PID := aPid;
buf.InitZero(SizeOf(MS_PROCESS_BASIC_INFORMATION));
try
sizeneeded := 0;
if NtQueryInformationProcess(prochandle, ProcessBasicInformation, buf.buf, buf.len, @sizeneeded) < 0 then
exit;
if buf.len < integer(sizeneeded) then begin
buf.Done;
buf.InitZero(sizeneeded);
if NtQueryInformationProcess(prochandle, ProcessBasicInformation, buf.buf, buf.len, @sizeneeded) < 0 then
exit;
end;
Include(aInfo.AvailableInfo, wpaiBasic);
pbi := buf.buf;
with aInfo do begin
PID := pbi^.UniqueProcessId;
ParentPID := pbi^.InheritedFromUniqueProcessId;
BasePriority := pbi^.BasePriority;
ExitStatus := pbi^.ExitStatus;
PEBBaseAddress := pbi^.PebBaseAddress;
AffinityMask := pbi^.AffinityMask;
end;
// read PEB (Process Environment Block)
if not Assigned(pbi.PebBaseAddress) then
exit;
bytesread := 0;
FillCharFast(peb, sizeof(MS_PEB), 0);
if not ReadProcessMemory(prochandle, pbi.PebBaseAddress, @peb, sizeof(MS_PEB), bytesread) then
exit;
Include(aInfo.AvailableInfo, wpaiPEB);
aInfo.SessionID := peb.SessionId;
aInfo.BeingDebugged := peb.BeingDebugged;
FillCharFast(peb_upp, sizeof(MS_RTL_USER_PROCESS_PARAMETERS), 0);
bytesread := 0;
if not ReadProcessMemory(prochandle, peb.ProcessParameters, @peb_upp, sizeof(MS_RTL_USER_PROCESS_PARAMETERS), bytesread) then
exit;
// command line info
if peb_upp.CommandLine.Length > 0 then begin
SetLength(aInfo.CommandLine, peb_upp.CommandLine.Length div 2);
bytesread := 0;
if not ReadProcessMemory(prochandle, peb_upp.CommandLine.Buffer, @aInfo.CommandLine[1], peb_upp.CommandLine.Length, bytesread) then
exit;
Include(aInfo.AvailableInfo, wpaiCommandLine);
end;
// image info
if(peb_upp.ImagePathName.Length > 0) then begin
SetLength(aInfo.ImagePath, peb_upp.ImagePathName.Length div 2);
bytesread := 0;
if not ReadProcessMemory(prochandle, peb_upp.ImagePathName.Buffer, @aInfo.ImagePath[1], peb_upp.ImagePathName.Length, bytesread) then
exit;
Include(aInfo.AvailableInfo, wpaiImagePath);
end;
result := true;
finally
CloseHandle(prochandle);
buf.Done;
end;
end;
procedure GetProcessInfo(aPid: Cardinal; out aInfo: TWinProcessInfo);
var
privileges: TSynWindowsPrivileges;
begin
privileges.Init(pttThread);
try
privileges.Enable(wspDebug);
InternalGetProcessInfo(aPid, aInfo);
finally
privileges.Done;
end;
end;
procedure GetProcessInfo(const aPidList: TCardinalDynArray; out aInfo: TWinProcessInfoDynArray);
var
privileges: TSynWindowsPrivileges;
i: integer;
begin
SetLength(aInfo, Length(aPidList));
privileges.Init(pttThread);
try
privileges.Enable(wspDebug);
for i := 0 to High(aPidList) do
InternalGetProcessInfo(aPidList[i], aInfo[i]);
finally
privileges.Done;
end;
end;
{$endif MSWINDOWS}
end.