Quick.AppService.pas 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. { ***************************************************************************
  2. Copyright (c) 2016-2017 Kike Pérez
  3. Unit : Quick.AppService
  4. Description : Allow run app as console or service
  5. Author : Kike Pérez
  6. Version : 1.0
  7. Created : 14/09/2017
  8. Modified : 01/12/2017
  9. This file is part of QuickLib: https://github.com/exilon/QuickLib
  10. ***************************************************************************
  11. Licensed under the Apache License, Version 2.0 (the "License");
  12. you may not use this file except in compliance with the License.
  13. You may obtain a copy of the License at
  14. http://www.apache.org/licenses/LICENSE-2.0
  15. Unless required by applicable law or agreed to in writing, software
  16. distributed under the License is distributed on an "AS IS" BASIS,
  17. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. See the License for the specific language governing permissions and
  19. limitations under the License.
  20. *************************************************************************** }
  21. unit Quick.AppService;
  22. {$i QuickLib.inc}
  23. interface
  24. {$IFNDEF FPC}
  25. {$IFDEF DELPHI2010_UP}
  26. {$RTTI EXPLICIT METHODS([]) PROPERTIES([]) FIELDS([])}
  27. {$WEAKLINKRTTI ON}
  28. {$ENDIF}
  29. {$ENDIF}
  30. uses
  31. {$IFDEF MSWINDOWS}
  32. Windows,
  33. {$ENDIF}
  34. SysUtils,
  35. {$IFNDEF FPC}
  36. WinSvc,
  37. {$ENDIF}
  38. Quick.Commons;
  39. const
  40. DEF_SERVICENAME = 'QuickAppService';
  41. DEF_DISPLAYNAME = 'QuickAppService';
  42. NUM_OF_SERVICES = 2;
  43. type
  44. TSvcStatus = (ssStopped = SERVICE_STOPPED,
  45. ssStopping = SERVICE_STOP_PENDING,
  46. ssStartPending = SERVICE_START_PENDING,
  47. ssRunning = SERVICE_RUNNING,
  48. ssPaused = SERVICE_PAUSED);
  49. TSvcStartType = (stAuto = SERVICE_AUTO_START,
  50. stManual = SERVICE_DEMAND_START,
  51. stDisabled = SERVICE_DISABLED);
  52. TSvcInitializeEvent = procedure of object;
  53. {$IFDEF FPC}
  54. TSvcAnonMethod = procedure of object;
  55. {$ELSE}
  56. TSvcAnonMethod = reference to procedure;
  57. {$ENDIF}
  58. TSvcRemoveEvent = procedure of object;
  59. TAppService = class
  60. private
  61. fSCMHandle : SC_HANDLE;
  62. fSvHandle : SC_HANDLE;
  63. fServiceName : string;
  64. fDisplayName : string;
  65. fLoadOrderGroup : string;
  66. fDependencies : string;
  67. fDesktopInteraction : Boolean;
  68. fUsername : string;
  69. fUserPass : string;
  70. fStartType : TSvcStartType;
  71. fFileName : string;
  72. fSilent : Boolean;
  73. fStatus : TSvcStatus;
  74. fCanInstallWithOtherName : Boolean;
  75. fOnInitialize : TSvcInitializeEvent;
  76. fOnStart : TSvcAnonMethod;
  77. fOnStop : TSvcAnonMethod;
  78. fOnExecute : TSvcAnonMethod;
  79. fAfterRemove : TSvcRemoveEvent;
  80. fServiceDescription : string;
  81. procedure ReportSvcStatus(dwCurrentState, dwWin32ExitCode, dwWaitHint: DWORD);
  82. procedure Execute;
  83. procedure Help;
  84. procedure DoStop;
  85. procedure SetServiceDescription;
  86. public
  87. constructor Create;
  88. destructor Destroy; override;
  89. property ServiceName : string read fServiceName write fServiceName;
  90. property DisplayName : string read fDisplayName write fDisplayName;
  91. property ServiceDescription : string read fServiceDescription write fServiceDescription;
  92. property LoadOrderGroup : string read fLoadOrderGroup write fLoadOrderGroup;
  93. property Dependencies : string read fDependencies write fDependencies;
  94. property DesktopInteraction : Boolean read fDesktopInteraction write fDesktopInteraction;
  95. property UserName : string read fUserName write fUserName;
  96. property UserPass : string read fUserPass write fUserPass;
  97. property StartType : TSvcStartType read fStartType write fStartType;
  98. property FileName : string read fFileName write fFileName;
  99. property Silent : Boolean read fSilent write fSilent;
  100. property CanInstallWithOtherName : Boolean read fCanInstallWithOtherName write fCanInstallWithOtherName;
  101. property Status : TSvcStatus read fStatus write fStatus;
  102. property OnStart : TSvcAnonMethod read fOnStart write fOnStart;
  103. property OnStop : TSvcAnonMethod read fOnStop write fOnStop;
  104. property OnExecute : TSvcAnonMethod read fOnExecute write fOnExecute;
  105. property OnInitialize : TSvcInitializeEvent read fOnInitialize write fOnInitialize;
  106. property AfterRemove : TSvcRemoveEvent read fAfterRemove write fAfterRemove;
  107. procedure Install;
  108. procedure Remove;
  109. procedure CheckParams;
  110. class function InstallParamsPresent : Boolean;
  111. class function ConsoleParamPresent : Boolean;
  112. class function IsRunningAsService : Boolean;
  113. class function IsRunningAsConsole : Boolean;
  114. end;
  115. var
  116. ServiceStatus : TServiceStatus;
  117. StatusHandle : SERVICE_STATUS_HANDLE;
  118. ServiceTable : array [0..NUM_OF_SERVICES] of TServiceTableEntry;
  119. ghSvcStopEvent: Cardinal;
  120. AppService : TAppService;
  121. implementation
  122. {$IFDEF MSWINDOWS}
  123. uses
  124. Registry;
  125. {$ENDIF}
  126. procedure ServiceCtrlHandler(Control: DWORD); stdcall;
  127. begin
  128. case Control of
  129. SERVICE_CONTROL_STOP:
  130. begin
  131. AppService.Status := TSvcStatus.ssStopping;
  132. SetEvent(ghSvcStopEvent);
  133. ServiceStatus.dwCurrentState := SERVICE_STOP_PENDING;
  134. SetServiceStatus(StatusHandle, ServiceStatus);
  135. end;
  136. SERVICE_CONTROL_PAUSE:
  137. begin
  138. AppService.Status := TSvcStatus.ssPaused;
  139. ServiceStatus.dwcurrentstate := SERVICE_PAUSED;
  140. SetServiceStatus(StatusHandle, ServiceStatus);
  141. end;
  142. SERVICE_CONTROL_CONTINUE:
  143. begin
  144. AppService.Status := TSvcStatus.ssRunning;
  145. ServiceStatus.dwCurrentState := SERVICE_RUNNING;
  146. SetServiceStatus(StatusHandle, ServiceStatus);
  147. end;
  148. SERVICE_CONTROL_INTERROGATE: SetServiceStatus(StatusHandle, ServiceStatus);
  149. SERVICE_CONTROL_SHUTDOWN:
  150. begin
  151. AppService.Status := TSvcStatus.ssStopped;
  152. AppService.DoStop;
  153. end;
  154. end;
  155. end;
  156. procedure RegisterService(dwArgc: DWORD; var lpszArgv: PChar); stdcall;
  157. begin
  158. ServiceStatus.dwServiceType := SERVICE_WIN32_OWN_PROCESS;
  159. ServiceStatus.dwCurrentState := SERVICE_START_PENDING;
  160. ServiceStatus.dwControlsAccepted := SERVICE_ACCEPT_STOP or SERVICE_ACCEPT_PAUSE_CONTINUE;
  161. ServiceStatus.dwServiceSpecificExitCode := 0;
  162. ServiceStatus.dwWin32ExitCode := 0;
  163. ServiceStatus.dwCheckPoint := 0;
  164. ServiceStatus.dwWaitHint := 0;
  165. StatusHandle := RegisterServiceCtrlHandler(PChar(AppService.ServiceName), @ServiceCtrlHandler);
  166. if StatusHandle <> 0 then
  167. begin
  168. AppService.ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
  169. try
  170. AppService.Status := TSvcStatus.ssRunning;
  171. AppService.Execute;
  172. finally
  173. AppService.ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
  174. end;
  175. end;
  176. end;
  177. constructor TAppService.Create;
  178. begin
  179. inherited;
  180. fServiceName := DEF_SERVICENAME;
  181. fDisplayName := DEF_DISPLAYNAME;
  182. fLoadOrderGroup := '';
  183. fDependencies := '';
  184. fDesktopInteraction := False;
  185. fUserName := '';
  186. fUserPass := '';
  187. fStartType := TSvcStartType.stAuto;
  188. fFileName := ParamStr(0);
  189. fSilent := True;
  190. fStatus := TSvcStatus.ssStopped;
  191. fCanInstallWithOtherName := False;
  192. fOnExecute := nil;
  193. IsQuickServiceApp := True;
  194. end;
  195. destructor TAppService.Destroy;
  196. begin
  197. fOnStart := nil;
  198. fOnStop := nil;
  199. fOnExecute := nil;
  200. if fSCMHandle <> 0 then CloseServiceHandle(fSCMHandle);
  201. if fSvHandle <> 0 then CloseServiceHandle(fSvHandle);
  202. inherited;
  203. end;
  204. procedure TAppService.ReportSvcStatus(dwCurrentState, dwWin32ExitCode, dwWaitHint: DWORD);
  205. begin
  206. //fill in the SERVICE_STATUS structure
  207. ServiceStatus.dwCurrentState := dwCurrentState;
  208. ServiceStatus.dwWin32ExitCode := dwWin32ExitCode;
  209. ServiceStatus.dwWaitHint := dwWaitHint;
  210. if dwCurrentState = SERVICE_START_PENDING then ServiceStatus.dwControlsAccepted := 0
  211. else ServiceStatus.dwControlsAccepted := SERVICE_ACCEPT_STOP;
  212. case (dwCurrentState = SERVICE_RUNNING) or (dwCurrentState = SERVICE_STOPPED) of
  213. True: ServiceStatus.dwCheckPoint := 0;
  214. False: ServiceStatus.dwCheckPoint := 1;
  215. end;
  216. //report service status to SCM
  217. SetServiceStatus(StatusHandle,ServiceStatus);
  218. end;
  219. procedure TAppService.SetServiceDescription;
  220. {$IFDEF MSWINDOWS}
  221. var
  222. reg: TRegistry;
  223. {$ENDIF}
  224. begin
  225. {$IFDEF MSWINDOWS}
  226. reg := TRegistry.Create(KEY_READ or KEY_WRITE);
  227. try
  228. reg.RootKey := HKEY_LOCAL_MACHINE;
  229. if reg.OpenKey('\SYSTEM\CurrentControlSet\Services\' + fServiceName, False) then
  230. begin
  231. reg.WriteString('Description', fServiceDescription);
  232. reg.CloseKey;
  233. end;
  234. finally
  235. reg.Free;
  236. end;
  237. {$ENDIF}
  238. end;
  239. procedure TAppService.Execute;
  240. begin
  241. //we have to do something or service will stop
  242. ghSvcStopEvent := CreateEvent(nil,True,False,nil);
  243. if ghSvcStopEvent = 0 then
  244. begin
  245. ReportSvcStatus(SERVICE_STOPPED,NO_ERROR,0);
  246. Exit;
  247. end;
  248. if Assigned(fOnStart) then fOnStart;
  249. //report running status when initialization is complete
  250. ReportSvcStatus(SERVICE_RUNNING,NO_ERROR,0);
  251. //perform work until service stops
  252. while True do
  253. begin
  254. //external callback process
  255. if Assigned(fOnExecute) then fOnExecute;
  256. //check whether to stop the service.
  257. WaitForSingleObject(ghSvcStopEvent,INFINITE);
  258. ReportSvcStatus(SERVICE_STOPPED,NO_ERROR,0);
  259. Exit;
  260. end;
  261. end;
  262. procedure TAppService.DoStop;
  263. begin
  264. if Assigned(fOnStop) then fOnStop;
  265. end;
  266. procedure TAppService.Remove;
  267. const
  268. cRemoveMsg = 'Service "%s" removed successfully!';
  269. var
  270. SCManager: SC_HANDLE;
  271. Service: SC_HANDLE;
  272. begin
  273. SCManager := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS);
  274. if SCManager = 0 then
  275. Exit;
  276. try
  277. Service := OpenService(SCManager,PChar(fServiceName),SERVICE_ALL_ACCESS);
  278. ControlService(Service,SERVICE_CONTROL_STOP,ServiceStatus);
  279. DeleteService(Service);
  280. CloseServiceHandle(Service);
  281. if fSilent then Writeln(Format(cRemoveMsg,[fServiceName]))
  282. else MessageBox(0,cRemoveMsg,PChar(fServiceName),MB_ICONINFORMATION or MB_OK or MB_TASKMODAL or MB_TOPMOST);
  283. finally
  284. CloseServiceHandle(SCManager);
  285. if Assigned(fAfterRemove) then fAfterRemove;
  286. end;
  287. end;
  288. procedure TAppService.Install;
  289. const
  290. cInstallMsg = 'Service "%s" installed successfully!';
  291. cSCMError = 'Error trying to open SC Manager (you need admin permissions)';
  292. var
  293. servicetype : Cardinal;
  294. starttype : Cardinal;
  295. svcloadgroup : PChar;
  296. svcdependencies : PChar;
  297. svcusername : PChar;
  298. svcuserpass : PChar;
  299. begin
  300. fSCMHandle := OpenSCManager(nil,nil,SC_MANAGER_ALL_ACCESS);
  301. if fSCMHandle = 0 then
  302. begin
  303. if fSilent then Writeln(cSCMError)
  304. else MessageBox(0,cSCMError,PChar(fServiceName),MB_ICONERROR or MB_OK or MB_TASKMODAL or MB_TOPMOST);
  305. Exit;
  306. end;
  307. //service interacts with desktop
  308. if fDesktopInteraction then servicetype := SERVICE_WIN32_OWN_PROCESS and SERVICE_INTERACTIVE_PROCESS
  309. else servicetype := SERVICE_WIN32_OWN_PROCESS;
  310. //service load order
  311. if fLoadOrderGroup.IsEmpty then svcloadgroup := nil
  312. else svcloadgroup := PChar(fLoadOrderGroup);
  313. //service dependencies
  314. if fDependencies.IsEmpty then svcdependencies := nil
  315. else svcdependencies := PChar(fDependencies);
  316. //service user name
  317. if fUserName.IsEmpty then svcusername := nil
  318. else svcusername := PChar(fUserName);
  319. //service user password
  320. if fUserPass.IsEmpty then svcuserpass := nil
  321. else svcuserpass := PChar(fUserPass);
  322. fSvHandle := CreateService(fSCMHandle,
  323. PChar(fServiceName),
  324. PChar(fDisplayName),
  325. SERVICE_ALL_ACCESS,
  326. servicetype,
  327. Cardinal(fStartType),
  328. SERVICE_ERROR_NORMAL,
  329. PChar(fFileName),
  330. svcloadgroup,
  331. nil,
  332. svcdependencies,
  333. svcusername, //user
  334. svcuserpass); //password
  335. if Length(fServiceDescription) > 0 then
  336. SetServiceDescription;
  337. if fSvHandle <> 0 then
  338. begin
  339. if fSilent then Writeln(Format(cInstallMsg,[fServiceName]))
  340. else MessageBox(0,cInstallMsg,PChar(fServiceName),MB_ICONINFORMATION or MB_OK or MB_TASKMODAL or MB_TOPMOST);
  341. end;
  342. end;
  343. procedure TAppService.Help;
  344. begin
  345. Writeln('HELP:');
  346. if fCanInstallWithOtherName then
  347. begin
  348. Writeln(Format('%s [/instance:<Service name>] [/console] [/install] [/remove] [/h] [/help]',[ExtractFileName(ParamStr(0))]));
  349. WriteLn(' [/instance:<service name>]'+#9+'Install service with a custom name');
  350. end
  351. else Writeln(Format('%s [/console] [/install] [/remove] [/h] [/help]',[ExtractFileName(ParamStr(0))]));
  352. WriteLn(' [/console]'+#9#9#9+'Force run as a console application (when run from another service)');
  353. WriteLn(' [/install]'+#9#9#9+'Install as a service');
  354. WriteLn(' [/remove]'+#9#9#9+'Remove service');
  355. WriteLn(' [/h /help]'+#9#9#9+'This help');
  356. end;
  357. procedure TAppService.CheckParams;
  358. var
  359. svcname : string;
  360. begin
  361. if ParamCount > 0 then
  362. begin
  363. if (ParamFindSwitch('h')) or (ParamFindSwitch('help')) then Self.Help
  364. else if ParamFindSwitch('install') then
  365. begin
  366. if (fCanInstallWithOtherName) and (ParamGetSwitch('instance',svcname)) then
  367. begin
  368. fServiceName := svcname;
  369. fDisplayName := svcname;
  370. end;
  371. Self.Install;
  372. end
  373. else if ParamFindSwitch('remove') then
  374. begin
  375. if (fCanInstallWithOtherName) and (ParamGetSwitch('instance',svcname)) then
  376. begin
  377. fServiceName := svcname;
  378. fDisplayName := svcname;
  379. end;
  380. Self.Remove;
  381. end
  382. else if ParamFindSwitch('console') then
  383. begin
  384. Writeln('Forced console mode');
  385. end
  386. else Writeln('Unknow parameter specified!');
  387. end
  388. else
  389. begin
  390. //initialize as a service
  391. if Assigned(fOnInitialize) then fOnInitialize;
  392. ServiceTable[0].lpServiceName := PChar(fServiceName);
  393. ServiceTable[0].lpServiceProc := @RegisterService;
  394. ServiceTable[1].lpServiceName := nil;
  395. ServiceTable[1].lpServiceProc := nil;
  396. {$IFDEF FPC}
  397. StartServiceCtrlDispatcher(@ServiceTable[0]);
  398. {$ELSE}
  399. StartServiceCtrlDispatcher(ServiceTable[0]);
  400. {$ENDIF}
  401. end;
  402. end;
  403. class function TAppService.ConsoleParamPresent : Boolean;
  404. begin
  405. Result := ParamFindSwitch('console');
  406. end;
  407. class function TAppService.InstallParamsPresent : Boolean;
  408. begin
  409. Result := (ParamFindSwitch('install') or ParamFindSwitch('remove') or ParamFindSwitch('help') or ParamFindSwitch('h'));
  410. end;
  411. class function TAppService.IsRunningAsService : Boolean;
  412. begin
  413. Result := (IsService and not ConsoleParamPresent) or InstallParamsPresent;
  414. end;
  415. class function TAppService.IsRunningAsConsole : Boolean;
  416. begin
  417. Result := (not IsService) or (ConsoleParamPresent);
  418. end;
  419. initialization
  420. AppService := TAppService.Create;
  421. finalization
  422. AppService.Free;
  423. end.