Quick.AppService.pas 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  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 : 05/10/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. TSvcInitializeEvent = procedure of object;
  50. {$IFDEF FPC}
  51. TSvcAnonMethod = procedure of object;
  52. {$ELSE}
  53. TSvcAnonMethod = reference to procedure;
  54. {$ENDIF}
  55. TSvcRemoveEvent = procedure of object;
  56. TAppService = class
  57. private
  58. fSCMHandle : SC_HANDLE;
  59. fSvHandle : SC_HANDLE;
  60. fServiceName : string;
  61. fDisplayName : string;
  62. fFileName : string;
  63. fSilent : Boolean;
  64. fStatus : TSvcStatus;
  65. fCanInstallWithOtherName : Boolean;
  66. fOnInitialize : TSvcInitializeEvent;
  67. fOnStart : TSvcAnonMethod;
  68. fOnStop : TSvcAnonMethod;
  69. fOnExecute : TSvcAnonMethod;
  70. fAfterRemove : TSvcRemoveEvent;
  71. procedure ReportSvcStatus(dwCurrentState, dwWin32ExitCode, dwWaitHint: DWORD);
  72. procedure Execute;
  73. procedure Help;
  74. procedure DoStop;
  75. public
  76. constructor Create;
  77. destructor Destroy; override;
  78. property ServiceName : string read fServiceName write fServiceName;
  79. property DisplayName : string read fDisplayName write fDisplayName;
  80. property FileName : string read fFileName write fFileName;
  81. property Silent : Boolean read fSilent write fSilent;
  82. property CanInstallWithOtherName : Boolean read fCanInstallWithOtherName write fCanInstallWithOtherName;
  83. property Status : TSvcStatus read fStatus write fStatus;
  84. property OnStart : TSvcAnonMethod read fOnStart write fOnStart;
  85. property OnStop : TSvcAnonMethod read fOnStop write fOnStop;
  86. property OnExecute : TSvcAnonMethod read fOnExecute write fOnExecute;
  87. property OnInitialize : TSvcInitializeEvent read fOnInitialize write fOnInitialize;
  88. property AfterRemove : TSvcRemoveEvent read fAfterRemove write fAfterRemove;
  89. procedure Install;
  90. procedure Remove;
  91. procedure CheckParams;
  92. class function InstallParamsPresent : Boolean;
  93. class function ConsoleParamPresent : Boolean;
  94. class function IsRunningAsService : Boolean;
  95. class function IsRunningAsConsole : Boolean;
  96. end;
  97. var
  98. ServiceStatus : TServiceStatus;
  99. StatusHandle : SERVICE_STATUS_HANDLE;
  100. ServiceTable : array [0..NUM_OF_SERVICES] of TServiceTableEntry;
  101. ghSvcStopEvent: Cardinal;
  102. AppService : TAppService;
  103. implementation
  104. procedure ServiceCtrlHandler(Control: DWORD); stdcall;
  105. begin
  106. case Control of
  107. SERVICE_CONTROL_STOP:
  108. begin
  109. AppService.Status := TSvcStatus.ssStopping;
  110. SetEvent(ghSvcStopEvent);
  111. ServiceStatus.dwCurrentState := SERVICE_STOP_PENDING;
  112. SetServiceStatus(StatusHandle, ServiceStatus);
  113. end;
  114. SERVICE_CONTROL_PAUSE:
  115. begin
  116. AppService.Status := TSvcStatus.ssPaused;
  117. ServiceStatus.dwcurrentstate := SERVICE_PAUSED;
  118. SetServiceStatus(StatusHandle, ServiceStatus);
  119. end;
  120. SERVICE_CONTROL_CONTINUE:
  121. begin
  122. AppService.Status := TSvcStatus.ssRunning;
  123. ServiceStatus.dwCurrentState := SERVICE_RUNNING;
  124. SetServiceStatus(StatusHandle, ServiceStatus);
  125. end;
  126. SERVICE_CONTROL_INTERROGATE: SetServiceStatus(StatusHandle, ServiceStatus);
  127. SERVICE_CONTROL_SHUTDOWN:
  128. begin
  129. AppService.Status := TSvcStatus.ssStopped;
  130. AppService.DoStop;
  131. end;
  132. end;
  133. end;
  134. procedure RegisterService(dwArgc: DWORD; var lpszArgv: PChar); stdcall;
  135. begin
  136. ServiceStatus.dwServiceType := SERVICE_WIN32_OWN_PROCESS;
  137. ServiceStatus.dwCurrentState := SERVICE_START_PENDING;
  138. ServiceStatus.dwControlsAccepted := SERVICE_ACCEPT_STOP or SERVICE_ACCEPT_PAUSE_CONTINUE;
  139. ServiceStatus.dwServiceSpecificExitCode := 0;
  140. ServiceStatus.dwWin32ExitCode := 0;
  141. ServiceStatus.dwCheckPoint := 0;
  142. ServiceStatus.dwWaitHint := 0;
  143. StatusHandle := RegisterServiceCtrlHandler(PChar(AppService.ServiceName), @ServiceCtrlHandler);
  144. if StatusHandle <> 0 then
  145. begin
  146. AppService.ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
  147. try
  148. AppService.Status := TSvcStatus.ssRunning;
  149. AppService.Execute;
  150. finally
  151. AppService.ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
  152. end;
  153. end;
  154. end;
  155. constructor TAppService.Create;
  156. begin
  157. inherited;
  158. fServiceName := DEF_SERVICENAME;
  159. fDisplayName := DEF_DISPLAYNAME;
  160. fFileName := ParamStr(0);
  161. fSilent := True;
  162. fStatus := TSvcStatus.ssStopped;
  163. fCanInstallWithOtherName := False;
  164. fOnExecute := nil;
  165. end;
  166. destructor TAppService.Destroy;
  167. begin
  168. fOnStart := nil;
  169. fOnStop := nil;
  170. fOnExecute := nil;
  171. if fSCMHandle <> 0 then CloseServiceHandle(fSCMHandle);
  172. if fSvHandle <> 0 then CloseServiceHandle(fSvHandle);
  173. inherited;
  174. end;
  175. procedure TAppService.ReportSvcStatus(dwCurrentState, dwWin32ExitCode, dwWaitHint: DWORD);
  176. begin
  177. //fill in the SERVICE_STATUS structure
  178. ServiceStatus.dwCurrentState := dwCurrentState;
  179. ServiceStatus.dwWin32ExitCode := dwWin32ExitCode;
  180. ServiceStatus.dwWaitHint := dwWaitHint;
  181. if dwCurrentState = SERVICE_START_PENDING then ServiceStatus.dwControlsAccepted := 0
  182. else ServiceStatus.dwControlsAccepted := SERVICE_ACCEPT_STOP;
  183. case (dwCurrentState = SERVICE_RUNNING) or (dwCurrentState = SERVICE_STOPPED) of
  184. True: ServiceStatus.dwCheckPoint := 0;
  185. False: ServiceStatus.dwCheckPoint := 1;
  186. end;
  187. //report service status to SCM
  188. SetServiceStatus(StatusHandle,ServiceStatus);
  189. end;
  190. procedure TAppService.Execute;
  191. begin
  192. //we have to do something or service will stop
  193. ghSvcStopEvent := CreateEvent(nil,True,False,nil);
  194. if ghSvcStopEvent = 0 then
  195. begin
  196. ReportSvcStatus(SERVICE_STOPPED,NO_ERROR,0);
  197. Exit;
  198. end;
  199. if Assigned(fOnStart) then fOnStart;
  200. //report running status when initialization is complete
  201. ReportSvcStatus(SERVICE_RUNNING,NO_ERROR,0);
  202. //perform work until service stops
  203. while True do
  204. begin
  205. //external callback process
  206. if Assigned(fOnExecute) then fOnExecute;
  207. //check whether to stop the service.
  208. WaitForSingleObject(ghSvcStopEvent,INFINITE);
  209. ReportSvcStatus(SERVICE_STOPPED,NO_ERROR,0);
  210. Exit;
  211. end;
  212. end;
  213. procedure TAppService.DoStop;
  214. begin
  215. if Assigned(fOnStop) then fOnStop;
  216. end;
  217. procedure TAppService.Remove;
  218. const
  219. cRemoveMsg = 'Service "%s" removed successfully!';
  220. var
  221. SCManager: SC_HANDLE;
  222. Service: SC_HANDLE;
  223. begin
  224. SCManager := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS);
  225. if SCManager = 0 then
  226. Exit;
  227. try
  228. Service := OpenService(SCManager,PChar(fServiceName),SERVICE_ALL_ACCESS);
  229. ControlService(Service,SERVICE_CONTROL_STOP,ServiceStatus);
  230. DeleteService(Service);
  231. CloseServiceHandle(Service);
  232. if fSilent then Writeln(Format(cRemoveMsg,[fServiceName]))
  233. else MessageBox(0,cRemoveMsg,PChar(fServiceName),MB_ICONINFORMATION or MB_OK or MB_TASKMODAL or MB_TOPMOST);
  234. finally
  235. CloseServiceHandle(SCManager);
  236. if Assigned(fAfterRemove) then fAfterRemove;
  237. end;
  238. end;
  239. procedure TAppService.Install;
  240. const
  241. cInstallMsg = 'Service "%s" installed successfully!';
  242. cSCMError = 'Error trying to open SC Manager (you need admin permissions)';
  243. begin
  244. fSCMHandle := OpenSCManager(nil,nil,SC_MANAGER_ALL_ACCESS);
  245. if fSCMHandle = 0 then
  246. begin
  247. if fSilent then Writeln(cSCMError)
  248. else MessageBox(0,cSCMError,PChar(fServiceName),MB_ICONERROR or MB_OK or MB_TASKMODAL or MB_TOPMOST);
  249. Exit;
  250. end;
  251. fSvHandle := CreateService(fSCMHandle,
  252. PChar(fServiceName),
  253. PChar(fDisplayName),
  254. SERVICE_ALL_ACCESS,
  255. SERVICE_WIN32_OWN_PROCESS,
  256. SERVICE_AUTO_START,
  257. SERVICE_ERROR_IGNORE,
  258. PChar(fFileName),
  259. 'System Reserved',
  260. nil,
  261. nil,
  262. nil, //user
  263. nil); //password
  264. if fSvHandle <> 0 then
  265. begin
  266. if fSilent then Writeln(Format(cInstallMsg,[fServiceName]))
  267. else MessageBox(0,cInstallMsg,PChar(fServiceName),MB_ICONINFORMATION or MB_OK or MB_TASKMODAL or MB_TOPMOST);
  268. end;
  269. end;
  270. procedure TAppService.Help;
  271. begin
  272. Writeln('HELP:');
  273. if fCanInstallWithOtherName then
  274. begin
  275. Writeln(Format('%s [/instance:<Service name>] [/console] [/install] [/remove] [/h] [/help]',[ExtractFileName(ParamStr(0))]));
  276. WriteLn(' [/instance:<service name>]'+#9+'Install service with a custom name');
  277. end
  278. else Writeln(Format('%s [/console] [/install] [/remove] [/h] [/help]',[ExtractFileName(ParamStr(0))]));
  279. WriteLn(' [/console]'+#9#9#9+'Run as a console application');
  280. WriteLn(' [/install]'+#9#9#9+'Install as a service');
  281. WriteLn(' [/remove]'+#9#9#9+'Remove service');
  282. WriteLn(' [/h /help]'+#9#9#9+'This help');
  283. end;
  284. procedure TAppService.CheckParams;
  285. var
  286. svcname : string;
  287. begin
  288. if ParamCount > 0 then
  289. begin
  290. if (ParamFindSwitch('h')) or (ParamFindSwitch('help')) then Self.Help
  291. else if ParamFindSwitch('install') then
  292. begin
  293. if (fCanInstallWithOtherName) and (ParamGetSwitch('instance',svcname)) then
  294. begin
  295. fServiceName := svcname;
  296. fDisplayName := svcname;
  297. end;
  298. Self.Install;
  299. end
  300. else if ParamFindSwitch('remove') then
  301. begin
  302. if (fCanInstallWithOtherName) and (ParamGetSwitch('instance',svcname)) then
  303. begin
  304. fServiceName := svcname;
  305. fDisplayName := svcname;
  306. end;
  307. Self.Remove;
  308. end
  309. else Writeln('Unknow parameter specified!');
  310. end
  311. else
  312. begin
  313. //initialize as a service
  314. if Assigned(fOnInitialize) then fOnInitialize;
  315. ServiceTable[0].lpServiceName := PChar(fServiceName);
  316. ServiceTable[0].lpServiceProc := @RegisterService;
  317. ServiceTable[1].lpServiceName := nil;
  318. ServiceTable[1].lpServiceProc := nil;
  319. {$IFDEF FPC}
  320. StartServiceCtrlDispatcher(@ServiceTable[0]);
  321. {$ELSE}
  322. StartServiceCtrlDispatcher(ServiceTable[0]);
  323. {$ENDIF}
  324. end;
  325. end;
  326. class function TAppService.ConsoleParamPresent : Boolean;
  327. begin
  328. Result := ParamFindSwitch('console');
  329. end;
  330. class function TAppService.InstallParamsPresent : Boolean;
  331. begin
  332. Result := (ParamFindSwitch('install') or ParamFindSwitch('remove') or ParamFindSwitch('help') or ParamFindSwitch('h'));
  333. end;
  334. class function TAppService.IsRunningAsService : Boolean;
  335. begin
  336. Result := (IsService and not ConsoleParamPresent) or InstallParamsPresent;
  337. end;
  338. class function TAppService.IsRunningAsConsole : Boolean;
  339. begin
  340. Result := (not IsService) or (ConsoleParamPresent);
  341. end;
  342. initialization
  343. AppService := TAppService.Create;
  344. finalization
  345. AppService.Free;
  346. end.