@@ -18,35 +18,53 @@ import (
18
18
"github.com/1f349/violet/servers/api"
19
19
"github.com/1f349/violet/servers/conf"
20
20
"github.com/1f349/violet/utils"
21
+ "github.com/charmbracelet/log"
22
+ "github.com/cloudflare/tableflip"
21
23
"github.com/google/subcommands"
22
- "github.com/mrmelon54/exit-reload"
23
24
"github.com/prometheus/client_golang/prometheus"
24
25
"github.com/prometheus/client_golang/prometheus/collectors"
25
26
"io/fs"
26
27
"net/http"
27
28
"os"
29
+ "os/signal"
28
30
"path/filepath"
31
+ "syscall"
32
+ "time"
29
33
)
30
34
31
35
type serveCmd struct {
32
36
configPath string
33
- cpuprofile string
37
+ debugLog bool
38
+ pidFile string
34
39
}
35
40
36
41
func (s * serveCmd ) Name () string { return "serve" }
37
42
func (s * serveCmd ) Synopsis () string { return "Serve reverse proxy server" }
38
43
func (s * serveCmd ) SetFlags (f * flag.FlagSet ) {
39
44
f .StringVar (& s .configPath , "conf" , "" , "/path/to/config.json : path to the config file" )
45
+ f .BoolVar (& s .debugLog , "debug" , false , "enable debug logging" )
46
+ f .StringVar (& s .pidFile , "pid-file" , "" , "path to pid file" )
40
47
}
41
48
func (s * serveCmd ) Usage () string {
42
- return `serve [-conf <config file>]
49
+ return `serve [-conf <config file>] [-debug] [-pid-file <pid file>]
43
50
Serve reverse proxy server using information from config file
44
51
`
45
52
}
46
53
47
54
func (s * serveCmd ) Execute (_ context.Context , _ * flag.FlagSet , _ ... interface {}) subcommands.ExitStatus {
55
+ if s .debugLog {
56
+ logger .Logger .SetLevel (log .DebugLevel )
57
+ }
48
58
logger .Logger .Info ("Starting..." )
49
59
60
+ upg , err := tableflip .New (tableflip.Options {
61
+ PIDFile : s .pidFile ,
62
+ })
63
+ if err != nil {
64
+ panic (err )
65
+ }
66
+ defer upg .Stop ()
67
+
50
68
if s .configPath == "" {
51
69
logger .Logger .Info ("Error: config flag is missing" )
52
70
return subcommands .ExitUsageError
@@ -71,13 +89,9 @@ func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{})
71
89
72
90
// working directory is the parent of the config file
73
91
wd := filepath .Dir (s .configPath )
74
- normalLoad (config , wd )
75
- return subcommands .ExitSuccess
76
- }
77
92
78
- func normalLoad (startUp startUpConfig , wd string ) {
79
93
// the cert and key paths are useless in self-signed mode
80
- if ! startUp .SelfSigned {
94
+ if ! config .SelfSigned {
81
95
// create path to cert dir
82
96
err := os .MkdirAll (filepath .Join (wd , "certs" ), os .ModePerm )
83
97
if err != nil {
@@ -92,11 +106,11 @@ func normalLoad(startUp startUpConfig, wd string) {
92
106
93
107
// errorPageDir stores an FS interface for accessing the error page directory
94
108
var errorPageDir fs.FS
95
- if startUp .ErrorPagePath != "" {
96
- errorPageDir = os .DirFS (startUp .ErrorPagePath )
97
- err := os .MkdirAll (startUp .ErrorPagePath , os .ModePerm )
109
+ if config .ErrorPagePath != "" {
110
+ errorPageDir = os .DirFS (config .ErrorPagePath )
111
+ err := os .MkdirAll (config .ErrorPagePath , os .ModePerm )
98
112
if err != nil {
99
- logger .Logger .Fatal ("Failed to create error page" , "path" , startUp .ErrorPagePath )
113
+ logger .Logger .Fatal ("Failed to create error page" , "path" , config .ErrorPagePath )
100
114
}
101
115
}
102
116
@@ -123,75 +137,113 @@ func normalLoad(startUp startUpConfig, wd string) {
123
137
)
124
138
125
139
ws := websocket .NewServer ()
126
- allowedDomains := domains .New (db ) // load allowed domains
127
- acmeChallenges := utils .NewAcmeChallenge () // load acme challenge store
128
- allowedCerts := certs .New (certDir , keyDir , startUp .SelfSigned ) // load certificate manager
129
- hybridTransport := proxy .NewHybridTransport (ws ) // load reverse proxy
130
- dynamicFavicons := favicons .New (db , startUp .InkscapeCmd ) // load dynamic favicon provider
131
- dynamicErrorPages := errorPages .New (errorPageDir ) // load dynamic error page provider
132
- dynamicRouter := router .NewManager (db , hybridTransport ) // load dynamic router manager
140
+ allowedDomains := domains .New (db ) // load allowed domains
141
+ acmeChallenges := utils .NewAcmeChallenge () // load acme challenge store
142
+ allowedCerts := certs .New (certDir , keyDir , config .SelfSigned ) // load certificate manager
143
+ hybridTransport := proxy .NewHybridTransport (ws ) // load reverse proxy
144
+ dynamicFavicons := favicons .New (db , config .InkscapeCmd ) // load dynamic favicon provider
145
+ dynamicErrorPages := errorPages .New (errorPageDir ) // load dynamic error page provider
146
+ dynamicRouter := router .NewManager (db , hybridTransport ) // load dynamic router manager
133
147
134
148
// struct containing config for the http servers
135
149
srvConf := & conf.Conf {
136
- ApiListen : startUp .Listen .Api ,
137
- HttpListen : startUp .Listen .Http ,
138
- HttpsListen : startUp .Listen .Https ,
139
- RateLimit : startUp .RateLimit ,
140
- DB : db ,
141
- Domains : allowedDomains ,
142
- Acme : acmeChallenges ,
143
- Certs : allowedCerts ,
144
- Favicons : dynamicFavicons ,
145
- Signer : mJwtVerify ,
146
- ErrorPages : dynamicErrorPages ,
147
- Router : dynamicRouter ,
150
+ RateLimit : config .RateLimit ,
151
+ DB : db ,
152
+ Domains : allowedDomains ,
153
+ Acme : acmeChallenges ,
154
+ Certs : allowedCerts ,
155
+ Favicons : dynamicFavicons ,
156
+ Signer : mJwtVerify ,
157
+ ErrorPages : dynamicErrorPages ,
158
+ Router : dynamicRouter ,
148
159
}
149
160
150
161
// create the compilable list and run a first time compile
151
162
allCompilables := utils.MultiCompilable {allowedDomains , allowedCerts , dynamicFavicons , dynamicErrorPages , dynamicRouter }
152
163
allCompilables .Compile ()
153
164
165
+ _ , httpsPort , ok := utils .SplitDomainPort (config .Listen .Https , 443 )
166
+ if ! ok {
167
+ httpsPort = 443
168
+ }
169
+
154
170
var srvApi , srvHttp , srvHttps * http.Server
155
- if srvConf .ApiListen != "" {
171
+ if config .Listen .Api != "" {
172
+ // Listen must be called before Ready
173
+ lnApi , err := upg .Listen ("tcp" , config .Listen .Api )
174
+ if err != nil {
175
+ logger .Logger .Fatal ("Listen failed" , "err" , err )
176
+ }
156
177
srvApi = api .NewApiServer (srvConf , allCompilables , promRegistry )
157
178
srvApi .SetKeepAlivesEnabled (false )
158
179
l := logger .Logger .With ("server" , "API" )
159
- l .Info ("Starting server" , "addr" , srvApi . Addr )
160
- go utils .RunBackgroundHttp (l , srvApi )
180
+ l .Info ("Starting server" , "addr" , config . Listen . Api )
181
+ go utils .RunBackgroundHttp (l , srvApi , lnApi )
161
182
}
162
- if srvConf .HttpListen != "" {
163
- srvHttp = servers .NewHttpServer (srvConf , promRegistry )
183
+ if config .Listen .Http != "" {
184
+ // Listen must be called before Ready
185
+ lnHttp , err := upg .Listen ("tcp" , config .Listen .Http )
186
+ if err != nil {
187
+ logger .Logger .Fatal ("Listen failed" , "err" , err )
188
+ }
189
+ srvHttp = servers .NewHttpServer (uint16 (httpsPort ), srvConf , promRegistry )
164
190
srvHttp .SetKeepAlivesEnabled (false )
165
191
l := logger .Logger .With ("server" , "HTTP" )
166
- l .Info ("Starting server" , "addr" , srvHttp . Addr )
167
- go utils .RunBackgroundHttp (l , srvHttp )
192
+ l .Info ("Starting server" , "addr" , config . Listen . Http )
193
+ go utils .RunBackgroundHttp (l , srvHttp , lnHttp )
168
194
}
169
- if srvConf .HttpsListen != "" {
195
+ if config .Listen .Https != "" {
196
+ // Listen must be called before Ready
197
+ lnHttps , err := upg .Listen ("tcp" , config .Listen .Https )
198
+ if err != nil {
199
+ logger .Logger .Fatal ("Listen failed" , "err" , err )
200
+ }
170
201
srvHttps = servers .NewHttpsServer (srvConf , promRegistry )
171
202
srvHttps .SetKeepAlivesEnabled (false )
172
203
l := logger .Logger .With ("server" , "HTTPS" )
173
- l .Info ("Starting server" , "addr" , srvHttps . Addr )
174
- go utils .RunBackgroundHttps (l , srvHttps )
204
+ l .Info ("Starting server" , "addr" , config . Listen . Https )
205
+ go utils .RunBackgroundHttps (l , srvHttps , lnHttps )
175
206
}
176
207
177
- exit_reload .ExitReload ("Violet" , func () {
178
- allCompilables .Compile ()
179
- }, func () {
180
- // stop updating certificates
181
- allowedCerts .Stop ()
208
+ // Do an upgrade on SIGHUP
209
+ go func () {
210
+ sig := make (chan os.Signal , 1 )
211
+ signal .Notify (sig , syscall .SIGHUP )
212
+ for range sig {
213
+ err := upg .Upgrade ()
214
+ if err != nil {
215
+ logger .Logger .Error ("Failed upgrade" , "err" , err )
216
+ }
217
+ }
218
+ }()
182
219
183
- // close websockets first
184
- ws .Shutdown ()
220
+ logger .Logger .Info ("Ready" )
221
+ if err := upg .Ready (); err != nil {
222
+ panic (err )
223
+ }
224
+ <- upg .Exit ()
185
225
186
- // close http servers
187
- if srvApi != nil {
188
- _ = srvApi .Close ()
189
- }
190
- if srvHttp != nil {
191
- _ = srvHttp .Close ()
192
- }
193
- if srvHttps != nil {
194
- _ = srvHttps .Close ()
195
- }
226
+ time .AfterFunc (30 * time .Second , func () {
227
+ logger .Logger .Warn ("Graceful shutdown timed out" )
228
+ os .Exit (1 )
196
229
})
230
+
231
+ // stop updating certificates
232
+ allowedCerts .Stop ()
233
+
234
+ // close websockets first
235
+ ws .Shutdown ()
236
+
237
+ // close http servers
238
+ if srvApi != nil {
239
+ _ = srvApi .Close ()
240
+ }
241
+ if srvHttp != nil {
242
+ _ = srvHttp .Close ()
243
+ }
244
+ if srvHttps != nil {
245
+ _ = srvHttps .Close ()
246
+ }
247
+
248
+ return subcommands .ExitSuccess
197
249
}
0 commit comments