-
-
Notifications
You must be signed in to change notification settings - Fork 136
/
Copy pathmormot.app.agl.pas
2101 lines (1992 loc) · 71.7 KB
/
mormot.app.agl.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/// High-Level Angelize Logic to Manage Multiple Daemons
// - this unit is a part of the Open Source Synopse mORMot framework 2,
// licensed under a MPL/GPL/LGPL three license - see LICENSE.md
unit mormot.app.agl;
{
*****************************************************************************
Launch, Watch and Kill Services or Executables from a main Service Instance
- TSynAngelizeService Sub-Service Settings and Process
- TSynAngelize Main Service Launcher and Watcher
*****************************************************************************
}
interface
{$I ..\mormot.defines.inc}
uses
sysutils,
classes,
variants,
mormot.core.base,
mormot.core.os,
mormot.core.unicode,
mormot.core.text,
mormot.core.buffers,
mormot.core.datetime,
mormot.core.threads,
mormot.core.rtti,
mormot.core.json,
mormot.core.data,
mormot.core.log,
mormot.core.zip,
mormot.net.http,
mormot.net.client,
mormot.app.console,
mormot.app.daemon;
{ ************ TSynAngelizeService Sub-Service Settings and Process }
type
/// exception class raised by TSynAngelize
ESynAngelize = class(ESynException);
/// define one TSynAngelizeService action
// - depending on the context, as "method:param" pair
// - may contain e.g. 'exec:/path/to/file', 'start:%run%' or even 'start'
TSynAngelizeAction = type RawUtf8;
/// define one or several TSynAngelizeService action(s)
// - stored as a JSON array in the settings
TSynAngelizeActions = array of TSynAngelizeAction;
TSynAngelize = class;
TSynAngelizeService = class;
/// define how "start:exename" RunRedirect() is executed
// - soReplaceEnv let "StartEnv" values fully replace the existing environment
// - soWinJobCloseChildren will setup a Windows Job to close any child
// process(es) when the created process quits - not set by default
// - soRedirectLogRotateCompress will rotate redirected console output files
// using .gz compression instead of plain copy
TStartOptions = set of (
soReplaceEnv,
soWinJobCloseChildren,
soRedirectLogRotateCompress);
/// used to process a "start:exename" command in the background
TSynAngelizeRunner = class(TThreadAbstract)
protected
fLog: TSynLogClass;
fSender: TSynAngelize;
fService: TSynAngelizeService;
fRetryEvent: TSynEvent;
fRedirect: TFileStreamEx;
fRedirectSize, fNotifyStableTix: Int64;
// copy of fService properties
fCmd, fEnv, fWrkDir, fRedirectFileName: TFileName;
fAbortRequested: boolean;
fRunOptions: TRunOptions;
procedure Execute; override;
procedure PerformRotation;
function OnRedirect(const text: RawByteString; pid: cardinal): boolean;
public
/// start the execution of a command in a background thread
constructor Create(aSender: TSynAngelize; aLog: TSynLog;
aService: TSynAngelizeService; const aCmd, aEnv, aWrkDir: TFileName;
aRedirect: TFileStreamEx); reintroduce;
/// finalize the execution
destructor Destroy; override;
/// abort this process (either at shutdown, or after "stop")
procedure Abort;
/// let Execute retry to start the process now
procedure RetryNow;
end;
// define TSynAngelizeService.DoNotify() kind of notification
TDoNotify = (
doNothing,
doExitNoRetry,
doExitRetry,
doExitRecover,
doWatchFailed);
/// one sub-process definition as recognized by TSynAngelize
// - TSynAngelizeAction properties will expand %abc% place-holders when needed
// - specifies how to start, stop and watch a given sub-process
// - main idea is to let sub-processes remain simple process, not Operating
// System daemons/services, for a cross-platform and user-friendly experience
TSynAngelizeService = class(TSynJsonFileSettings)
protected
fName: RawUtf8;
fOwner: TSynAngelize;
fDescription: RawUtf8;
fRun: RawUtf8;
fStartWorkDir, fNotify: RawUtf8;
fStartEnv: TRawUtf8DynArray;
fStart, fStop, fWatch: TSynAngelizeActions;
fStateMessage: RawUtf8;
fState: TServiceState;
fStartOptions: TStartOptions;
fOS: TOperatingSystem;
fDisabled: boolean;
fLevel, fStopRunAbortTimeoutSec: integer;
fWatchDelaySec, fWatchCountRestart, fWatchCount: integer;
fRetryStableSec: integer;
fRedirectLogFile: RawUtf8;
fRedirectLogRotateFiles, fRedirectLogRotateBytes: integer;
fStarted: RawUtf8;
fRunner: TSynAngelizeRunner;
fRunnerExitCode: integer;
fAbortExitCodes: TIntegerDynArray;
fNextWatch: Int64;
fLastNotify: TDoNotify;
fLastNotifyMsg: RawUtf8;
procedure SetState(NewState: TServiceState; const Fmt: RawUtf8;
const Args: array of const; ResetMessage: boolean = false);
procedure DoStart(log: TSynLog);
function DoStop(log: TSynLog): boolean;
procedure DoNotify(What: TDoNotify;
const Fmt: RawUtf8; const Args: array of const); virtual;
procedure OnWatchFailed(const Msg: RawUtf8); virtual;
public
/// initialize and set the default settings
constructor Create; override;
/// finalize the sub-process instance
destructor Destroy; override;
/// the current state of the service, as retrieved during the Watch phase
property State: TServiceState
read fState;
/// text associated to the current state, as generated during Watch phase
property StateMessage: RawUtf8
read fStateMessage;
published
/// if true, this service definition will be ignored
// - is set e.g. after /new --new
property Disabled: boolean
read fDisabled write fDisabled;
/// computer-friendly case-insensitive identifier of this sub-service
// - as used internally by TSynAngelize to identify this instance
// - should be a short, if possible ASCII and pascal-compatible, identifier
// - contains e.g. "Name": "AuthService",
property Name: RawUtf8
read fName write fName;
/// some reusable parameter available as %run% place holder
// - default is void '', but could be used to store an executable name
// or a Windows service name, e.g. "Run":"/path/to/authservice" or
// "Run": "MyCompanyService" or "Run":"\"c:\path to\program.exe\" param1 param2"
// - is used as default if a "Start" "Stop" or "Watch" command has no second
// value (e.g. ["start"] means ["start:%run%"]), or is void (e.g. "Start":[]
// means also ["start:%run"])
// - the easiest case is to put the path to executable here (with double
// quotes for space within the file name), and keep "Start" "Stop" and
// "Watch" entries as [], for a NSSM-like behavior
property Run: RawUtf8
read fRun write fRun;
/// human-friendly Unicode text which could be displayed on Web or Console UI
// - in addition to the Name short identifier
// - contains e.g. "Description": "Authentication Service",
property Description: RawUtf8
read fDescription write fDescription;
/// sub-services are started from their increasing Level
// - allow to define dependencies between sub-services
// - it could be a good idea to define it by increments of ten (10,20,30...),
// so that intermediate services may be inserted easily in the rings
// - will disable the entry if set to 0 or any negative number
property Level: integer
read fLevel write fLevel;
/// this sub-service could be activated only for a given Operating System
// - default osUnknown will run it on all systems
// - you can specify osWindows, osOSX, osBSD, osLinux or osPOSIX
// - or even a specific Linux distribution (instead of wider osLinux/osPOSIX)
property OS: TOperatingSystem
read fOS write fOS;
/// the action(s) executed to start the sub-process
// - will be executed in-order
// - could include %abc% place holders
// - "start:/path/to/file" for starting and monitoring the process,
// terminated with "Stop": [ "stop:/path/to/file" ] command, optionally
// writing console output to RedirectLogFile (like NSSM)
// - "exec:/path/to/file" for not waiting up to its ending
// - "wait:/path/to/file" for waiting for its ending with 0 exitcode
// - "http://127.0.0.1:8080/publish/on" for a local HTTP request
// - "sleep:1000" e.g. for 1 second wait between steps or after start
// - on Windows, "service:ServiceName" calls TServiceController.Start
// - if no ':' is set, ":%run%" is assumed, e.g. "start" = "start:%run%"
// - you can add =## to change the expected result (0 as file exitcode, 200
// as http status) - e.g. 'http://127.0.0.1:8900/test=201'
// - default void "Start":[] will assume executing 'start:%run%'
property Start: TSynAngelizeActions
read fStart write fStart;
/// array of "name=value" pairs to be added to the "start" env variables
// - only supported on Windows by now
// - could include %abc% place holders
property StartEnv: TRawUtf8DynArray
read fStartEnv write fStartEnv;
/// define how "start" RunRedirect() is executed
property StartOptions: TStartOptions
read fStartOptions write fStartOptions;
/// optional working folder for "start" monitored process
// - default void "" will use the sub-process "start" executable path
// - could include %abc% place holders
property StartWorkDir: RawUtf8
read fStartWorkDir write fStartWorkDir;
/// the action(s) executed to stop the sub-process
// - will be executed in-order
// - could include %abc% place holders
// - "exec:/path/to/file" for not waiting up to its ending
// - "wait:/path/to/file" for waiting for its ending
// - "stop:/path/to/file" for stopping a process monitored from a "Start":
// [ "start:/path/to/file" ] previous command
// - "http://127.0.0.1:8080/publish/off" for a local HTTP request
// - "sleep:1000" e.g. for 1 second wait between steps or after stop
// - on Windows, "service:ServiceName" calls TServiceController.Stop
// - if no ':' is set, ":%run%" is assumed, e.g. "stop" = "stop:%run%"
// - you can add =## but the value is ignored during stopping
// - default void "Stop":[] will assume executing 'stop:%run%'
property Stop: TSynAngelizeActions
read fStop write fStop;
/// how many seconds to wait for a process to exit after gracefull signals
// - e.g. after Ctrl+C or WMQUIT on Windows, or SIGTERM on POSIX
// - default is 10 seconds
// - you may set 0 for no gracefull stop, but direct TerminateProcess/SIGKILL
property StopRunAbortTimeoutSec: integer
read fStopRunAbortTimeoutSec write fStopRunAbortTimeoutSec;
/// how many seconds a "start" monitored process exit is considered stable
// - similar to NSSM, by default it will try to restart the executable if
// the application died without a "stop" signal, with increasing pauses
// - this value is the number of seconds after which a process could be
// retried with no pausing - default is 60, i.e. one minute
// - setting 0 to this property would disable the whole restart algorithm
// - call "agl /retry" to restart/retry all aborted or paused service(s)
property RetryStableSec: integer
read fRetryStableSec write fRetryStableSec;
/// if the process stopped with one of this exit codes, won't restart
// - call "agl /retry" to restart/retry all aborted or paused service(s)
property AbortExitCodes: TIntegerDynArray
read fAbortExitCodes write fAbortExitCodes;
/// where to send a notification message about a failure or recovery
// - e.g. when a "start" monitored process exited without "stop" signal
// or on "Watch" failure
// - could be a regular email address like '[email protected]' - with
// main TSynAngelizeSettings.Smtp/SmtpFrom properly defined
// - could be an executable file to run
// - could be a .log file to append the notification message as text
// - could be a http:// or https:// request to GET
// - could include %abc% place holders, e.g. %name% for the sub-process name
// or %msg% and %urimsg% for the context (URI-encoded) message, or %what%
// for the text of the associated TDoNotify enumerate
// - on restart failure, it won't send a notify unless RetryStableSec delay
// is reached (to avoid too verbose notifications)
// - you can specify several notifications as CSV, e.g.
// '[email protected],%log%notif-%name%.log'
property Notify: RawUtf8
read fNotify write fNotify;
// - will be executed in-order every at WatchDelaySec pace
// - could include %abc% place holders
// - "exec:/path/to/file" for not waiting up to its ending
// - "wait:/path/to/file" for waiting for its ending with 0 exitcode
// - "http://127.0.0.1:8080/publish/watchme" for a local HTTP
// request returning 200 on status success
// - on Windows, "service:ServiceName" calls TServiceController.State
// - if no ':###' is set, ":%run%" is assumed, e.g. "wait" = "wait:%run%"
// - you can add '=##' to change the expected result (0 as file exitcode,
// 200 as http status)
// - on failure, it will execute the "Notify" process, then try to restart
// the sub-process, i.e. call all "Stop" then "Start" steps, unless
// RetryStableSec was set to 0 to disable this restart feature
// - note that a process monitored from a "Start": [ "start:/path/to/file" ]
// previous command is automatically watched in its monitoring thread, so
// you can keep the default void "Watch":[] entry, with "Notify" if needed
property Watch: TSynAngelizeActions
read fWatch write fWatch;
/// how many seconds should we wait between each "Watch" method step
// - default is 60 seconds
// - note that all "Watch" commands of all services are done in a single
// thread, so a too small value here may have no practical impact
property WatchDelaySec: integer
read fWatchDelaySec write fWatchDelaySec;
/// after how many "Watch" failures should we try to restart the sub-process
// - restarting at the first failure may be too paranoid
// - default is 2, so it will restart after 1..2 minutes with WatchDelaySec=60
property WatchCountRestart: integer
read fWatchCountRestart write fWatchCountRestart;
/// redirect "start:/path/to/executable" console output to a log file
// - could include %abc% place holders, e.g. '%agl.base%' or %agl.now%
// - a typical value is therefore "%agl.logpath%%name%-%agl.now%.log"
property RedirectLogFile: RawUtf8
read fRedirectLogFile write fRedirectLogFile;
/// how many rotate files RedirectLogFile could generate at once
// - default 0 disable the whole rotation process
property RedirectLogRotateFiles: integer
read fRedirectLogRotateFiles write fRedirectLogRotateFiles;
/// after how many bytes in RedirectLogFile rotation should occur
// - default is 100 MB
property RedirectLogRotateBytes: integer
read fRedirectLogRotateBytes write fRedirectLogRotateBytes;
end;
/// meta-class of TSynAngelizeService
// - so that base TSynAngelizeService class could be inherited and completed
// with custom properties, ready to be retrieved as %...% placeholders
TSynAngelizeServiceClass = class of TSynAngelizeService;
{ ************ TSynAngelize Main Service Launcher and Watcher }
/// define the main TSynAngelize daemon/service behavior
TSynAngelizeSettings = class(TSynDaemonSettings)
protected
fFolder, fExt, fStateFile, fCommandFile: TFileName;
fHtmlStateFileIdentifier, fSmtp, fSmtpFrom: RawUtf8;
fHttpTimeoutMS, fStartDelayMS, fStartTimeoutSec: integer;
public
/// set the default values
constructor Create; override;
published
/// where the TSynAngelizeService settings are stored
// - default is the 'services' sub-folder of the TSynAngelizeSettings
property Folder: TFileName
read fFolder write fFolder;
/// the extension of the TSynAngelizeService settings files
// - default is '.service'
property Ext: TFileName
read fExt write fExt;
/// timeout in milliseconds for "http://....." local HTTP requests
// - default is 200
property HttpTimeoutMS: integer
read fHttpTimeoutMS write fHttpTimeoutMS;
/// the local file used to communicate the current (sub-)process state
// from the background running daemon to the command line /list command
// - default is a TemporaryFileName instance
property StateFile: TFileName
read fStateFile write fStateFile;
/// the local file used to communicate from command line
// - currently only the 'reload' command is available from /reload --reload
// - whole feature is disabled if void
// - if no directory is specified, the Folder *.service directory is used
// - default 'cmd' will use a file in this Folder
property CommandFile: TFileName
read fCommandFile write fCommandFile;
/// if set, will generate a StateFile+'.html' content
// - with a HTML page with this text as description, followed by a <table>
// of the current services states
// - could be served e.g. via a local nginx server over Internet (or
// Intranet) to monitor the services state from anywhere in the world
property HtmlStateFileIdentifier: RawUtf8
read fHtmlStateFileIdentifier write fHtmlStateFileIdentifier;
/// how many milliseconds to wait between each TSynAngelizeService "Level"
// - allow each set of services level to actually start its process
property StartDelayMS: integer
read fStartDelayMS write fStartDelayMS;
/// how many seconds a "Level" should wait for all its processes to start
// - default is 30 seconds
// - you can set to 0 to not wait for starting
property StartTimeoutSec: integer
read fStartTimeoutSec write fStartTimeoutSec;
/// STMP server information for optional email notifications
// - expects TSmtpConnection.FromText 'user:password@smtpserver:port' format
property Smtp: RawUtf8
read fSmtp write fSmtp;
/// identify the SMTP sender From: when sending an email
// - expects it to be not '' void - otherwise Smtp is ignored
property SmtpFrom: RawUtf8
read fSmtpFrom write fSmtpFrom;
end;
/// used to serialize the current state of the services
// - as a local temporary binary file, for "agl --list" execution
TSynAngelizeState = packed record
Service: array of record
Name: RawUtf8;
State: TServiceState;
Info: RawUtf8;
end;
end;
/// context enumerate for TSynAngelize internal process
TAglContext = (
acDoStart,
acDoStop,
acDoWatch
);
/// handle internal set of services definitions
{$ifdef USERECORDWITHMETHODS}
TSynAngelizeSet = record
{$else}
TSynAngelizeSet = object
{$endif USERECORDWITHMETHODS}
/// access to the internal services lists, sorted per level and name
Service: array of TSynAngelizeService;
/// the levels used by the services, in increasing order
Levels: TIntegerDynArray;
/// if any service needs actually some watching practice
HasWatchs: boolean;
/// fill the fields from Owner.Settings.Folder files content
function LoadServices(Owner: TSynAngelize): integer;
/// quick check a service from its internal name
function FindService(const ServiceName: RawUtf8): TSynAngelizeService;
/// release all stored data
procedure Done;
end;
/// can run a set of executables as sub-process(es) from *.service definitions
// - agl ("angelize") is an alternative to NSSM / SRVANY / WINSW
// - at OS level, there will be a single agl daemon or service
// - this main agl instance will manage one or several executables as
// sub-process(es), and act as both Launcher and WatchDog
// - in addition to TSynDaemon command line switches, you could use /list
// to retrieve the state of services
TSynAngelize = class(TSynDaemon)
protected
fAdditionalParams: TFileName; // not yet implemented
fServiceClass: TSynAngelizeServiceClass;
fSas: TSynAngelizeSettings; // = fSettings
fExpandLevel: byte;
fServiceStarted: boolean;
fLastUpdateServicesFromSettingsFolder: cardinal;
fSet: TSynAngelizeSet; // Service[] Levels[]
fSectionName: RawUtf8;
fStarted: array of TSynAngelizeService;
fLastGetServicesStateFile: RawByteString;
fWatchThread: TSynBackgroundThreadProcess;
fRunJob: THandle; // a single Windows Job to rule them all
fSmtp: TSmtpConnection;
fCommandFile: TFileName;
// TSynDaemon command line methods
function CustomParseCmd(P: PUtf8Char): boolean; override;
function CustomCommandLineSyntax: string; override;
procedure ClearServicesState;
function LoadServicesState(out state: TSynAngelizeState): boolean;
procedure ListServices;
procedure NewService;
procedure ServiceChangeState(disable: boolean);
procedure StartServices;
procedure WaitStarted(log: TSynLog; level: integer);
procedure StopServices;
procedure StartWatching;
procedure WatchEverySecond(Sender: TSynBackgroundThreadProcess);
procedure ReloadSettings;
procedure StopWatching;
// sub-service support
procedure ComputeServicesStateFiles;
procedure ComputeServicesHtmlFile;
function DoExpand(aService: TSynAngelizeService;
const aInput: TSynAngelizeAction): TSynAngelizeAction; virtual;
procedure DoExpandLookup(aInstance: TObject;
var aProp: RawUtf8; const aID: RawUtf8); virtual;
procedure DoOne(Log: TSynLog; Service: TSynAngelizeService;
Ctxt: TAglContext; const Action: TSynAngelizeAction);
procedure DoWatch(aLog: TSynLog; aService: TSynAngelizeService;
const aAction: TSynAngelizeAction); virtual;
function DoHttpGet(const aUri: RawUtf8): integer;
function DoNotifyByEmail(const aService: TSynAngelizeService;
const aWhat, aEmailTo, aContext: RawUtf8): boolean;
public
/// initialize the main daemon/server redirection instance
// - main TSynAngelizeSettings is loaded
constructor Create(aServiceClass: TSynAngelizeServiceClass = nil;
aLog: TSynLogClass = nil; const aSectionName: RawUtf8 = 'Main';
const aWorkFolder: TFileName = ''; const aSettingsFolder: TFileName = '';
const aLogFolder: TFileName = ''; const aSettingsExt: TFileName = '.settings';
const aSettingsName: TFileName = '';
aSettingsOptions: TSynJsonFileSettingsOptions = []); reintroduce;
/// finalize the stored information
destructor Destroy; override;
/// read and parse all *.service definitions from Settings.Folder
// - e.g. as called by Start overriden method
// - may be called before head to validate the execution settings
// - raise ESynAngelize on invalid settings or dubious StateFile
function LoadServicesFromSettingsFolder: integer;
/// compute a path/action, replacing all %abc% place holders with their values
// - TSystemPath values are available as %CommonData%, %UserData%,
// %CommonDocuments%, %UserDocuments%, %TempFolder% and %Log%
// - %agl.base% is the location of the agl executable
// - %agl.now% is the current local date and time, in a filename compatible format
// - %agl.utc% is the current UTC date and time, in a filename compatible format
// - %agl.params% are the additional parameters supplied to the command line
// - %agl.propname% is the "propname": property value in the main
// TSynAngelizeSettings, e.g. %agl.folder% for location of the *.service files,
// or %agl.logpath% for the/log sub-folder
// - %propname% is the "propname": property value in the .service settings,
// e.g. %run% is the main executable or service name as defined in "Run": "...."
function Expand(aService: TSynAngelizeService; const aAction: TSynAngelizeAction;
aUnQuote: boolean): TSynAngelizeAction;
/// compute a TFileName value, replacing %abc% place holders
function FileNameExpand(const aName: TFileName): TFileName;
/// overriden for proper sub-process starting
procedure Start; override;
/// overriden for proper sub-process stoping
// - should do nothing if the daemon was already stopped
procedure Stop; override;
/// overriden for proper sub-process retry
procedure Resume; override;
end;
implementation
{ ************ TSynAngelizeService Sub-Service Settings and Process }
{ TSynAngelizeRunner }
constructor TSynAngelizeRunner.Create(aSender: TSynAngelize; aLog: TSynLog;
aService: TSynAngelizeService; const aCmd, aEnv, aWrkDir: TFileName;
aRedirect: TFileStreamEx);
begin
fSender := aSender;
fLog := aLog.LogClass;
fService := aService;
fService.fRunnerExitCode := -777;
fService.fRunner := self;
// fService may be set to nil: make a local copy of all RunRedirect() params
fCmd := aCmd;
fEnv := aEnv;
fWrkDir := aWrkDir;
if soWinJobCloseChildren in aService.StartOptions then
include(fRunOptions, roWinJobCloseChildren); // just ignored on POSIX
if not (soReplaceEnv in aService.StartOptions) then
include(fRunOptions, roEnvAddExisting);
fRedirect := aRedirect;
if fRedirect <> nil then
fRedirectFileName := fRedirect.FileName;
fRetryEvent := TSynEvent.Create;
FreeOnTerminate := true;
inherited Create({suspended=}false);
end;
destructor TSynAngelizeRunner.Destroy;
begin
Abort; // ensure fRetryEvent.WaitFor in Execute is released
inherited Destroy;
FreeAndNil(fRedirect);
FreeAndNil(fRetryEvent);
end;
procedure TSynAngelizeRunner.Abort;
begin
if (self = nil) or
fAbortRequested then
exit;
fAbortRequested := true;
fRetryEvent.SetEvent;
end;
procedure TSynAngelizeRunner.RetryNow;
begin
if self <> nil then
fRetryEvent.SetEvent; // unlock WaitFor(pause) below
end;
function ComputePauseSec(tix: Int64; var lastunstable: Int64): integer;
var
min: integer;
begin
result := 2;
if lastunstable = 0 then
lastunstable := tix
else
begin
min := (tix - lastunstable) div MilliSecsPerMin;
if min > 0 then // retry every 2 sec until 1 min
if min < 5 then
result := 15 // retry every 15 sec until 5 min
else if min > 10 then
result := 30 // retry every 30 sec until 10 min
else if min > 30 then
result := 60 // retry every min until 30 min
else if min > 60 then
result := 120 // retry every 2 min until 1 hour
else
result := 240; // retry every 4 min
end;
end;
procedure TSynAngelizeRunner.Execute;
var
log: TSynLog;
pause, err: integer;
tix, start, lastunstable: Int64;
notifytix: boolean;
// some values are copied from fService to avoid most unexpected GPF
timeout: integer; // RetryStableSec
sn: RawUtf8; // Name
se: TIntegerDynArray; // AbortExitCodes
procedure NotifyException(E: Exception);
begin
log.Log(sllWarning, 'Execute % raised %', [sn, PClass(E)^], self);
fService.SetState(ssFailed, '% [%]', [E, E.Message]);
end;
begin
log := fLog.Add;
timeout := fService.RetryStableSec shl MilliSecsPerSecShl;
sn := fService.Name;
se := fService.AbortExitCodes;
notifytix := false;
SetCurrentThreadName('run %', [sn]);
try
lastunstable := 0;
repeat
err := -7777777;
fService.SetState(ssStarting, '%', [fCmd], {resetmessage=}true);
start := GetTickCount64;
if notifytix then
begin
fNotifyStableTix := start + timeout;
notifytix := false;
end
else
fNotifyStableTix := 0;
try
log.Log(sllTrace, 'Execute %: %', [sn, fCmd], self);
// run the command in this thread, calling OnRedirect during execution
RunRedirect(fCmd, @err, OnRedirect, INFINITE, {setresult=}false,
fEnv, fWrkDir, fRunOptions);
// if we reached here, the command was properly finished (or stopped)
fService.SetState(ssStopped, 'ExitCode=%', [err]);
except
on E: Exception do
NotifyException(E);
end;
if Terminated or
(fService = nil) then
break;
fService.fRunnerExitCode := err;
if fAbortRequested then
break;
if (timeout = 0) or
IntegerScanExists(pointer(se), length(se), err) then
begin
// RetryStableSec=0 or AbortExitCodes[] match = no automatic retry
log.Log(sllTrace, 'Execute %: pause forever after ExitCode=%',
[sn, err], self);
if fService <> nil then
begin
fService.DoNotify(doExitNoRetry,
'% exited as % - no retry', [fCmd, err]);
notifytix := true;
fService.SetState(ssPaused, 'Wait for abort or /retry', []);
end;
fRetryEvent.WaitForEver; // will wait for abort or /retry
end
else
begin
// restart the service
tix := GetTickCount64;
if tix - start < timeout then
begin
// it did not last RetryStableSec: seems not stable - pause and retry
pause := ComputePauseSec(tix, lastunstable);
if fService <> nil then
begin
fService.DoNotify(doExitRetry,
'% exited as % - retry in % sec', [fCmd, err, pause]);
notifytix := true;
fService.SetState(ssPaused, 'Wait % sec', [pause]);
end;
pause := pause * 1000 + integer(Random32(pause) * 100);
// add a small random threshold to smoothen several services restart
log.Log(sllTrace, 'Execute %: pause % after ExitCode=%',
[sn, MilliSecToString(pause), err], self);
fRetryEvent.WaitFor(pause);
end
else
begin
// stable for enough time: retry now, and reset increasing pauses
lastunstable := 0;
fService.DoNotify(doExitRetry, '% exited as % - retry now', [fCmd, err]);
notifytix := true;
log.Log(sllTrace, 'Execute %: retry after ExitCode=%', [sn, err], self);
end;
end;
until fAbortRequested or
Terminated;
log.Log(sllTrace, 'Execute %: finished', [sn], self);
except
on E: Exception do
NotifyException(E);
end;
if fService <> nil then
fService.fRunner := nil; // notify ended
log.NotifyThreadEnded; // as needed by TSynLog
end;
procedure TSynAngelizeRunner.PerformRotation;
var
fn: array of TFileName;
n, i, old: PtrInt;
begin
n := fService.RedirectLogRotateFiles;
SetLength(fn, n - 1);
old := 0;
for i := n - 1 downto 1 do
begin
fn[i - 1] := fRedirectFileName + '.' + IntToStr(i);
if soRedirectLogRotateCompress in fService.StartOptions then
fn[i - 1] := fn[i - 1] + '.gz';
if (old = 0) and
FileExists(fn[i - 1]) then
old := i;
end;
if old = n - 1 then
DeleteFile(fn[old - 1]); // delete e.g. 'xxx.9'
for i := n - 2 downto 1 do
RenameFile(fn[i - 1], fn[i]); // e.g. 'xxx.8' -> 'xxx.9'
FreeAndNil(fRedirect);
if soRedirectLogRotateCompress in fService.StartOptions then
// 'xxx' -> 'xxx.1.gz' (libdeflate)
GZFile(fRedirectFileName, fn[0], {complevel=}1, {copydate=}true)
else
// 'xxx' -> 'xxx.1'
RenameFile(fRedirectFileName, fn[0]);
// delete and recreate 'xxx'
DeleteFile(fRedirectFileName);
fRedirect := TFileStreamNoWriteError.CreateAndRenameIfLocked(fRedirectFileName);
end;
function TSynAngelizeRunner.OnRedirect(
const text: RawByteString; pid: cardinal): boolean;
var
i, textstart, textlen: PtrInt;
begin
result := fAbortRequested or Terminated; // return true to quit RunRedirect
if not result and
(fNotifyStableTix <> 0) and
(GetTickCount64 > fNotifyStableTix) and
(fService <> nil) then
begin
fService.DoNotify(doExitRecover, '% recovered', [fCmd]);
fNotifyStableTix := 0;
end;
if text = '' then
begin
// at startup, or idle
if not result and
(fService <> nil) and
(fService.State = ssStarting) then
fService.SetState(ssRunning, 'PID=%', [pid]);
exit;
end;
//fLog.Add.Log(sllTrace, '[%]', text, self);
// handle optional console output redirection to a file
if (fRedirect <> nil) and
(text <> '') then
try
textstart := 0;
textlen := length(text);
if fRedirectSize = 0 then
fRedirectSize := fRedirect.Size
else
inc(fRedirectSize, textlen);
if (fService.RedirectLogRotateFiles <> 0) and
(fRedirectSize > fService.RedirectLogRotateBytes) then
begin
// need to rotate the file(s)
fLog.Add.Log(sllDebug, 'OnRedirect: % file rotation after %',
[fRedirectFileName, KB(fRedirectSize)], self);
for i := textlen downto 1 do
if PByteArray(text)[i - 1] in [10, 13] then
begin
fRedirect.Write(pointer(text)^, i); // write up to last LF
textstart := i;
dec(textlen, i);
break;
end;
PerformRotation;
fRedirectSize := textlen;
if fAbortRequested or Terminated then
result := true; // aborted during rotation
end;
// text output to log file
fRedirect.Write(PByteArray(text)[textstart], textlen);
//TODO: optional TSynLog format with timestamps
except
on E: Exception do
begin
fLog.Add.Log(sllWarning,
'OnRedirect: abort log writing after %', [E], self);
FreeAndNil(fRedirect);
end;
end;
end;
{ TSynAngelizeService }
constructor TSynAngelizeService.Create;
begin
inherited Create;
fWatchDelaySec := 60;
fWatchCountRestart := 2;
fStopRunAbortTimeoutSec := 10;
fRedirectLogRotateBytes := 100 shl 20; // 100MB
fRetryStableSec := 60;
end;
destructor TSynAngelizeService.Destroy;
begin
inherited Destroy;
if fRunner <> nil then
begin
fRunner.fService := nil; // avoid GPF
fRunner.Terminate;
fRunner.Abort; // release and free the thread
end;
end;
procedure TSynAngelizeService.SetState(NewState: TServiceState;
const Fmt: RawUtf8; const Args: array of const; ResetMessage: boolean);
var
msg: RawUtf8;
begin
if self <> nil then
try
fState := NewState;
if ResetMessage then
fStateMessage := '';
FormatUtf8(Fmt, Args, msg);
if msg <> '' then
begin
if fStateMessage <> '' then
fStateMessage := fStateMessage + ', ';
fStateMessage := fStateMessage + msg;
end;
if fOwner <> nil then
begin
fOwner.fSettings.LogClass.Add.Log(
sllTrace, 'SetState(%) [%]', [ToText(NewState)^, msg], self);
fOwner.ComputeServicesStateFiles; // real time notification
end;
except
// so that it is safe to call this method in any context
end;
end;
procedure TSynAngelizeService.DoStart(log: TSynLog);
var
a: PtrInt;
begin
if (fStart = nil) and
(fRun <> '') then
// "Start":[] will assume 'start:%run%'
fOwner.DoOne(log, self, acDoStart, 'start')
else
// execute all "Start":[...,...,...] actions
for a := 0 to high(fStart) do
// any exception on DoOne() should break the starting
fOwner.DoOne(log, self, acDoStart, fStart[a]);
if fWatch <> nil then
fNextWatch := GetTickCount64 + fWatchDelaySec * MilliSecsPerSec;
end;
function TSynAngelizeService.DoStop(log: TSynLog): boolean;
var
a: PtrInt;
errmsg: string;
begin
result := true;
errmsg := '';
if (fStop = nil) and
(fRun <> '') then
try
// "Stop":[] will assume 'stop:%run%'
fOwner.DoOne(log, self, acDoStop, 'stop')
except
on E: Exception do
begin
// any exception should continue the stopping
log.Log(sllWarning, 'StopServices: DoStop(%) failed as %',
[Name, PClass(E)^], self);
FormatString(' raised %: %', [E, E.Message], errmsg);
result := false;
end;
end
else
// execute all "Stop":[...,...,...] actions
for a := 0 to high(fStop) do
try
fOwner.DoOne(log, self, acDoStop, fStop[a]);
except
on E: Exception do
begin
// any exception should continue the stopping
log.Log(sllWarning, 'StopServices: DoStop(%,%) failed as %',
[Name, fStop[a], PClass(E)^], self);
FormatString(' raised %: %', [E, E.Message], errmsg);
result := false;
end;
end;
SetState(ssStopped, 'Shutdown%', [errmsg]);
end;
procedure TSynAngelizeService.DoNotify(What: TDoNotify; const Fmt: RawUtf8;
const Args: array of const);
var
n, w, msg: RawUtf8;
P: PUtf8Char;
fn: TFileName;
http: boolean;
res: integer;
mem: TMemoryInfo;
begin
if (fNotify = '') or
(fOwner = nil) then
exit;
FormatUtf8(Fmt, Args, msg);
if (fLastNotify = What) and
(fLastNotifyMsg = msg) then
exit; // nothing new to notify
fLastNotify := What;
fLastNotifyMsg := msg;
if What = doNothing then
exit; // doNothing can be used to reset the notification message
w := GetEnumNameTrimed(TypeInfo(TDoNotify), ord(What));
P := pointer(fNotify);
while P <> nil do
begin
GetNextItemTrimed(P, ',', n);
if n = '' then
continue;
http := IsHttp(n);
n := StringReplaceAll(n, '%what%', w);
if http then
n := StringReplaceAll(n, '%urimsg%', UrlEncode(msg));
n := fOwner.Expand(self, StringReplaceAll(n, '%msg%', msg), false);
if http then
res := fOwner.DoHttpGet(n)
else if PosExChar('@', n) <> 0 then
res := ord(fOwner.DoNotifyByEmail(self, w, n, msg))
else
begin
Utf8ToFileName(ExtractExecutableName(n), fn);
if FileIsExecutable(fn) then
res := RunCommand(Utf8ToString(n), {waitfor=}true)
else
begin
GetMemoryInfo(mem, false);
res := ord(AppendToTextFile(FormatUtf8('% %: % [mem=%/%] %',
[w, Name, msg, KBNoSpace(mem.memfree), KBNoSpace(mem.memtotal),
RetrieveLoadAvg]), fn));
end;
end;
fOwner.fSettings.LogClass.Add.Log(sllTrace,
'DoNotify % %: % res=%', [w, Name, msg, res], self);
end;
end;
procedure TSynAngelizeService.OnWatchFailed(const Msg: RawUtf8);
var
log: TSynLog;
begin
if fWatchCountRestart > 1 then
begin
inc(fWatchCount);
if fWatchCount < fWatchCountRestart then
exit;
fWatchCount := 0;
end;
SetState(ssFailed, '%', [Msg], {resetmessage=}true);
DoNotify(doWatchFailed, '%', [Msg]);
if fRetryStableSec <> 0 then
begin
log := fOwner.fSettings.LogClass.Add;
log.Log(sllTrace, 'OnWatchFailed [%]: try to restart %', [Msg, Name], self);
if not DoStop(log) then
exit;
SleepHiRes(500); // wait a little for its actual shutdown
try
DoStart(log);
except
on E: Exception do
log.Log(sllDebug, 'OnWatchFailed: DoStart raised %', [PClass(E)^], self);
end;