// Command stepd provides an HTTP server that automatically executes STEP files. package main import "os" import "log" import "time" import "slices" import "errors" import "context" import "net/http" import "path/filepath" import "git.tebibyte.media/sashakoshka/step" import "git.tebibyte.media/sashakoshka/go-cli" import "git.tebibyte.media/sashakoshka/step/providers" import "git.tebibyte.media/sashakoshka/goutil/container" import "git.tebibyte.media/sashakoshka/go-service/daemon" import stephttp"git.tebibyte.media/sashakoshka/step/http" import "git.tebibyte.media/sashakoshka/go-service/routines" const configFileName = "step.meta" func main () { // parse command line arguments flagPidFile := cli.NewInputFlag ( 'p', "pid-file", "Write the PID to the specified file.", "", cli.ValString) flagHTTPAddress := cli.NewInputFlag ( 'h', "http-address", "The address to host the HTTP server on.", ":8080", cli.ValString) flagHTTPErrorDocument := cli.NewInputFlag ( 0, "http-error-document", "The document to use for displaying http errors.", "", cli.ValString) flagDirectories := cli.NewFlag ( 'd', "directories", "Serve the contents of directories.") cmd := cli.New ( "Run an HTTP server that automaticaly executes STEP files.", flagPidFile, flagHTTPAddress, flagHTTPErrorDocument, flagDirectories, cli.NewHelp()) cmd.Syntax = "[OPTIONS]... [DIRECTORY]" cmd.ParseOrExit(os.Args) log.Println(`==========| STEP |===========`) log.Println(`Scriptable Template Processor`) log.Println(`... initializing`) // manage start and end of program ctx, done := context.WithCancel(context.Background()) defer done() daemon.OnSigint(done) pidFileAbs, err := filepath.Abs(flagPidFile.Value) if err != nil { log.Fatalln("XXX", err) } pidFile := daemon.PidFile(pidFileAbs) if !pidFile.Empty() { err := pidFile.Start() if err != nil { log.Println("!!! could not write pid:", err) } defer func () { err := pidFile.Close() if err != nil { log.Println("!!! could not delete pidfile:", err) } } () } // the single argument is for the directory to serve. we actually cd // there. if len(cmd.Args) == 1 { err := os.Chdir(cmd.Args[0]) if err != nil { log.Fatalln("XXX", err) } } else if len(cmd.Args) > 1 { cmd.Usage() os.Exit(1) } // read the config file var config step.Meta if configFile, err := os.Open(configFileName); err == nil { defer configFile.Close() config, err = step.DecodeMeta(configFile) configFile.Close() if err != nil { log.Fatalln("XXX", err) } } else { config = make(step.Meta) log.Printf ( "(i) could not open %s, using default configuration", configFileName) } // override the config file with explicitly specified options if flagHTTPAddress.Value != "" { config.Set("http.address", flagHTTPAddress.Value) } if flagHTTPErrorDocument.Value != "" { config.Set("http.error-document", flagHTTPErrorDocument.Value) } if flagDirectories.Value != "" { config.Set("http.serve-directories", flagDirectories.Value) } // set up the environment environment := step.Environment { Config: config, } environment.Providers = providers.All() err = environment.Init(context.Background()) if err != nil { log.Fatal(err) } // set up the HTTP handler handler := stephttp.Handler { Environment: &environment, Directories: config.Get("http.serve-directories") == "true", StepExt: ucontainer.NewSet(slices.Clone(config["http.step-extension"])...), Index: slices.Clone(config["http.index-file"]), ErrorDocument: config.Get("http.error-document"), DenyAll: ucontainer.NewSet(configFileName), } if len(handler.StepExt) == 0 { handler.StepExt.Add(".step") } if len(handler.Index) == 0 { handler.Index = []string { "index.step", "index.html", "index" } } // set up the HTTP server httpServer := http.Server { Addr: config.Get("http.address"), Handler: &handler, } httpServerRoutine := httpServerRoutine(httpServer) // set up the routine manager manager := routines.Manager { Routines: []routines.Routine { &httpServerRoutine, }, } log.Println(`.// initialized.`) log.Printf("(i) listening on %s\n", httpServer.Addr) if err := manager.Run(ctx); err != nil && !errors.Is(err, context.Canceled) { log.Fatalln("XXX", err) } } type httpServerRoutine http.Server func (this *httpServerRoutine) Run (ctx context.Context) error { ctx, done := context.WithCancel(ctx) defer done() server := http.Server(*this) go func () { <- ctx.Done() shutdownCtx, _ := context.WithTimeout ( context.Background(), 16 * time.Second) server.Shutdown(shutdownCtx) } () err := server.ListenAndServe() if ctx.Err() != nil { return ctx.Err() } return err }