-
Notifications
You must be signed in to change notification settings - Fork 0
/
appstart.go
538 lines (509 loc) · 16.9 KB
/
appstart.go
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
package goservicetools
import (
"fmt"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"sync"
"syscall"
configuration "github.com/ilya1st/configuration-go"
)
// Here we will place more complex functions about application start, log setup sockets, etc
// AppStartSetup structure with setup application hooks
// goes to
// SystemSetup starts database connections and same other things
// IAppStartSetup appstart setup for application
// Methods at start are called at following order:
// 1. CommandLineHook add command line reading
// 2. CheckUserConfig add additional config tests
// 3. SystemSetup - prepare own socket listeners etc
// 3. ConfigureHTTPServer configure e.g. mux for http
// 4. SystemStart - stat listen listeners, etc.
// At SIGINT or SIGTERM are called:
// 1. HandleSignal - to determine type of signal and handle them
// 2. SystemShutdown - shutdown listenere
// At SIGUSR1 - system does graceful restart and calls:
// 1. SystemShutdown if there is graceful flag - do not close listeners - just shut down your services
// 2. SetupOwnExtraFiles - setup them here to make restart app with open sockets
type IAppStartSetup interface {
// NeedHTTP does app need http service or not
NeedHTTP() bool
// CommandLineHookadds additional command line flags to global cmdFlags structure
CommandLineHook(cmdFlags map[string]string)
// checks user config parts
CheckUserConfig(mainconf configuration.IConfig) error
// SystemSetup setup other suid ports here, sockets, etc
SystemSetup(graceful bool) error
// SetupOwnExtraFiles for graceful restart - transfer suid ports to graceful child
// for made System setup when suid or graceful restart
// newConfig is full new app config to compare settings
SetupOwnExtraFiles(cmd *exec.Cmd, newConfig configuration.IConfig) error
// HandleSignal handles signal from OS
HandleSignal(sg os.Signal) error
// Set up custom http mux, log, etc
ConfigureHTTPServer(graceful bool) error
// Start custom services - after ports are ready
SystemStart(graceful bool) error
// Run on app shutdown
SystemShutdown(graceful bool) error
}
// DefaultAppStartSetup implements IAppStartSetup
// this is default app start setup and an example on how to write own appstart class
type DefaultAppStartSetup struct {
}
// NeedHTTP implements IAppStartSetup.NeedHTTP() method
func (*DefaultAppStartSetup) NeedHTTP() bool {
return true
}
// CommandLineHook implements IAppStartSetup.CommandLineHook() method
func (*DefaultAppStartSetup) CommandLineHook(cmdFlags map[string]string) {
// do nothing
}
// CheckUserConfig checks user config parts
func (*DefaultAppStartSetup) CheckUserConfig(mainconf configuration.IConfig) error {
return nil
}
// SystemSetup implements IAppStartSetup.SystemSetup() method
func (*DefaultAppStartSetup) SystemSetup(graceful bool) error {
l := GetSystemLogger()
if l != nil {
l.Info().Msg("Running default system startup")
}
return nil
}
// HandleSignal handles signal from OS
func (*DefaultAppStartSetup) HandleSignal(sg os.Signal) error {
// DO NOTHING CAUSE DO NOT NEED HANDLE ANYTHIG
return nil
}
// ConfigureHTTPServer implements IAppStartSetup.ConfigureHTTPServer() method
func (*DefaultAppStartSetup) ConfigureHTTPServer(graceful bool) error {
newMux := http.NewServeMux()
newMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "This is default server mux. See defaultAppStartSetup setting IAppStartSetup in appstart.go file. You can create you one. URI: %v", r.URL.Path)
// Get X-Forwarded-For from header
xfwdfor := r.Header.Get("X-Forwarded-for")
xrealip := r.Header.Get("X-Real-IP")
e := GetHTTPLogger().Info().
Str("ip", r.RemoteAddr)
if xfwdfor != "" {
e = e.Str("x-forwarded-for", xfwdfor)
}
if xrealip != "" {
e = e.Str("x-real-ip", xrealip)
}
e.
Str("method", r.Method).
Str("host", r.Host).
Str("url", r.URL.Path).
Str("agent", r.UserAgent()).
Str("referer", r.Referer()).
Int("code", http.StatusOK).
Msg("request")
})
// TODO: move new mux parameter to appstart
SetHTTPServeMux(newMux)
l := GetSystemLogger()
if l != nil {
l.Info().Msg("Default http server set up")
}
return nil
}
// SystemStart start custom services
func (*DefaultAppStartSetup) SystemStart(graceful bool) error {
return nil
}
// SystemShutdown implements IAppStartSetup.SystemShutdown
func (*DefaultAppStartSetup) SystemShutdown(graceful bool) error {
return nil
}
// SetupOwnExtraFiles for graceful restart
func (*DefaultAppStartSetup) SetupOwnExtraFiles(cmd *exec.Cmd, newConfig configuration.IConfig) error {
/*
place here something like that:
cmd.ExtraFiles =
files = append(cmd.ExtraFiles, <file from you listener>)
cmd.Env = append(cmd.Env, fmt.Sprintf("GRACEFUL_YOUR_SERVICE_FD=%d", 2+len(cmd.ExtraFiles)))
*/
return nil
}
var (
appAppStartSetup IAppStartSetup
appConfigPath string
)
// AppStart app start function
// if graceful - then to app transmitted http socket in fd 3 https in fd 4 etc.
func AppStart(setup IAppStartSetup) (exitCode int, err error) {
// setuid will be also graceful
graceful := os.Getenv("GRACEFUL_START") == "YES"
if setup == nil {
appAppStartSetup = &DefaultAppStartSetup{}
} else {
appAppStartSetup = setup
}
// main environment init
cmdp := GetCommandLineFlags(appAppStartSetup.CommandLineHook)
_env, ok := cmdp["env"]
if ok {
err = ValidateEnv(_env)
if err != nil {
return ExitCodeWrongEnv, fmt.Errorf("%s", err)
}
// init internal environment value
_env, err = GetEnvironment(true, _env)
} else {
if os.Getenv("ENV") == "" {
// by default we think we work with production environment
_env, err = GetEnvironment(true, "prod")
} else {
_env, err = GetEnvironment()
}
}
if nil != err {
return ExitCodeWrongEnv, fmt.Errorf("Environment init error %v", err)
}
// now try load configuration to memory
_config, ok := cmdp["config"]
if !ok {
return ExitCodeConfigError, fmt.Errorf("There is no configuration file in commandline arguments")
}
appConfigPath = _config
// here goes app startup at all. TODO: think AppStartup and AppDown functions
_, err = configuration.GetConfigInstance("main", "HJSON", _config)
if err != nil {
return ExitCodeConfigError, fmt.Errorf("Error occurred while loading configuration.\nConfig file: %s\nError: %s\nExiting", _config, err)
}
conf, err := configuration.GetConfigInstance("main")
if err != nil {
return ExitCodeConfigError, err
}
_env, err = GetEnvironment()
if err != nil {
return ExitCodeConfigError, err
}
err = CheckAppConfig(conf)
if err != nil {
return ExitCodeConfigError, fmt.Errorf("Configuration file error %v", err)
}
workdir, _ := conf.GetStringValue(_env, "workdir")
if workdir != "" {
st, err := os.Stat(workdir)
if err != nil {
return ExitCodeConfigError, fmt.Errorf("Working directory stat error: %v", err)
}
if !st.IsDir() {
return ExitCodeConfigError, fmt.Errorf("Working directory workdir %s is not a directury", workdir)
}
err = os.Chdir(workdir)
if err != nil {
return ExitCodeConfigError, fmt.Errorf("Cannot chdir to Working directory workdir %s, error: %v", workdir, err)
}
}
setuidConf, err := conf.GetSubconfig(_env, "setuid")
if err != nil {
switch err.(type) {
case *configuration.ConfigItemNotFound:
err = nil
default:
}
}
if err != nil {
return ExitCodeConfigError, fmt.Errorf("Application configuration error: %v", err)
}
mainconf, err := conf.GetSubconfig(_env)
if err != nil {
return ExitCodeConfigError, fmt.Errorf("Application configuration error: %v", err)
}
// here we transmit working part of config.
err = appAppStartSetup.CheckUserConfig(mainconf)
if err != nil {
return ExitCodeConfigError, fmt.Errorf("Application configuration error: %v", err)
}
if setuidConf != nil {
setuid, err := setuidConf.GetBooleanValue("setuid")
if err != nil {
return ExitCodeConfigError, fmt.Errorf("Application configuration error: %v", err)
}
if setuid && !graceful { // run all things and graceful stop
err = appAppStartSetup.SystemSetup(false)
if err != nil {
return ExitUserDefinedCodeError, fmt.Errorf(`Error occurred while setting up custom app listeners. look appAppStartSetup.SystemSetup(). Error: %v\nExiting`, err)
}
if appAppStartSetup.NeedHTTP() {
httpConf, _ := conf.GetSubconfig(_env, "http")
err = PrepareHTTPListener(false, httpConf)
}
if err != nil {
return ExitHTTPStartError, fmt.Errorf(`Error occurred while setting up HTTP Listener. Error: %v\nExiting`, err)
}
sd, err := GetSetUIDGIDData(setuidConf)
if err != nil {
return ExitCodeConfigError, fmt.Errorf("Got getting uid and gid error: %v", err)
}
return AppStop(true, sd)
}
}
h, _ := conf.GetSubconfig(_env, "lockfile") // no err check above cause of we use err = CheckAppConfig(conf)
err = SetupLockFile(h)
if err != nil {
return ExitCodeLockfileError, err
}
p, _ := conf.GetSubconfig(_env, "pidfile") // no err check above cause of we use err = CheckAppConfig(conf)
err = SetupPidfile(p)
if err != nil {
return ExitCodeLockfileError, err
}
SetupSighupHandlers()
systemLogConf, _ := conf.GetSubconfig(_env, "logs", "system") // no err check above cause of we use err = CheckAppConfig(conf)
_, err = SetupLog("system", systemLogConf)
if err != nil {
return ExitCodeConfigError, fmt.Errorf(`Error occurred while loading configuration.
Cannot setup system log file.
Error: %v\nExiting`, err)
}
GetSystemLogger().Info().Msg("Application starts. System log ready")
SetupSighupRotationForLogs()
err = appAppStartSetup.SystemSetup(graceful)
// TODO: add here process name
if err != nil {
return ExitUserDefinedCodeError, fmt.Errorf(`Error make system initialization while appAppStartSetup.SystemSetup running: %v`, err)
}
// yes it must be there without errors after config check
if appAppStartSetup.NeedHTTP() {
GetSystemLogger().Info().Msg("Setting up http logs")
httpLogConf, _ := conf.GetSubconfig(_env, "logs", "http") // no err check above cause of we use err = CheckAppConfig(conf)
_, err = SetupLog("http", httpLogConf)
if err != nil {
return ExitCodeConfigError, fmt.Errorf(`Error occurred while loading configuration.
Cannot setup http log file.
Error: %v\nExiting`, err)
}
httpConf, _ := conf.GetSubconfig(_env, "http")
err = PrepareHTTPListener(graceful, httpConf)
if err != nil {
return ExitHTTPStartError, fmt.Errorf(`Error occurred while setting up HTTP Listener. Error: %v\nExiting`, err)
}
addr, _ := httpConf.GetStringValue("address")
GetSystemLogger().Info().Msgf("Http listener ready. address: %v. Setting up HTTP server itself", addr)
SetupHTTPServer(httpConf)
err = appAppStartSetup.ConfigureHTTPServer(graceful)
StartHTTPServer()
GetSystemLogger().Info().Msg("HTTP server started")
}
// starting other than default HTTP custom services
err = appAppStartSetup.SystemStart(graceful)
if err != nil {
return ExitCustomAppError, fmt.Errorf(`Error occurred while starting custom services. Error: %v\nExiting`, err)
}
return 0, nil
}
var appStopMutex sync.Mutex
// AppStop is intended to control app stop while things:
// SIGINT handling, SIGTERM handling, SIGUSR handling for a graceful exit to give socket descriptors
// to new process
// if graceful==true then function tries make graceful restart
// function makes app restart and also makes it's suid start
// NOTICE: if set suid and graceful restart and lower ports are used we assume than you do not change port numbers
// cause we do not have root controller process to supervise lower ports openings
func AppStop(graceful bool, sd *SetuidData) (exitCode int, err error) {
appStopMutex.Lock()
defer appStopMutex.Unlock()
var (
newConfig configuration.IConfig
)
newConfig = nil
conf, err := configuration.GetConfigInstance("main")
if err != nil {
GetSystemLogger().Fatal().Msgf("Config is not set to run %v", err)
}
_env, err := GetEnvironment()
if err != nil {
GetSystemLogger().Fatal().Msgf("Environment is not set to run: %v", err)
}
if graceful {
newConfig, err = configuration.GetConfigInstance(nil, "HJSON", appConfigPath)
if err != nil {
GetSystemLogger().Fatal().Msgf("Can not restart. Application config file was broken: %v", err)
}
err = CheckAppConfig(newConfig)
if err != nil {
GetSystemLogger().Fatal().Msgf("Can not restart. Application config file contains errors: %v", err)
}
mainconf, err := newConfig.GetSubconfig(_env)
if err != nil {
return ExitCodeConfigError, fmt.Errorf("Application configuration error: %v", err)
}
// here we transmit working part of config.
err = appAppStartSetup.CheckUserConfig(mainconf)
if err != nil {
return ExitCodeConfigError, fmt.Errorf("Application configuration error: %v", err)
}
// WHAT IS THE RIGHT WAy HERE?
}
_, err = configuration.GetConfigInstance("main")
if err != nil {
return ExitCodeConfigError, err
}
_, err = GetEnvironment()
if err != nil {
return ExitCodeWrongEnv, err
}
CleanupSighupHandlers()
l := GetSystemLogger()
if l != nil {
l.Info().Msg("Shutting down http")
}
if appAppStartSetup.NeedHTTP() && !graceful {
DropHTTPServer()
if !graceful {
DropHTTPListener()
}
}
err = appAppStartSetup.SystemShutdown(graceful)
if err != nil {
if l == nil {
panic(fmt.Errorf("Error during system shutdown occurred: %v", err))
}
l.Fatal().Msgf("Error during system shutdown occurred: %v", err)
}
DropLogger("http")
if !graceful {
DropLogger("system")
}
DropLockFile()
DropPidfile()
if graceful {
cmd := exec.Command(os.Args[0], os.Args[1:]...)
cmd.ExtraFiles = []*os.File{}
cmd.Env = os.Environ()
if appAppStartSetup.NeedHTTP() {
oldAddr, err := conf.GetStringValue(_env, "http", "address")
if err != nil {
if l == nil {
panic(fmt.Errorf("AppStop() restart: No address in the config"))
}
l.Fatal().Msg("AppStop() restart: No address in the config")
}
oldSocketType, err := conf.GetStringValue(_env, "http", "socket_type")
if err != nil {
if l == nil {
panic(fmt.Errorf("AppStop() restart: No addsocket_type in the config"))
}
l.Fatal().Msg("AppStop() restart: No addsocket_type in the config")
}
newAddr, err := newConfig.GetStringValue(_env, "http", "address")
if err != nil {
if l == nil {
panic(fmt.Errorf("AppStop() restart: No address in the new config"))
}
l.Fatal().Msg("AppStop() restart: No address in the new config")
}
newSocketType, err := newConfig.GetStringValue(_env, "http", "socket_type")
if err != nil {
if l == nil {
panic(fmt.Errorf("AppStop() restart: No addsocket_type in the new config"))
}
l.Fatal().Msg("AppStop() restart: No addsocket_type in the new config")
}
if oldAddr == newAddr && oldSocketType == newSocketType {
li := GetHTTPListener()
if li == nil {
if l == nil {
panic(fmt.Errorf("HTTP listener nil"))
}
l.Fatal().Msgf("HTTP listener nil")
}
var f *os.File
switch v := li.(type) {
case *net.TCPListener:
f, err = v.File()
case *net.UnixListener:
f, err = v.File()
default:
if l == nil {
panic(fmt.Errorf("Wrong tcp or unix listener"))
}
l.Fatal().Msgf("Wrong tcp or unix listener")
}
if err != nil {
return 0, err
}
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
cmd.Env = append(cmd.Env, fmt.Sprintf("GRACEFUL_HTTP_FD=%d", 2+len(cmd.ExtraFiles)))
}
}
cmd.Env = append(cmd.Env, "GRACEFUL_START=YES")
if sd != nil {
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
Credential: &syscall.Credential{
Uid: sd.uid,
Gid: sd.gid,
},
}
}
err := appAppStartSetup.SetupOwnExtraFiles(cmd, newConfig)
if err != nil {
if l == nil {
panic(fmt.Errorf("custom appstart error: %v", err))
}
l.Fatal().Msgf("custom appstart error: %v", err)
}
if l != nil {
l.Info().Msg("Graceful application restart")
}
DropLogger("system")
if err := cmd.Start(); err != nil {
return 0, err
}
fmt.Printf("Spawned process %d, exiting\n", cmd.Process.Pid)
cmd.Process.Release()
os.Exit(0)
}
return 0, nil
}
var appRunChan chan os.Signal
var appRunMutex sync.Mutex
// AppRun just to run app when we all do
// here is no way cover with tests cause need manually test them or make integration tests there
func AppRun() {
appRunChan = make(chan os.Signal, 1)
signal.Notify(appRunChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
goloop:
for {
sg, ok := <-appRunChan
appRunMutex.Lock()
if !ok {
sighupMutex.Unlock()
break goloop
}
switch sg {
case syscall.SIGINT:
fallthrough
case syscall.SIGTERM:
appAppStartSetup.HandleSignal(sg)
exitCode, err := AppStop(false, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Error while app shutdown occurred: %v", err)
}
Exit(exitCode)
case syscall.SIGUSR1:
appAppStartSetup.HandleSignal(sg)
exitCode, err := AppStop(true, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Error while app shutdown occurred: %v", err)
Exit(exitCode)
}
Exit(exitCode)
default:
}
appRunMutex.Unlock()
}
}
func init() {
appAppStartSetup = nil
appConfigPath = ""
}