Delphi、WinSvc.StartService 个参数未成功传递给服务应用

Delphi, WinSvc.StartService arguments not successfully passed Service app

我正在 Delphi 10.2 pro 中编写一些服务应用程序,我想添加一个启动时间可控参数以强制服务应用程序进入启动等待循环足够长的时间请允许我点击进入 "Run\Attach to Process" window(在应用程序开始初始化代码之前)。

为此,我想将睡眠循环放入 TService.OnCreate 处理程序中,只有在 Winapi.WinSvc.StartService 传递指定所需延迟长度(以秒为单位)的参数时才会激活该处理程序。

我遇到的问题:放置到 lpServiceArgVectors(StartService 第三个参数)中的值在服务中的 ParamStr(1) 函数中不可用。我读到过传递此参数的 VAR 参数存在问题,但我认为我已经在我的测试应用程序中解决了这个问题(StartService 始终 returns TRUE)。

我只是无法在服务中看到参数,我需要一些帮助来绕过这堵墙。

我整理了一个简短的(大概)独立示例。此示例的症结在于 TMainWindow.StartService(lpServiceArgVectors 在此组装并传递)与 TSimpleServiceDelayTest 中的 ServiceCreate -> CheckStartUpDelayParam 过程之间的交互。该服务记录到一个显示一些诊断日志记录的文本文件;日志按降序排列,以便将最新数据插入顶部。

有 3 个不同的菜单项可以调用 StartService(以改变调用参数)请注意,无论选择哪个 Start Service 菜单选项,ParamStr(1) 的记录值始终是:

//------------- SimpleHeartbeatService.dpr --------------

program SimpleHeartbeatService;

uses
  Vcl.SvcMgr,
  ServiceUnit in 'ServiceUnit.pas' {SimpleServiceDelayTest: TService};

{$R *.RES}

begin
  if not Application.DelayInitialize or Application.Installing then
    Application.Initialize;
  Application.CreateForm(TSimpleServiceDelayTest, SimpleServiceDelayTest);
  Application.Run;
end.

//----------------- ServiceUnit.pas ------------------ --------

unit ServiceUnit;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.SvcMgr, Vcl.Dialogs;

type
  TSimpleServiceDelayTest = class(TService)
    procedure ServiceExecute(Sender: TService);
    procedure ServiceCreate(Sender: TObject);
    procedure ServiceShutdown(Sender: TService);
    procedure ServiceStart(Sender: TService; var Started: Boolean);
    procedure ServiceStop(Sender: TService; var Stopped: Boolean);
    procedure ServiceDestroy(Sender: TObject);
  private
    PrevHeartbeatStr: String;
    ServiceLog: TStringList;
    Procedure CheckStartUpDelayParam;
    Procedure DriveHeartbeatLogging;
    Procedure Log(Const Msg: String);
    Function LogFileName: String;
  public
    function GetServiceController: TServiceController; override;
    { Public declarations }
  end;

var
  SimpleServiceDelayTest: TSimpleServiceDelayTest;

implementation

{$R *.dfm}

// =============================================================================

procedure ServiceController(CtrlCode: DWord); stdcall;
begin
  SimpleServiceDelayTest.Controller(CtrlCode);
end;

// =============================================================================

Procedure TSimpleServiceDelayTest.CheckStartUpDelayParam;
Const
  OneSec = 1 / 86400;
Var
  DelaySecs: Integer;
  TZero: TDateTime;
Begin
  Log('CheckStartUpDelayParam');
  Log('ParamStr(0)=' + ParamStr(0));
  Log('ParamStr(1)=' + ParamStr(1));
  // ********** THIS IS THE GOAL OF THIS WHOLE ENDEAVOR: **********
  // I want to pause the initialization long enough to attach the
  // Delphi  debugger (via Run | Attach to Process...)
  // I want to pass a command line parameter via the NumArgs/pArgVectors args
  // from: Winapi.WinSvc.StartService(Svc, NumArgs, pArgVectors)
  // So far, I have not been able to pass arguments this way.
  DelaySecs := StrToIntDef(ParamStr(1), 0);
  If DelaySecs > 0 Then
  Begin
    TZero := Now;
    While Now - TZero > DelaySecs * OneSec do
      Sleep(250);
  End;
End;

// =============================================================================

Procedure TSimpleServiceDelayTest.DriveHeartbeatLogging;
Var
  HeartbeatStr: String;
begin
  HeartbeatStr := FormatDateTime('hh:mm', Now);
  If HeartbeatStr <> PrevHeartbeatStr Then
    Try
      Log('HeartbeatStr = ' + HeartbeatStr);
    Finally
      PrevHeartbeatStr := HeartbeatStr;
    End;
end;

// =============================================================================

function TSimpleServiceDelayTest.GetServiceController: TServiceController;
begin
  Result := ServiceController;
end;

// =============================================================================

Procedure TSimpleServiceDelayTest.Log(const Msg: string);
begin
  ServiceLog.Insert(0, FormatDateTime('yyyy/mm/dd hh:mm:ss.zzz ', Now) + Msg);
  While ServiceLog.Count > 500 do
    ServiceLog.Delete(ServiceLog.Count-1);
  // Save after every addition; inefficient, but thorough for debugging
  ServiceLog.SaveToFile(LogFileName);
end;

// =============================================================================

Function TSimpleServiceDelayTest.LogFileName: String;
Begin
  Result := System.SysUtils.ChangeFileExt(ParamStr(0), '.txt');
End;

// =============================================================================

procedure TSimpleServiceDelayTest.ServiceCreate(Sender: TObject);
begin
  ServiceLog := TStringList.Create;
  If FileExists(LogFileName) Then
    ServiceLog.LoadFromFile(LogFileName);
  Log('^^^ ServiceCreate ^^^');
  CheckStartUpDelayParam;
end;

// =============================================================================

procedure TSimpleServiceDelayTest.ServiceDestroy(Sender: TObject);
begin
  PrevHeartbeatStr := '';
  ServiceLog.Free;
end;

// =============================================================================

procedure TSimpleServiceDelayTest.ServiceExecute(Sender: TService);
begin
  Try
    Log('Entering ServiceExecute loop');
    While Not Terminated do
    Begin
      ServiceThread.ProcessRequests(False);
      DriveHeartbeatLogging;
      // Do other stuff
      Sleep(1000);
    End;
    Log('Exiting due to normal termination');
  Except
    On E: Exception do
      Log('Exiting due to Exception:' + #13#10 + E.Message);
  End;
End;

// =============================================================================

procedure TSimpleServiceDelayTest.ServiceShutdown(Sender: TService);
begin
  Log('ServiceShutdown');
end;

// =============================================================================

procedure TSimpleServiceDelayTest.ServiceStart(Sender: TService;
  var Started: Boolean);
begin
  Log('ServiceStart');
  Started := True;
end;

// =============================================================================

procedure TSimpleServiceDelayTest.ServiceStop(Sender: TService;
  var Stopped: Boolean);
begin
  Log('ServiceStop');
  Stopped := True;
end;

// =============================================================================

end.

//------------ ServiceUnit.dfm ----------------------

object SimpleServiceDelayTest: TSimpleServiceDelayTest
  OldCreateOrder = False
  OnCreate = ServiceCreate
  OnDestroy = ServiceDestroy
  DisplayName = 'Simple Delphi Service (Startup-Delay Test)'
  OnExecute = ServiceExecute
  OnShutdown = ServiceShutdown
  OnStart = ServiceStart
  OnStop = ServiceStop
  Height = 150
  Width = 215
end

接下来,要(卸载)安装一个简短的 GUI 服务界面应用程序,Start/Stop

//------------- SimpleServiceController.dpr ----------

program SimpleServiceController;

uses
  Vcl.Forms,
  ControllerMainUnit in 'ControllerMainUnit.pas' {MainWindow};

{$R *.res}

begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TMainWindow, MainWindow);
  Application.Run;
end.

//----------------ControlerMainUnit.pas----------------

unit ControllerMainUnit;

interface

uses
  System.Classes, System.SysUtils, System.Variants, Vcl.ComCtrls,
  Vcl.Controls, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.Forms, Vcl.Graphics, Vcl.Menus,
  Vcl.StdCtrls, Winapi.Messages, Winapi.Windows;

type
  TMainWindow = class(TForm)
    InstallService1: TMenuItem;
    MainMenu1: TMainMenu;
    Memo1: TMemo;
    StartService1: TMenuItem;
    StopService1: TMenuItem;
    Timer1: TTimer;
    UninstallService1: TMenuItem;
    StatusBar1: TStatusBar;
    StartWithoutDelayMenuItem: TMenuItem;
    StartWith10SecondDelay1: TMenuItem;
    StartWithXParameter1: TMenuItem;
    procedure Timer1Timer(Sender: TObject);
    procedure InstallService1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure StartWithoutDelayMenuItemClick(Sender: TObject);
    procedure StartWith10SecondDelay1Click(Sender: TObject);
    procedure StopService1Click(Sender: TObject);
    procedure UninstallService1Click(Sender: TObject);
    procedure StartWithXParameter1Click(Sender: TObject);
  private
    { Private declarations }
    FileTimeLoaded: _FILETIME;
    SCMError: Cardinal;
    SCMHandle: THandle;
    StatusStr: String;
    Function CurrentFileTime: _FILETIME;
    Function LogFileName: String;
    Procedure RelaunchElevatedPrompt;
    Function ServiceExePath: String;
    Procedure StartService(Const Parameter: String);
    Procedure StopService;
  public
    { Public declarations }
  end;

var
  MainWindow: TMainWindow;

implementation

{$R *.dfm}

Uses
  System.UITypes, Winapi.ShellAPI, Winapi.WinSvc;

Const
  cServiceName = 'SimpleServiceDelayTest';

// =============================================================================

Function AppHasElevatedPrivs: Boolean;

const
  TokenElevationType = 18;
  TokenElevation = 20;
  TokenElevationTypeDefault = 1;
  TokenElevationTypeFull = 2;
  TokenElevationTypeLimited = 3;

var
  token: THandle;
  Elevation: DWord;
  dwSize: Cardinal;

begin
  Try
    if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, token) then
      try
        if GetTokenInformation(token, TTokenInformationClass(TokenElevation),
          @Elevation, SizeOf(Elevation), dwSize) then
          Result := Elevation <> 0
        else
          Result := False;
      finally
        CloseHandle(token);
      end
    else
      Result := False;
  Except
    Result := False;
  End;
End;

// =============================================================================

Procedure Launch(Exe, Params: String);
Var
  Dir: String;
Begin
  Dir := ExtractFileDir(Exe);
  ShellExecute(0, 'open', PChar(Exe), PChar(Params), PChar(Dir), SW_SHOWNORMAL);
End;

// =============================================================================

Function NowStr: String;
Begin
  Result := FormatDateTime('yyyy/mm/dd hh:mm:ss', Now);
End;

// =============================================================================

Procedure LaunchElevated(Const Exe, Params: String);
Var
  Dir: String;
Begin
  Dir := ExtractFileDir(Exe);
  ShellExecute(0, 'runas', PChar(Exe), PChar(Params), PChar(Dir),
    SW_SHOWNORMAL);
End;

// =============================================================================

Function TMainWindow.CurrentFileTime;
Var
  FAD: TWin32FileAttributeData;
begin
  GetFileAttributesEx(PChar(LogFileName), GetFileExInfoStandard, @FAD);
  Result := FAD.ftLastWriteTime;
end;

// =============================================================================

procedure TMainWindow.FormCreate(Sender: TObject);
begin
  Application.Title := 'SimpleServiceController';
  if AppHasElevatedPrivs then
  begin
    SetLastError(0);
    SCMHandle := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS);
    SCMError := GetLastError;
  end
  else
  begin
    SCMHandle := 0;
    SCMError := 0;
  end;
end;

// =============================================================================

procedure TMainWindow.InstallService1Click(Sender: TObject);
begin
  If AppHasElevatedPrivs Then
    Launch(ServiceExePath, '/install')
  Else
    LaunchElevated(ServiceExePath, '/install');
End;

// =============================================================================

Function TMainWindow.LogFileName: String;
Begin
  Result := ExtractFileDir(Application.ExeName) + '\SimpleHeartbeatService.txt';
End;

// =============================================================================

Procedure TMainWindow.RelaunchElevatedPrompt;
Var
  Prompt: String;
  X, Y: Integer;
Begin
  Prompt := 'Elevated privileges required to start/stop service.'#13#10 +
    'Re-launch ' + Application.Title + ' with elevated privileges?';
  X := Left + 32;
  Y := Top + 32;
  If MessageDlgPos(Prompt, mtConfirmation, [mbYes, mbNo], 0, X, Y) = mrYes then
  Begin
    LaunchElevated(Application.ExeName, '');
    Close;
  End;
End;

// =============================================================================

Function TMainWindow.ServiceExePath;
begin
  Result := ExtractFileDir(Application.ExeName) + '\SimpleHeartbeatService.exe';
end;

// =============================================================================

Procedure TMainWindow.StartService(Const Parameter: string);
Var
  Result:Boolean;
  Svc: THandle;
  NumArgs: DWord;
  // ********** IS THIS THE CORRECT WAY TO SETUP lpServiceArgVectors ? *********
  // docs.microsoft.com/en-us/windows/desktop/api/winsvc/nf-winsvc-startservicea
  // ***************************************************************************
  ArgVectors: Array [0 .. 1] of PChar;
  pArgVectors: LPCWSTR; // To match VAR parameter type in StartService

Begin
  Try
    If SCMHandle = 0 Then
      RelaunchElevatedPrompt
    Else
    Begin
      Svc := OpenService(SCMHandle, PChar(cServiceName), SERVICE_START);
      if Svc = 0 then
        RaiseLastOSError;
      try
        // ******************* THIS IS WHERE I AM STYMIED **********************
        // StartService reports no errors either way it gets called below,
        // but no parameter are detected in the service when
        // ArgVectors = 'SimpleServiceDelayTest','10' and NumArgs = 2
        // *********************************************************************
        If Parameter <> '' Then
        Begin
          NumArgs := 2;
          ArgVectors[0] := PChar(cServiceName);
          ArgVectors[1] := PChar(Parameter); // Try 10 second delay
          pArgVectors := @ArgVectors;
        End
        Else
        Begin
          NumArgs := 0;
          ArgVectors[0] := '';
          ArgVectors[1] := '';
          pArgVectors := Nil;
        End;
        // NO ERROR, EITHER WAY; BUT PARAMSTR(1) ALWAYS BLANK IN SERVICE
        If Parameter = 'X'
          Then
            // http://codeverge.com/embarcadero.delphi.nativeapi/calling-startservice-with-multip/1067853
            Result := Winapi.WinSvc.StartService(Svc, NumArgs, ArgVectors[0])
          Else
            Result := Winapi.WinSvc.StartService(Svc, NumArgs, pArgVectors);
        If Result then
          ShowMessage('StartService('''+Parameter+''') returned TRUE')
        else
          RaiseLastOSError;
      finally
        CloseServiceHandle(Svc);
      end;
    End;
  except
    On E: Exception do
      Raise Exception.Create('StartService: ' + E.Message);
  end;
end;

// =============================================================================

procedure TMainWindow.StartWith10SecondDelay1Click(Sender: TObject);
begin
  StartService('10');
end;

// =============================================================================

procedure TMainWindow.StartWithoutDelayMenuItemClick(Sender: TObject);
begin
  StartService('');
end;

procedure TMainWindow.StartWithXParameter1Click(Sender: TObject);
begin
  StartService('X');
end;

// =============================================================================

Procedure TMainWindow.StopService;
Const
  OneSec = 1 / 86400;
Var
  Svc: THandle;
  Status: SERVICE_STATUS;
  TZero: TDateTime;
begin
  Try
    If SCMHandle = 0 Then
      RelaunchElevatedPrompt
    Else
    Begin
      Svc := OpenService(SCMHandle, PChar(cServiceName), SERVICE_STOP or
        SERVICE_QUERY_STATUS);
      if Svc = 0 then
        RaiseLastOSError
      else
        Try
          if Winapi.WinSvc.ControlService(Svc, SERVICE_CONTROL_STOP, Status)
          then
          Begin
            TZero := Now;
            while QueryServiceStatus(Svc, Status) and
              (Status.dwCurrentState <> SERVICE_STOPPED) and
              (Now - TZero < 5 * OneSec) do
            Begin
              Application.ProcessMessages;
              Sleep(10);
            End;
          End
          Else
            Raise Exception.Create('WinSvc.ControlService returned FALSE');
        finally
          CloseServiceHandle(Svc);
        end;
    End;
  except
    On E: Exception do
      Raise Exception.Create('StartService: ' + E.Message);
  end;
end;

// =============================================================================

procedure TMainWindow.StopService1Click(Sender: TObject);
begin
  StopService;
end;

// =============================================================================

procedure TMainWindow.Timer1Timer(Sender: TObject);
begin
  Try
    If Int64(CurrentFileTime) <> Int64(FileTimeLoaded) Then
    Begin
      Memo1.Lines.LoadFromFile(LogFileName);
      FileTimeLoaded := CurrentFileTime;
      StatusStr := ' File loaded @ ' + NowStr;
    End;
  Except
    StatusStr := ' Unable to load file @ ' + NowStr;
  End;
  StatusBar1.Panels[0].Text := FormatDateTime('hh:mm:ss ', Now) + StatusStr;
end;

// =============================================================================

procedure TMainWindow.UninstallService1Click(Sender: TObject);
begin
  If AppHasElevatedPrivs Then
    Launch(ServiceExePath, '/uninstall')
  Else
    LaunchElevated(ServiceExePath, '/uninstall');
end;

// =============================================================================

end.

//--------------------ControllerMainUnit.dfm----------------

object MainWindow: TMainWindow
  Left = 0
  Top = 0
  Caption = 'Simple Service Controller'
  ClientHeight = 264
  ClientWidth = 530
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  Menu = MainMenu1
  OldCreateOrder = False
  OnCreate = FormCreate
  PixelsPerInch = 96
  TextHeight = 13
  object Memo1: TMemo
    Left = 0
    Top = 0
    Width = 530
    Height = 245
    Align = alClient
    Font.Charset = ANSI_CHARSET
    Font.Color = clWindowText
    Font.Height = -13
    Font.Name = 'Courier New'
    Font.Style = []
    Lines.Strings = (
      'Memo1')
    ParentFont = False
    ScrollBars = ssBoth
    TabOrder = 0
  end
  object StatusBar1: TStatusBar
    Left = 0
    Top = 245
    Width = 530
    Height = 19
    Panels = <
      item
        Width = 50
      end>
  end
  object MainMenu1: TMainMenu
    Left = 136
    Top = 40
    object InstallService1: TMenuItem
      Caption = 'Install Service'
      OnClick = InstallService1Click
    end
    object UninstallService1: TMenuItem
      Caption = 'Uninstall Service'
      OnClick = UninstallService1Click
    end
    object StartService1: TMenuItem
      Caption = 'Start Service'
      object StartWithoutDelayMenuItem: TMenuItem
        Caption = 'Start Without Delay'
        OnClick = StartWithoutDelayMenuItemClick
      end
      object StartWith10SecondDelay1: TMenuItem
        Caption = 'Start With 10 Second Delay'
        OnClick = StartWith10SecondDelay1Click
      end
      object StartWithXParameter1: TMenuItem
        Caption = 'Start With "X" Parameter'
        OnClick = StartWithXParameter1Click
      end
    end
    object StopService1: TMenuItem
      Caption = 'Stop Service'
      OnClick = StopService1Click
    end
  end
  object Timer1: TTimer
    OnTimer = Timer1Timer
    Left = 240
    Top = 40
  end
end

TService.OnCreate event is the wrong place to run a delay loop. You need to put it in the TService.OnStart 事件。

OnCreate 事件总是在进程启动时调用,无论进程为何 运行 -(卸载)安装或服务启动。

OnStart 事件仅在 SCM 启动服务时调用。那是您需要处理服务启动参数的地方。

ParamStr() function retrieves the calling process's command-line parameters only, and that is not the correct place to look for service start parameters as they are not passed on the command line. They will be accessible from the TService.Param[] 属性 相反,一旦 SCM 发出服务启动信号。

尝试更像这样的东西:

Procedure TSimpleServiceDelayTest.CheckStartUpDelayParam;
const
  OneSec = 1000;
var
  DelaySecs: Integer;
  TZero: DWORD;
  i, num: Integer;
begin
  Log('CheckStartUpDelayParam');

  DelaySecs := 0;
  for i := 0 to ParamCount-1 do
  begin
    Log('Param['+IntToStr(i)+']=' + Param[i]);
    if DelaySecs = 0 then
    begin
      if TryStrToInt(Param[i], num) and (num > 0) then
        DelaySecs := num;
    end;
  end;

  if DelaySecs > 0 then
  begin
    TZero := GetTickCount();
    repeat
      Sleep(250);  // NOTE: should not exceed the TService.WaitHint value...
      ReportStatus;
    until (GetTickCount() - TZero) >= (DelaySecs * OneSec);
  end;
end;

...

procedure TSimpleServiceDelayTest.ServiceCreate(Sender: TObject);
begin
  ServiceLog := TStringList.Create;
  if FileExists(LogFileName) then
    ServiceLog.LoadFromFile(LogFileName);
  Log('^^^ ServiceCreate ^^^');
  // DO NOT call CheckStartUpDelayParam() here!
end;

procedure TSimpleServiceDelayTest.ServiceStart(Sender: TService; var Started: Boolean);
begin
  Log('ServiceStart');
  // call CheckStartUpDelayParam() here instead!
  CheckStartUpDelayParam;
  Started := True;
end;

procedure TMainWindow.StartService(Const Parameter: string);
var
  Result: Boolean;
  Svc: THandle;
  ArgVectors: Array [0 .. 1] of PChar;
  NumArgs: DWORD;
  pArgs: PPChar;
begin
  try
    if SCMHandle = 0 Then
      RelaunchElevatedPrompt
    else
    begin
      Svc := OpenService(SCMHandle, PChar(cServiceName), SERVICE_START);
      if Svc = 0 then
        RaiseLastOSError;
      try
        if Parameter <> '' then
        begin
          NumArgs := 2;
          ArgVectors[0] := PChar(cServiceName);
          ArgVectors[1] := PChar(Parameter);
          pArgs := @ArgVectors[0];
        end
        else
        begin
          NumArgs := 0;
          pArgs := nil;
        end;
        if not Winapi.WinSvc.StartService(Svc, NumArgs, pArgs^) then
          RaiseLastOSError;
      finally
        CloseServiceHandle(Svc);
      end;
      ShowMessage('StartService('''+Parameter+''') returned TRUE')
    end;
  except
    on E: Exception do
    begin
      raise Exception.Create('StartService: ' + E.Message);
    end;
  end;
end;