engine/config.go
Rama main · réplica de lectura del repositorio original.
// Package engine provides the server configuration struct and several functions for serving files over HTTP
package engine
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/rpc"
"os"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/evanw/esbuild/pkg/api"
"github.com/sirupsen/logrus"
"github.com/xyproto/algernon/cachemode"
"github.com/xyproto/algernon/lua/luastate"
"github.com/xyproto/algernon/platformdep"
"github.com/xyproto/algernon/utils"
"github.com/xyproto/datablock"
"github.com/xyproto/env/v2"
"github.com/xyproto/mime"
"github.com/xyproto/pinterface/v2"
"github.com/xyproto/recwatch"
"github.com/xyproto/unzip"
"github.com/xyproto/vt"
)
const (
// Version number. Stable API within major version numbers.
Version = 2.0
// The default supporting filename for a Lua script that provides data to a template
defaultLuaDataFilename = "data.lua"
)
// Config is the main structure for the Algernon server.
// It contains all the state and settings.
// The order of the fields has been decided by the "fieldalignment" utility.
type Config struct {
perm pinterface.IPermissions // the user state, for the permissions system
mimereader *mime.Reader
serverReadyFunctionLua func() // configuration that may only be set in the server configuration script(s)
fs *datablock.FileStat // for checking if file exists, possibly in a cached way
luapool *luastate.Pool // a pool of Lua interpreters
handlerPool *handlerPool // a pool of Lua states for handle() requests
cache *datablock.FileCache
reverseProxyConfig *ReverseProxyConfig
bundleCache *bundleCache // cache for on-the-fly esbuild bundles
dirConfCache *dirConfigCache // cache for parsed .algernon configurations
pluginClients map[string]*rpc.Client // cache of persistent plugin clients
redisAddr string
defaultEventPath string
defaultEventRefresh string
description string // description of the current program
versionString string // program name and version number
defaultOpenExecutable string // default program for opening files and URLs in the current operating system
serverHost string
defaultEventColonPort string
defaultLimitString string // default rate limit, as a string
limitRequestsString string // store the request limit as a string for faster HTTP header creation later on
defaultBoltFilename string // default bolt database file, for some operating systems
defaultLogFile string // default log file, for some operating systems
defaultRedisColonPort string
serverDirOrFilename string // exposed to the server configuration scripts(s)
serverAddr string // exposed to the server configuration scripts(s)
serverConfScript string // exposed to the server configuration scripts(s)
defaultWebColonPort string
serverLogFile string // exposed to the server configuration scripts(s)
serverTempDir string // temporary directory
cookieSecret string // secret to be used when setting and getting user login cookies
defaultTheme string // theme for Markdown and error pages
openExecutable string // open the URL after serving, with a specific executable
serverAddrLua string // configuration that may only be set in the server configuration script(s)
dbName string
serverHeaderName string // used in the HTTP headers as the "Server" name
eventAddr string // for the Server-Sent Event (SSE) server (host and port)
eventRefresh string // for the Server-Sent Event (SSE) server (duration of an event cycle)
luaServerFilename string // if a single Lua file is provided, or if Server() is used
autoRefreshDir string // if only watching a single directory recursively
combinedAccessLogFilename string // CLF access log
commonAccessLogFilename string // NCSA access log
combinedAccessLog *logWriter
commonAccessLog *logWriter
serverLog *logWriter // the --log file, if any
boltFilename string
internalLogFilename string // exposed to the server configuration scripts(s)
mariadbDSN string // connection string
mariaDatabase string // database name
sqliteConnectionString string // SQLite connection string
postgresDSN string // connection string
postgresDatabase string // database name
dirBaseURL string // optional Base URL, for the directory listings
jsxOptions api.TransformOptions // JSX rendering options
serverConfigurationFilenames []string // list of configuration filenames to check
serve ServeConfig
cacheMaxGivenDataSize uint64
largeFileSize uint64 // threshold for not reading large files into memory
bundleCacheMaxMemory uint64 // max memory for bundle cache (0 = unlimited)
refreshDuration time.Duration // for the auto-refresh feature
redisDBindex int
cacheSize uint64
cacheMode cachemode.Setting
shutdownTimeout time.Duration
cacheMaxEntitySize uint64
defaultLimit int64
defaultCacheMaxEntitySize uint64 // 64 KiB
defaultLargeFileSize uint64 // 42 MiB: the default size for when a static file is large enough to not be read into memory
limitRequests int64 // rate limit to this many requests per client per second
handlerPoolSize int // number of Lua states available for handle() request parallelism
writeTimeout uint64 // timeout when writing data to a client, in seconds
defaultStatCacheRefresh time.Duration // refresh the stat cache, if the stat cache feature is enabled
defaultCacheSize uint64 // 1 MiB
pluginClientsMu sync.Mutex
defaultPermissions os.FileMode
quietMode bool // no output to the command line
autoRefresh bool // enable the event server and inject JavaScript to reload pages when sources change
serverMode bool // server mode: non-interactive
productionMode bool // server mode: non-interactive, assume the server is running as a system service
verboseMode bool // server mode: be more verbose
debugMode bool // server mode: enable debug features and better error messages
cacheFileStat bool // assume files will not be removed from the served directories while Algernon is running, which allows caching of costly os.Stat calls
serverAddDomain bool // look for files in the directory with the same name as the requested hostname
stricterHeaders bool // stricter HTTP headers
simpleMode bool // server mode: for serving a directory with files over regular HTTP, nothing more nothing less
openURLAfterServing bool // open the URL after serving
onlyLuaMode bool // if only using the Lua REPL, and not serving anything
quitAfterFirstRequest bool // quit when the first request has been responded to?
markdownMode bool
serveJustQUIC bool // if only QUIC or HTTP/3
serveJustHTTP bool // if only HTTP
serveJustHTTP2 bool // if only HTTP/2
ctrldTwice bool // require a double press of ctrl-d to exit the REPL
noHeaders bool // HTTP headers
redisAddrSpecified bool
noCache bool
showVersion bool
curlSupport bool // support clients like "curl" that downloads uncompressed by default
noBanner bool // don't display the ANSI-graphics banner at start
cacheCompressionSpeed bool // compression speed over compactness
cacheCompression bool
singleFileMode bool // if only serving a single file, like a Lua script
hyperApp bool // convert JSX to HyperApp JS, or React JS?
devMode bool // server mode: aims to make it easy to get started
clearDefaultPathPrefixes bool // clear default path prefixes like "/admin" from the permission system?
disableRateLimiting bool
useBolt bool
useNoDatabase bool // don't use a database. There will be a loss of functionality.
separateEventServer bool // use a dedicated port for the SSE event server
hideDotfiles bool // hide files and directories starting with "."
strictSymlinks bool // don't follow symlinks that lead out of the served directory
serverModeFromCLI bool // --noninteractive / -s was set from the command line
}
// PortSetting describes a single listener endpoint with a protocol and TLS preference
type PortSetting struct {
Addr string // [host]:port
Protocol string // "http", "http2", "http3" (or "quic"), "event"
TLS bool // use TLS?
}
// ServeConfig groups all listener and TLS settings. It is the single source of
// truth for how and on which addresses/ports Algernon serves traffic.
type ServeConfig struct {
httpAddr string // explicit HTTP listen address (from --http-addr or positional)
httpsAddr string // explicit HTTPS listen address (from --https-addr or positional)
serverCert string // exposed to the server configuration scripts(s)
serverKey string // exposed to the server configuration scripts(s)
portSettings []PortSetting // explicit listener configuration (from SetPorts in Lua)
certMagicDomains []string
redirectHTTP bool // redirect HTTP traffic to HTTPS?
useCertMagic bool // use CertMagic and Let's Encrypt for all directories in the given directory that contains a "."
useCertMagicStaging bool // use the Let's Encrypt staging CA instead of the production CA
portConfigFromCLI bool // port configuration was set from flags or positional args
certMagicFromCLI bool // --letsencrypt / -c was set from the command line
redirectFromCLI bool // --redirect was set from the command line
}
// ErrVersion is returned when the initialization quits because all that is done
// is showing version information
var (
ErrVersion = errors.New("only showing version information")
ErrDatabase = errors.New("could not find a usable database backend")
)
// New creates a new server configuration based using the default values
func New(versionString, description string) (*Config, error) {
tmpdir := env.Str("TMPDIR", "/tmp")
ac := &Config{
curlSupport: true,
shutdownTimeout: 10 * time.Second,
// Pool size for parallel Lua handle() requests. One state per CPU by default.
handlerPoolSize: runtime.NumCPU(),
defaultWebColonPort: ":3000",
defaultRedisColonPort: ":6379",
defaultEventColonPort: ":5553",
defaultEventRefresh: "350ms",
defaultEventPath: "/sse",
defaultLimit: 10,
defaultPermissions: 0o660,
defaultCacheSize: 1 * utils.MiB, // 1 MiB
defaultCacheMaxEntitySize: 64 * utils.KiB, // 64 KB
defaultStatCacheRefresh: time.Minute * 1, // Refresh the stat cache, if the stat cache feature is enabled
// Threshold for streaming static files instead of reading them
// into memory. Also caps incoming request body size.
defaultLargeFileSize: 42 * utils.MiB, // 42 MiB
// Default rate limit, as a string
defaultLimitString: strconv.Itoa(10),
// Default Bolt database file, for some operating systems
defaultBoltFilename: filepath.Join(tmpdir, "algernon.db"),
// Default log file, for some operating systems
defaultLogFile: filepath.Join(tmpdir, "algernon.log"),
// List of configuration filenames to check
serverConfigurationFilenames: []string{"/etc/algernon/serverconf.lua", "/etc/algernon/server.lua"},
// Compression speed over compactness
cacheCompressionSpeed: true,
// TODO: Make configurable
// Maximum given file size for caching, 7 MiB
cacheMaxGivenDataSize: 7 * utils.MiB,
// Program for opening URLs, keep empty for using the default in url.go
defaultOpenExecutable: "",
// General information about Algernon
versionString: versionString,
description: description,
// Cache for on-the-fly esbuild bundles
bundleCache: newBundleCache(),
// JSX rendering options
jsxOptions: api.TransformOptions{
Loader: api.LoaderJSX,
MinifyWhitespace: true,
MinifyIdentifiers: true,
MinifySyntax: true,
Charset: api.CharsetUTF8,
},
}
if err := ac.initFilesAndCache(); err != nil {
return nil, err
}
// Read in the mimetype information from the system. Set UTF-8 when setting Content-Type.
ac.mimereader = mime.New("/etc/mime.types", true)
ac.setupLogging()
// File stat cache
ac.fs = datablock.NewFileStat(ac.cacheFileStat, ac.defaultStatCacheRefresh)
return ac, nil
}
// SetFileStatCache can be used to set a different FileStat cache than the default one
func (ac *Config) SetFileStatCache(fs *datablock.FileStat) {
ac.fs = fs
}
// Initialize a temporary directory, handle flags, output version and handle profiling
func (ac *Config) initFilesAndCache() error {
// Temporary directory that might be used for logging, databases or file extraction
serverTempDir, err := os.MkdirTemp("", "algernon")
if err != nil {
return err
}
ac.serverTempDir = serverTempDir
// Set several configuration variables, based on the given flags and arguments
ac.handleFlags(ac.serverTempDir)
// Version (--version)
if ac.showVersion {
if !ac.quietMode {
fmt.Println(ac.versionString)
}
return ErrVersion
}
// CPU and memory profiling, if it is enabled at build time, and one of these
// flags are provided (+ a filename): -cpuprofile, -memprofile, -fgtrace or -trace
traceStart()
// Open the common access log, if specified
if ac.commonAccessLogFilename != "" {
lw, err := openLogWriter(ac.commonAccessLogFilename, defaultLogPermissions)
if err != nil {
return err
}
ac.commonAccessLog = lw
}
// Open the combined access log, if specified
if ac.combinedAccessLogFilename != "" {
lw, err := openLogWriter(ac.combinedAccessLogFilename, defaultLogPermissions)
if err != nil {
return err
}
ac.combinedAccessLog = lw
}
// 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".
ac.cache = datablock.NewFileCache(ac.cacheSize, ac.cacheCompression, ac.cacheMaxEntitySize, ac.cacheCompressionSpeed, ac.cacheMaxGivenDataSize)
return nil
}
func (ac *Config) setupLogging() {
// Log to a file as JSON, if a log file has been specified
if ac.serverLogFile != "" {
lw, errJSONLog := openLogWriter(ac.serverLogFile, defaultLogPermissions)
if errJSONLog != nil {
logrus.Warnf("Could not log to %s: %s", ac.serverLogFile, errJSONLog)
} else {
// Log to the given log filename
ac.serverLog = lw
logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.SetOutput(lw)
}
} else if ac.quietMode {
// If quiet mode is enabled and no log file has been specified, disable logging
logrus.SetOutput(io.Discard)
}
// Close stdout and stderr if quite mode has been enabled
if ac.quietMode {
os.Stdout.Close()
os.Stderr.Close()
}
}
// Close removes the temporary directory
func (ac *Config) Close() {
os.RemoveAll(ac.serverTempDir)
}
// Fatal exit
func (ac *Config) fatalExit(err error) {
// Log to file, if a log file is used
if ac.serverLogFile != "" {
logrus.Error(err)
}
// Then switch to stderr and log the message there as well
logrus.SetOutput(os.Stderr)
// Use the standard formatter
logrus.SetFormatter(&logrus.TextFormatter{})
// Log and exit
logrus.Fatalln(err.Error())
}
// Abrupt exit
func (ac *Config) abruptExit(msg string) {
// Log to file, if a log file is used
if ac.serverLogFile != "" {
logrus.Info(msg)
}
// Then switch to stderr and log the message there as well
logrus.SetOutput(os.Stderr)
// Use the standard formatter
logrus.SetFormatter(&logrus.TextFormatter{})
// Log and exit
logrus.Info(msg)
os.Exit(0)
}
// Quit after a short duration
func (ac *Config) quitSoon(msg string, soon time.Duration) {
time.Sleep(soon)
ac.abruptExit(msg)
}
// Return true of the given file type (extension) should be cached
func (ac *Config) shouldCache(ext string) bool {
switch ac.cacheMode {
case cachemode.On:
return true
case cachemode.Production, cachemode.Small:
switch ext {
case ".amber", ".lua", ".po2", ".pongo2", ".tl", ".tpl":
return false
default:
return true
}
case cachemode.Images:
switch ext {
case ".apng", ".bmp", ".gif", ".ico", ".jpeg", ".jpg", ".png", ".qoi", ".svg", ".webp":
return true
default:
return false
}
case cachemode.Off:
return false
case cachemode.Development, cachemode.Unset:
fallthrough
default:
switch ext {
case ".amber", ".gcss", ".happ", ".js", ".jsx", ".lua", ".md", ".po2", ".pongo2", ".scss", ".ts", ".tsx", ".tl", ".tpl":
return false
default:
return true
}
}
}
// hasHandlers checks if the given filename contains "handle(" or "handle ("
func hasHandlers(fn string) bool {
data, err := os.ReadFile(fn)
return err == nil && (bytes.Contains(data, []byte("handle(")) || bytes.Contains(data, []byte("handle (")))
}
// has checks if a given slice of strings contains a given string
func has(sl []string, e string) bool {
return slices.Contains(sl, e)
}
// unique removes all repeated elements from a slice of strings
func unique(sl []string) []string {
var nl []string
for _, s := range sl {
if !has(nl, s) {
nl = append(nl, s)
}
}
return nl
}
// globalsLuaPath returns the expected location of an optional globals.lua next
// to the served root, or "" if a path cannot be derived.
func globalsLuaPath(serverDirOrFilename string, singleFileMode bool) string {
if serverDirOrFilename == "" {
return ""
}
if singleFileMode {
return filepath.Join(filepath.Dir(serverDirOrFilename), "globals.lua")
}
return filepath.Join(serverDirOrFilename, "globals.lua")
}
// MustServe sets up a server with handlers
func (ac *Config) MustServe(mux *http.ServeMux) error {
var err error
defer ac.Close()
// Output what we are attempting to access and serve
if ac.verboseMode {
logrus.Info("Accessing " + ac.serverDirOrFilename)
}
// Check if the given directory really is a directory
if !ac.fs.IsDir(ac.serverDirOrFilename) {
// It is not a directory
serverFile := ac.serverDirOrFilename
// Return with an error if the file to serve does not exist
if !ac.fs.Exists(serverFile) {
return fmt.Errorf("file does not exist: %s", serverFile)
}
if ac.markdownMode {
// Serve the given Markdown file as a static HTTP server
if serveErr := ac.ServeStaticFile(serverFile, ac.defaultWebColonPort); serveErr != nil {
// Must serve
ac.fatalExit(serveErr)
}
return nil
}
// Switch based on the lowercase filename extension
switch strings.ToLower(filepath.Ext(serverFile)) {
case ".md", ".markdown":
// Serve the given Markdown file as a static HTTP server
if serveErr := ac.ServeStaticFile(serverFile, ac.defaultWebColonPort); serveErr != nil {
// Must serve
ac.fatalExit(serveErr)
}
return nil
case ".zip", ".alg":
// Assume this to be a compressed Algernon application
webApplicationExtractionDir := "/dev/shm" // extract to memory, if possible
testfile := filepath.Join(webApplicationExtractionDir, "canary")
if _, err := os.Create(testfile); err == nil { // success
os.Remove(testfile)
} else {
// Could not create the test file
// Use the server temp dir (typically /tmp) instead of /dev/shm
webApplicationExtractionDir = ac.serverTempDir
}
// Extract the web application
if extractErr := unzip.Extract(serverFile, webApplicationExtractionDir); extractErr != nil {
return extractErr
}
// Use the directory where the file was extracted as the server directory
ac.serverDirOrFilename = webApplicationExtractionDir
// If there is only one directory there, assume it's the
// directory of the newly extracted ZIP file.
if filenames := utils.GetFilenames(ac.serverDirOrFilename); len(filenames) == 1 {
fullPath := filepath.Join(ac.serverDirOrFilename, filenames[0])
if ac.fs.IsDir(fullPath) {
// Use this as the server directory instead
ac.serverDirOrFilename = fullPath
}
}
// If there are server configuration files in the extracted
// directory, register them.
for _, filename := range ac.serverConfigurationFilenames {
configFilename := filepath.Join(ac.serverDirOrFilename, filename)
ac.serverConfigurationFilenames = append(ac.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 ac.serverConfigurationFilenames {
if strings.Count(filepath.ToSlash(filename), "/") == 0 {
// Remove the filename from the slice
ac.serverConfigurationFilenames = append(ac.serverConfigurationFilenames[:i], ac.serverConfigurationFilenames[i+1:]...)
}
}
default:
ac.singleFileMode = true
}
}
// Make a few changes to the defaults if we are serving a single file
if ac.singleFileMode {
ac.serveJustHTTP = true
}
to := vt.NewTextOutput(!platformdep.IsWindows, !ac.quietMode)
// Console output
if !ac.quietMode && !ac.singleFileMode && !ac.simpleMode && !ac.noBanner {
// Output a colorful ansi logo if a proper terminal is available
fmt.Println(Banner(ac.versionString, ac.description))
} else if !ac.quietMode && !ac.singleFileMode {
timestamp := time.Now().Format("2006-01-02 15:04")
to.OutputTags("<cyan>" + ac.versionString + "<darkgray> - " + timestamp + "<off>")
}
// Disable the database backend if the BoltDB filename is the /dev/null file (or OS equivalent)
if ac.boltFilename == os.DevNull {
ac.useNoDatabase = true
}
if !ac.useNoDatabase {
// Connect to a database and retrieve a Permissions struct
ac.perm, err = ac.DatabaseBackend()
if err != nil {
return ErrDatabase
}
}
// Lua LState pool
ac.luapool = luastate.New()
AtShutdown(func() {
ac.luapool.Shutdown()
})
// Auto-detect globals.lua next to the served root and apply it to every
// new pool state. See issue #103.
globalsPath := globalsLuaPath(ac.serverDirOrFilename, ac.singleFileMode)
if globalsPath != "" {
if data, err := os.ReadFile(globalsPath); err == nil {
ac.luapool.SetGlobalsScript(data)
if ac.verboseMode {
logrus.Info("Loaded globals.lua from ", globalsPath)
}
} else if !os.IsNotExist(err) {
logrus.Errorf("globals.lua: %s", err)
}
}
AtShutdown(ac.closePluginClients)
// TODO: save repl history + close luapool + close logs ++ at shutdown
if ac.singleFileMode && (filepath.Ext(ac.serverDirOrFilename) == ".lua" || ac.onlyLuaMode) {
ac.luaServerFilename = ac.serverDirOrFilename
if ac.luaServerFilename == "index.lua" || ac.luaServerFilename == "data.lua" {
// Friendly message to new users
if !hasHandlers(ac.luaServerFilename) {
logrus.Warnf("Found no handlers in %s", ac.luaServerFilename)
logrus.Info("How to implement \"Hello, World!\" in " + ac.luaServerFilename + " file:\n\nhandle(\"/\", function()\n print(\"Hello, World!\")\nend)\n")
}
}
ac.serverDirOrFilename = filepath.Dir(ac.serverDirOrFilename)
// Make it possible to read other files from the Lua script
ac.singleFileMode = false
}
ac.serverConfigurationFilenames = unique(ac.serverConfigurationFilenames)
// Color scheme
arrowColor := "<lightblue>"
filenameColor := "<white>"
luaOutputColor := "<darkgray>"
dashLineColor := "<red>"
// Create a Colorize struct that will not reset colors after colorizing
// strings meant for the terminal.
// c := colorstring.Colorize{Colors: colorstring.DefaultColors, Reset: false}
if (len(ac.serverConfigurationFilenames) > 0) && !ac.quietMode && !ac.onlyLuaMode {
fmt.Println(to.Tags(dashLineColor + strings.Repeat("-", 49) + "<off>"))
}
// Read server configuration script, if present.
// The scripts may change global variables.
var ranConfigurationFilenames []string
for _, filename := range unique(ac.serverConfigurationFilenames) {
if ac.fs.Exists(filename) {
// Dividing line between the banner and output from any of the configuration scripts
if !ac.quietMode && !ac.onlyLuaMode {
// Output the configuration filename
to.Println(arrowColor + "-> " + filenameColor + filename + "<off>")
fmt.Print(to.Tags(luaOutputColor))
} else if ac.verboseMode {
logrus.Info("Running Lua configuration file: " + filename)
}
withHandlerFunctions := true
errConf := ac.RunConfiguration(filename, mux, withHandlerFunctions)
if errConf != nil {
if ac.perm != nil {
logrus.Error("Could not use configuration script: " + filename)
return errConf
}
if ac.verboseMode {
logrus.Info("Skipping " + filename + " because the database backend is not in use.")
}
}
ranConfigurationFilenames = append(ranConfigurationFilenames, filename)
} else {
if ac.verboseMode {
logrus.Info("Looking for: " + filename)
}
}
}
// Only keep the active ones. Used when outputting server information.
ac.serverConfigurationFilenames = ranConfigurationFilenames
// Run the standalone Lua server, if specified
if ac.luaServerFilename != "" {
// Run the Lua server file and set up handlers
if !ac.quietMode && !ac.onlyLuaMode {
// Output the configuration filename
to.Println(arrowColor + "-> " + filenameColor + ac.luaServerFilename + "<off>")
fmt.Print(to.Tags(luaOutputColor))
} else if ac.verboseMode {
fmt.Println("Running Lua configuration file: " + ac.luaServerFilename)
}
withHandlerFunctions := true
errLua := ac.RunConfiguration(ac.luaServerFilename, mux, withHandlerFunctions)
if errLua != nil {
return fmt.Errorf("error in %s (interpreted as a server script):\n%w", ac.luaServerFilename, errLua)
}
} else {
// Register HTTP handler functions
ac.RegisterHandlers(mux, "/", ac.serverDirOrFilename, ac.serverAddDomain)
}
// Register the React 19 endpoints
registerReact19Handlers(mux)
// Register the HMR endpoints when auto-refresh is active
if ac.autoRefresh {
mux.HandleFunc(hmrUpdatePrefix, ac.HMRUpdateHandler)
mux.HandleFunc(hmrRefreshRuntimePath, ac.HMRRefreshRuntimeHandler)
}
// If --eventserver was explicitly provided, use a separate SSE server
ac.separateEventServer = ac.eventAddr != ""
// Set the values that has not been set by flags nor scripts
// (and can be set by both)
ranServerReadyFunction := ac.finalConfiguration(ac.serverHost)
if !ac.quietMode && !ac.onlyLuaMode {
to.Print("<off>")
}
// If no configuration files were being ran successfully,
// output basic server information.
if len(ac.serverConfigurationFilenames) == 0 {
if !ac.quietMode && !ac.onlyLuaMode {
fmt.Println(ac.Info())
}
ranServerReadyFunction = true
}
// Separator between the output of the configuration scripts and
// the rest of the server output.
if ranServerReadyFunction && (len(ac.serverConfigurationFilenames) > 0) && !ac.quietMode && !ac.onlyLuaMode {
to.Tags(dashLineColor + strings.Repeat("-", 49) + "<off>")
}
// Direct internal logging elsewhere. Open for writing so that a newly
// specified filename is created rather than falling through to the
// hard-coded "internal.log" fallback.
internalLogFile, err := os.OpenFile(ac.internalLogFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, ac.defaultPermissions)
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, ac.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 {
ac.fatalExit(fmt.Errorf("could not write to %s nor %s", ac.internalLogFilename, "internal.log"))
}
}
defer internalLogFile.Close()
log.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 ac.autoRefresh {
ac.refreshDuration, err = time.ParseDuration(ac.eventRefresh)
if err != nil {
logrus.Warnf("%s is an invalid duration. Using %s instead.", ac.eventRefresh, ac.defaultEventRefresh)
// Ignore the error, since defaultEventRefresh is a constant and must be parseable
ac.refreshDuration, _ = time.ParseDuration(ac.defaultEventRefresh)
}
recwatch.SetVerbose(ac.verboseMode)
recwatch.LogError = func(err error) {
logrus.Error(err)
}
recwatch.FatalExit = ac.fatalExit
recwatch.Exists = ac.fs.Exists
watchDir := ac.serverDirOrFilename
if ac.autoRefreshDir != "" {
watchDir = ac.autoRefreshDir
}
absdir, err := filepath.Abs(watchDir)
if err != nil {
absdir = watchDir
}
// Mount the SSE handler on the main mux by default (same port, no
// CORS needed). If --eventserver was explicitly provided, start a
// dedicated SSE server on that address instead.
if ac.separateEventServer {
sseScheme := "https"
if ac.serveJustHTTP {
sseScheme = "http"
}
sseAllowed := sseScheme + "://" + ac.serverAddr
recwatch.EventServer(absdir, sseAllowed, ac.eventAddr, ac.defaultEventPath, ac.refreshDuration)
} else {
sseHandler, err := recwatch.EventServerHandler(absdir, "", ac.defaultEventPath, ac.refreshDuration)
if err != nil {
logrus.Error("Could not set up SSE file watcher: ", err)
} else {
mux.Handle(ac.defaultEventPath, sseHandler)
}
}
}
// 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 !ac.serverMode {
// If the REPL uses readline, the SIGWINCH signal is handled there
go ac.REPL(ready, done)
} else {
// Ignore SIGWINCH if we are not going to use a REPL
platformdep.IgnoreTerminalResizeSignal()
}
// Setup a signal handler for clearing the cache when USR1 is received, for some platforms
platformdep.SetupSignals(ac.ClearCache, logrus.Infof)
// Re-open the log files when HUP is received, for log rotation
platformdep.SetupLogRotationSignal(ac.ReopenLogs, logrus.Infof)
// Run the shutdown functions if graceful does not
defer ac.GenerateShutdownFunction(nil)()
// Serve HTTP, HTTP/2 and/or HTTPS
return ac.Serve(mux, done, ready)
}