-
-
Notifications
You must be signed in to change notification settings - Fork 138
/
main.go
330 lines (292 loc) · 9.81 KB
/
main.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
// HTTP/2 web server with built-in support for Lua, Markdown, GCSS, Amber and JSX.
package main
import (
"errors"
"fmt"
log "github.com/sirupsen/logrus"
"io/ioutil"
internallog "log"
"net/http"
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"strings"
"time"
"github.com/xyproto/unzip"
"github.com/yuin/gopher-lua"
)
const (
versionString = "Algernon 0.84"
description = "HTTP/2 Web Server"
)
var (
// For convenience. Set in the main function.
serverHost string
dbName string
refreshDuration time.Duration
fs *FileStat
)
func main() {
var err error
// Will be default in Go 1.5
runtime.GOMAXPROCS(runtime.NumCPU())
// Temporary directory that might be used for logging, databases or file extraction
serverTempDir, err := ioutil.TempDir("", "algernon")
if err != nil {
log.Fatalln(err)
}
defer os.RemoveAll(serverTempDir)
// Set several configuration variables, based on the given flags and arguments
serverHost = handleFlags(serverTempDir)
// Version
if showVersion {
if !quietMode {
fmt.Println(versionString)
}
os.Exit(0)
}
// Console output
if !quietMode {
fmt.Println(banner())
}
// CPU profiling
if profileCPU != "" {
f, err := os.Create(profileCPU)
if err != nil {
log.Fatal(err)
}
go func() {
log.Info("Profiling CPU usage")
pprof.StartCPUProfile(f)
}()
atShutdown(func() {
pprof.StopCPUProfile()
log.Info("Done profiling CPU usage")
})
}
// Memory profiling at server shutdown
if profileMem != "" {
atShutdown(func() {
f, err := os.Create(profileMem)
defer f.Close()
if err != nil {
log.Fatal(err)
}
log.Info("Saving heap profile to ", profileMem)
pprof.WriteHeapProfile(f)
})
}
// Dividing line between the banner and output from any of the configuration scripts
if len(serverConfigurationFilenames) > 0 && !quietMode {
fmt.Println("--------------------------------------- - - · ·")
}
// Request handlers
mux := http.NewServeMux()
// Read mime data from the system, if available
initializeMime()
// Create a new FileStat struct, with optional caching (for speed).
// Clear the cache every 10 minutes.
fs = NewFileStat(cacheFileStat, time.Minute*10)
// Check if the given directory really is a directory
if !fs.isDir(serverDir) {
// Possibly a file
filename := serverDir
// Check if the file exists
if fs.exists(filename) {
// Switch based on the lowercase filename extension
switch strings.ToLower(filepath.Ext(filename)) {
case ".md", ".markdown":
// Serve the given Markdown file as a static HTTP server
serveStaticFile(filename, defaultWebColonPort)
return
case ".zip", ".alg":
// Assume this to be a compressed Algernon application
err := unzip.Extract(filename, serverTempDir)
if err != nil {
log.Fatalln(err)
}
// Use the directory where the file was extracted as the server directory
serverDir = serverTempDir
// If there is only one directory there, assume it's the
// directory of the newly extracted ZIP file.
if filenames := getFilenames(serverDir); len(filenames) == 1 {
fullPath := filepath.Join(serverDir, filenames[0])
if fs.isDir(fullPath) {
// Use this as the server directory instead
serverDir = fullPath
}
}
// If there are server configuration files in the extracted
// directory, register them.
for _, filename := range serverConfigurationFilenames {
configFilename := filepath.Join(serverDir, filename)
if fs.exists(configFilename) {
serverConfigurationFilenames = append(serverConfigurationFilenames, configFilename)
}
}
// Disregard all configuration files from the current directory
// (filenames without a path separator), since we are serving a
// ZIP file.
for i, filename := range serverConfigurationFilenames {
if strings.Count(filepath.ToSlash(filename), "/") == 0 {
// Remove the filename from the slice
serverConfigurationFilenames = append(serverConfigurationFilenames[:i], serverConfigurationFilenames[i+1:]...)
}
}
default:
singleFileMode = true
}
} else {
fatalExit(errors.New("File does not exist: " + filename))
}
}
// Make a few changes to the defaults if we are serving a single file
if singleFileMode {
debugMode = true
serveJustHTTP = true
}
// Connect to a database and retrieve a Permissions struct
perm := mustAquirePermissions()
// Lua LState pool
luapool := &lStatePool{saved: make([]*lua.LState, 0, 4)}
atShutdown(func() {
luapool.Shutdown()
})
// TODO: save repl history + close luapool + close logs ++ at shutdown
// Create a cache struct for reading files (contains functions that can
// be used for reading files, also when caching is disabled).
// The final argument is for compressing with "fast" instead of "best".
cache := newFileCache(cacheSize, cacheCompression, cacheMaxEntitySize)
if singleFileMode && filepath.Ext(serverDir) == ".lua" {
luaServerFilename = serverDir
if luaServerFilename == "index.lua" || luaServerFilename == "data.lua" {
log.Warn("Using " + luaServerFilename + " as a standalone server!\nYou might wish to serve a directory instead.")
}
serverDir = filepath.Dir(serverDir)
singleFileMode = false
}
// Log to a file as JSON, if a log file has been specified
if serverLogFile != "" {
f, err := os.OpenFile(serverLogFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, defaultPermissions)
if err != nil {
log.Error("Could not log to", serverLogFile)
fatalExit(err)
}
log.SetFormatter(&log.JSONFormatter{})
log.SetOutput(f)
} else if quietMode {
// If quiet mode is enabled and no log file has been specified, disable logging
log.SetOutput(ioutil.Discard)
}
if quietMode {
os.Stdout.Close()
os.Stderr.Close()
}
// Read server configuration script, if present.
// The scripts may change global variables.
var ranConfigurationFilenames []string
for _, filename := range serverConfigurationFilenames {
if fs.exists(filename) {
if verboseMode {
fmt.Println("Running configuration file: " + filename)
}
if err := runConfiguration(filename, perm, luapool, cache, mux, false); err != nil {
log.Error("Could not use configuration script: " + filename)
fatalExit(err)
}
ranConfigurationFilenames = append(ranConfigurationFilenames, filename)
}
}
// Only keep the active ones. Used when outputting server information.
serverConfigurationFilenames = ranConfigurationFilenames
// Run the standalone Lua server, if specified
if luaServerFilename != "" {
// Run the Lua server file and set up handlers
if verboseMode {
fmt.Println("Running Lua Server File")
}
if err := runConfiguration(luaServerFilename, perm, luapool, cache, mux, true); err != nil {
log.Error("Error in Lua server script: " + luaServerFilename)
fatalExit(err)
}
} else {
// Register HTTP handler functions
registerHandlers(mux, "/", serverDir, perm, luapool, cache, serverAddDomain)
}
// Set the values that has not been set by flags nor scripts
// (and can be set by both)
ranServerReadyFunction := finalConfiguration(serverHost)
// If no configuration files were being ran succesfully,
// output basic server information.
if len(serverConfigurationFilenames) == 0 {
if !quietMode {
fmt.Println(serverInfo())
}
ranServerReadyFunction = true
}
// Dividing line between the banner and output from any of the
// configuration scripts. Marks the end of the configuration output.
if ranServerReadyFunction && !quietMode {
fmt.Println("--------------------------------------- - - · ·")
}
// Direct internal logging elsewhere
internalLogFile, err := os.Open(internalLogFilename)
defer internalLogFile.Close()
if err != nil {
// Could not open the internalLogFilename filename, try using another filename
internalLogFile, err = os.OpenFile("internal.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, defaultPermissions)
atShutdown(func() {
// TODO This one is is special and should be closed after the other shutdown functions.
// Set up a "done" channel instead of sleeping.
time.Sleep(100 * time.Millisecond)
internalLogFile.Close()
})
if err != nil {
fatalExit(fmt.Errorf("Could not write to %s nor %s.", internalLogFilename, "internal.log"))
}
}
internallog.SetOutput(internalLogFile)
// Serve filesystem events in the background.
// Used for reloading pages when the sources change.
// Can also be used when serving a single file.
if autoRefreshMode {
refreshDuration, err = time.ParseDuration(eventRefresh)
if err != nil {
log.Warn(fmt.Sprintf("%s is an invalid duration. Using %s instead.", eventRefresh, defaultEventRefresh))
// Ignore the error, since defaultEventRefresh is a constant and must be parseable
refreshDuration, _ = time.ParseDuration(defaultEventRefresh)
}
if autoRefreshDir != "" {
// Only watch the autoRefreshDir, recursively
EventServer(eventAddr, defaultEventPath, autoRefreshDir, refreshDuration, "*")
} else {
// Watch everything in the server directory, recursively
EventServer(eventAddr, defaultEventPath, serverDir, refreshDuration, "*")
}
}
// For communicating to and from the REPL
ready := make(chan bool) // for when the server is up and running
done := make(chan bool) // for when the user wish to quit the server
// The Lua REPL
if !serverMode {
go REPL(perm, luapool, cache, ready, done)
}
conf := &algernonServerConfig{
productionMode: productionMode,
serverHost: serverHost,
serverAddr: serverAddr,
serverCert: serverCert,
serverKey: serverKey,
serveJustHTTP: serveJustHTTP,
serveJustHTTP2: serveJustHTTP2,
shutdownTimeout: 10 * time.Second,
internalLogFilename: internalLogFilename,
}
// Run the shutdown functions if graceful does not
defer runShutdown()
// Serve HTTP, HTTP/2 and/or HTTPS
if err := serve(conf, mux, done, ready); err != nil {
fatalExit(err)
}
}