From 2f1353a2f33762e10a304cbebf3a6a8d0381d316 Mon Sep 17 00:00:00 2001
From: zeripath <art27@cantab.net>
Date: Mon, 19 Oct 2020 22:03:08 +0100
Subject: [PATCH] Move install pages out of main macaron routes (#13195)

* Move install pages out of main macaron loop

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Update templates/post-install.tmpl

Co-authored-by: Lauris BH <lauris@nix.lv>

* remove prefetch

Signed-off-by: Andrew Thornton <art27@cantab.net>
---
 cmd/web.go                  | 141 +++++++++++++++++++++++-------------
 cmd/web_graceful.go         |   6 ++
 modules/context/auth.go     |   6 --
 modules/graceful/manager.go |   2 +-
 modules/graceful/server.go  |   2 +-
 routers/init.go             | 120 ++++++++++++++++++------------
 routers/install.go          |  36 ++++++---
 routers/routes/routes.go    |   9 +++
 templates/install.tmpl      |   3 +-
 templates/post-install.tmpl |  24 ++++++
 10 files changed, 234 insertions(+), 115 deletions(-)
 create mode 100644 templates/post-install.tmpl

diff --git a/cmd/web.go b/cmd/web.go
index 92d4b11b69..e16d1afb53 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -19,6 +19,8 @@ import (
 	"code.gitea.io/gitea/routers"
 	"code.gitea.io/gitea/routers/routes"
 
+	"gitea.com/macaron/macaron"
+
 	context2 "github.com/gorilla/context"
 	"github.com/unknwon/com"
 	"github.com/urfave/cli"
@@ -114,6 +116,39 @@ func runWeb(ctx *cli.Context) error {
 		setting.WritePIDFile = true
 	}
 
+	// Flag for port number in case first time run conflict.
+	if ctx.IsSet("port") {
+		if err := setPort(ctx.String("port")); err != nil {
+			return err
+		}
+	}
+
+	// Perform pre-initialization
+	needsInstall := routers.PreInstallInit(graceful.GetManager().HammerContext())
+	if needsInstall {
+		m := routes.NewMacaron()
+		routes.RegisterInstallRoute(m)
+		err := listen(m, false)
+		select {
+		case <-graceful.GetManager().IsShutdown():
+			<-graceful.GetManager().Done()
+			log.Info("PID: %d Gitea Web Finished", os.Getpid())
+			log.Close()
+			return err
+		default:
+		}
+	} else {
+		NoInstallListener()
+	}
+
+	if setting.EnablePprof {
+		go func() {
+			log.Info("Starting pprof server on localhost:6060")
+			log.Info("%v", http.ListenAndServe("localhost:6060", nil))
+		}()
+	}
+
+	log.Info("Global init")
 	// Perform global initialization
 	routers.GlobalInit(graceful.GetManager().HammerContext())
 
@@ -121,41 +156,49 @@ func runWeb(ctx *cli.Context) error {
 	m := routes.NewMacaron()
 	routes.RegisterRoutes(m)
 
-	// Flag for port number in case first time run conflict.
-	if ctx.IsSet("port") {
-		setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, ctx.String("port"), 1)
-		setting.HTTPPort = ctx.String("port")
+	err := listen(m, true)
+	<-graceful.GetManager().Done()
+	log.Info("PID: %d Gitea Web Finished", os.Getpid())
+	log.Close()
+	return err
+}
 
-		switch setting.Protocol {
-		case setting.UnixSocket:
-		case setting.FCGI:
-		case setting.FCGIUnix:
-		default:
-			// Save LOCAL_ROOT_URL if port changed
-			cfg := ini.Empty()
-			if com.IsFile(setting.CustomConf) {
-				// Keeps custom settings if there is already something.
-				if err := cfg.Append(setting.CustomConf); err != nil {
-					return fmt.Errorf("Failed to load custom conf '%s': %v", setting.CustomConf, err)
-				}
-			}
+func setPort(port string) error {
+	setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, port, 1)
+	setting.HTTPPort = port
 
-			defaultLocalURL := string(setting.Protocol) + "://"
-			if setting.HTTPAddr == "0.0.0.0" {
-				defaultLocalURL += "localhost"
-			} else {
-				defaultLocalURL += setting.HTTPAddr
-			}
-			defaultLocalURL += ":" + setting.HTTPPort + "/"
-
-			cfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL)
-
-			if err := cfg.SaveTo(setting.CustomConf); err != nil {
-				return fmt.Errorf("Error saving generated JWT Secret to custom config: %v", err)
+	switch setting.Protocol {
+	case setting.UnixSocket:
+	case setting.FCGI:
+	case setting.FCGIUnix:
+	default:
+		// Save LOCAL_ROOT_URL if port changed
+		cfg := ini.Empty()
+		if com.IsFile(setting.CustomConf) {
+			// Keeps custom settings if there is already something.
+			if err := cfg.Append(setting.CustomConf); err != nil {
+				return fmt.Errorf("Failed to load custom conf '%s': %v", setting.CustomConf, err)
 			}
 		}
-	}
 
+		defaultLocalURL := string(setting.Protocol) + "://"
+		if setting.HTTPAddr == "0.0.0.0" {
+			defaultLocalURL += "localhost"
+		} else {
+			defaultLocalURL += setting.HTTPAddr
+		}
+		defaultLocalURL += ":" + setting.HTTPPort + "/"
+
+		cfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL)
+
+		if err := cfg.SaveTo(setting.CustomConf); err != nil {
+			return fmt.Errorf("Error saving generated JWT Secret to custom config: %v", err)
+		}
+	}
+	return nil
+}
+
+func listen(m *macaron.Macaron, handleRedirector bool) error {
 	listenAddr := setting.HTTPAddr
 	if setting.Protocol != setting.UnixSocket && setting.Protocol != setting.FCGIUnix {
 		listenAddr = net.JoinHostPort(listenAddr, setting.HTTPPort)
@@ -166,37 +209,40 @@ func runWeb(ctx *cli.Context) error {
 		log.Info("LFS server enabled")
 	}
 
-	if setting.EnablePprof {
-		go func() {
-			log.Info("Starting pprof server on localhost:6060")
-			log.Info("%v", http.ListenAndServe("localhost:6060", nil))
-		}()
-	}
-
 	var err error
 	switch setting.Protocol {
 	case setting.HTTP:
-		NoHTTPRedirector()
+		if handleRedirector {
+			NoHTTPRedirector()
+		}
 		err = runHTTP("tcp", listenAddr, context2.ClearHandler(m))
 	case setting.HTTPS:
 		if setting.EnableLetsEncrypt {
 			err = runLetsEncrypt(listenAddr, setting.Domain, setting.LetsEncryptDirectory, setting.LetsEncryptEmail, context2.ClearHandler(m))
 			break
 		}
-		if setting.RedirectOtherPort {
-			go runHTTPRedirector()
-		} else {
-			NoHTTPRedirector()
+		if handleRedirector {
+			if setting.RedirectOtherPort {
+				go runHTTPRedirector()
+			} else {
+				NoHTTPRedirector()
+			}
 		}
 		err = runHTTPS("tcp", listenAddr, setting.CertFile, setting.KeyFile, context2.ClearHandler(m))
 	case setting.FCGI:
-		NoHTTPRedirector()
+		if handleRedirector {
+			NoHTTPRedirector()
+		}
 		err = runFCGI("tcp", listenAddr, context2.ClearHandler(m))
 	case setting.UnixSocket:
-		NoHTTPRedirector()
+		if handleRedirector {
+			NoHTTPRedirector()
+		}
 		err = runHTTP("unix", listenAddr, context2.ClearHandler(m))
 	case setting.FCGIUnix:
-		NoHTTPRedirector()
+		if handleRedirector {
+			NoHTTPRedirector()
+		}
 		err = runFCGI("unix", listenAddr, context2.ClearHandler(m))
 	default:
 		log.Fatal("Invalid protocol: %s", setting.Protocol)
@@ -206,8 +252,5 @@ func runWeb(ctx *cli.Context) error {
 		log.Critical("Failed to start server: %v", err)
 	}
 	log.Info("HTTP Listener: %s Closed", listenAddr)
-	<-graceful.GetManager().Done()
-	log.Info("PID: %d Gitea Web Finished", os.Getpid())
-	log.Close()
-	return nil
+	return err
 }
diff --git a/cmd/web_graceful.go b/cmd/web_graceful.go
index f3c41766af..9e039de699 100644
--- a/cmd/web_graceful.go
+++ b/cmd/web_graceful.go
@@ -37,6 +37,12 @@ func NoMainListener() {
 	graceful.GetManager().InformCleanup()
 }
 
+// NoInstallListener tells our cleanup routine that we will not be using a possibly provided listener
+// for our install HTTP/HTTPS service
+func NoInstallListener() {
+	graceful.GetManager().InformCleanup()
+}
+
 func runFCGI(network, listenAddr string, m http.Handler) error {
 	// This needs to handle stdin as fcgi point
 	fcgiServer := graceful.NewServer(network, listenAddr)
diff --git a/modules/context/auth.go b/modules/context/auth.go
index 00a7032e27..02248384e1 100644
--- a/modules/context/auth.go
+++ b/modules/context/auth.go
@@ -26,12 +26,6 @@ type ToggleOptions struct {
 // Toggle returns toggle options as middleware
 func Toggle(options *ToggleOptions) macaron.Handler {
 	return func(ctx *Context) {
-		// Cannot view any page before installation.
-		if !setting.InstallLock {
-			ctx.Redirect(setting.AppSubURL + "/install")
-			return
-		}
-
 		isAPIPath := auth.IsAPIPath(ctx.Req.URL.Path)
 
 		// Check prohibit login users.
diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go
index 6b134e7d0c..903d05ed21 100644
--- a/modules/graceful/manager.go
+++ b/modules/graceful/manager.go
@@ -31,7 +31,7 @@ const (
 //
 // If you add an additional place you must increment this number
 // and add a function to call manager.InformCleanup if it's not going to be used
-const numberOfServersToCreate = 3
+const numberOfServersToCreate = 4
 
 // Manager represents the graceful server manager interface
 var manager *Manager
diff --git a/modules/graceful/server.go b/modules/graceful/server.go
index db73174ac1..e7394f349e 100644
--- a/modules/graceful/server.go
+++ b/modules/graceful/server.go
@@ -162,7 +162,7 @@ func (srv *Server) Serve(serve ServeFunction) error {
 	srv.setState(stateTerminate)
 	GetManager().ServerDone()
 	// use of closed means that the listeners are closed - i.e. we should be shutting down - return nil
-	if err != nil && strings.Contains(err.Error(), "use of closed") {
+	if err == nil || strings.Contains(err.Error(), "use of closed") || strings.Contains(err.Error(), "http: Server closed") {
 		return nil
 	}
 	return err
diff --git a/routers/init.go b/routers/init.go
index 793033f4a4..702acb7260 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -117,9 +117,46 @@ func InitLocales() {
 	})
 }
 
+// PreInstallInit preloads the configuration to check if we need to run install
+func PreInstallInit(ctx context.Context) bool {
+	setting.NewContext()
+	if !setting.InstallLock {
+		log.Trace("AppPath: %s", setting.AppPath)
+		log.Trace("AppWorkPath: %s", setting.AppWorkPath)
+		log.Trace("Custom path: %s", setting.CustomPath)
+		log.Trace("Log path: %s", setting.LogRootPath)
+		log.Trace("Preparing to run install page")
+		InitLocales()
+		if setting.EnableSQLite3 {
+			log.Info("SQLite3 Supported")
+		}
+		setting.InitDBConfig()
+		svg.Init()
+	}
+
+	return !setting.InstallLock
+}
+
+// PostInstallInit rereads the settings and starts up the database
+func PostInstallInit(ctx context.Context) {
+	setting.NewContext()
+	setting.InitDBConfig()
+	if setting.InstallLock {
+		if err := initDBEngine(ctx); err == nil {
+			log.Info("ORM engine initialization successful!")
+		} else {
+			log.Fatal("ORM engine initialization failed: %v", err)
+		}
+		svg.Init()
+	}
+}
+
 // GlobalInit is for global configuration reload-able.
 func GlobalInit(ctx context.Context) {
 	setting.NewContext()
+	if !setting.InstallLock {
+		log.Fatal("Gitea is not installed")
+	}
 	if err := git.Init(ctx); err != nil {
 		log.Fatal("Git module init failed: %v", err)
 	}
@@ -134,59 +171,50 @@ func GlobalInit(ctx context.Context) {
 
 	NewServices()
 
-	if setting.InstallLock {
-		highlight.NewContext()
-		external.RegisterParsers()
-		markup.Init()
-		if err := initDBEngine(ctx); err == nil {
-			log.Info("ORM engine initialization successful!")
-		} else {
-			log.Fatal("ORM engine initialization failed: %v", err)
-		}
-
-		if err := models.InitOAuth2(); err != nil {
-			log.Fatal("Failed to initialize OAuth2 support: %v", err)
-		}
-
-		models.NewRepoContext()
-
-		// Booting long running goroutines.
-		cron.NewContext()
-		issue_indexer.InitIssueIndexer(false)
-		code_indexer.Init()
-		if err := stats_indexer.Init(); err != nil {
-			log.Fatal("Failed to initialize repository stats indexer queue: %v", err)
-		}
-		mirror_service.InitSyncMirrors()
-		webhook.InitDeliverHooks()
-		if err := pull_service.Init(); err != nil {
-			log.Fatal("Failed to initialize test pull requests queue: %v", err)
-		}
-		if err := task.Init(); err != nil {
-			log.Fatal("Failed to initialize task scheduler: %v", err)
-		}
-		eventsource.GetManager().Init()
+	highlight.NewContext()
+	external.RegisterParsers()
+	markup.Init()
+	if err := initDBEngine(ctx); err == nil {
+		log.Info("ORM engine initialization successful!")
+	} else {
+		log.Fatal("ORM engine initialization failed: %v", err)
 	}
+
+	if err := models.InitOAuth2(); err != nil {
+		log.Fatal("Failed to initialize OAuth2 support: %v", err)
+	}
+
+	models.NewRepoContext()
+
+	// Booting long running goroutines.
+	cron.NewContext()
+	issue_indexer.InitIssueIndexer(false)
+	code_indexer.Init()
+	if err := stats_indexer.Init(); err != nil {
+		log.Fatal("Failed to initialize repository stats indexer queue: %v", err)
+	}
+	mirror_service.InitSyncMirrors()
+	webhook.InitDeliverHooks()
+	if err := pull_service.Init(); err != nil {
+		log.Fatal("Failed to initialize test pull requests queue: %v", err)
+	}
+	if err := task.Init(); err != nil {
+		log.Fatal("Failed to initialize task scheduler: %v", err)
+	}
+	eventsource.GetManager().Init()
+
 	if setting.EnableSQLite3 {
 		log.Info("SQLite3 Supported")
 	}
 	checkRunMode()
 
-	// Now because Install will re-run GlobalInit once it has set InstallLock
-	// we can't tell if the ssh port will remain unused until that's done.
-	// However, see FIXME comment in install.go
-	if setting.InstallLock {
-		if setting.SSH.StartBuiltinServer {
-			ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
-			log.Info("SSH server started on %s:%d. Cipher list (%v), key exchange algorithms (%v), MACs (%v)", setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
-		} else {
-			ssh.Unused()
-		}
-	}
-
-	if setting.InstallLock {
-		sso.Init()
+	if setting.SSH.StartBuiltinServer {
+		ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
+		log.Info("SSH server started on %s:%d. Cipher list (%v), key exchange algorithms (%v), MACs (%v)", setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
+	} else {
+		ssh.Unused()
 	}
+	sso.Init()
 
 	svg.Init()
 }
diff --git a/routers/install.go b/routers/install.go
index a7fe557748..5d0d089dc0 100644
--- a/routers/install.go
+++ b/routers/install.go
@@ -5,7 +5,7 @@
 package routers
 
 import (
-	"errors"
+	"net/http"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -27,13 +27,15 @@ import (
 
 const (
 	// tplInstall template for installation page
-	tplInstall base.TplName = "install"
+	tplInstall     base.TplName = "install"
+	tplPostInstall base.TplName = "post-install"
 )
 
 // InstallInit prepare for rendering installation page
 func InstallInit(ctx *context.Context) {
 	if setting.InstallLock {
-		ctx.NotFound("Install", errors.New("Installation is prohibited"))
+		ctx.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
+		ctx.HTML(200, tplPostInstall)
 		return
 	}
 
@@ -357,7 +359,8 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) {
 		return
 	}
 
-	GlobalInit(graceful.GetManager().HammerContext())
+	// Re-read settings
+	PostInstallInit(ctx.Req.Context())
 
 	// Create admin account
 	if len(form.AdminName) > 0 {
@@ -380,6 +383,11 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) {
 			u, _ = models.GetUserByName(u.Name)
 		}
 
+		days := 86400 * setting.LogInRememberDays
+		ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
+		ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd),
+			setting.CookieRememberName, u.Name, days, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true)
+
 		// Auto-login for admin
 		if err = ctx.Session.Set("uid", u.ID); err != nil {
 			ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
@@ -397,12 +405,18 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) {
 	}
 
 	log.Info("First-time run install finished!")
-	// FIXME: This isn't really enough to completely take account of new configuration
-	// We should really be restarting:
-	// - On windows this is probably just a simple restart
-	// - On linux we can't just use graceful.RestartProcess() everything that was passed in on LISTEN_FDS
-	//   (active or not) needs to be passed out and everything new passed out too.
-	//   This means we need to prevent the cleanup goroutine from running prior to the second GlobalInit
+
 	ctx.Flash.Success(ctx.Tr("install.install_success"))
-	ctx.Redirect(form.AppURL + "user/login")
+
+	ctx.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login")
+	ctx.HTML(200, tplPostInstall)
+
+	// Now get the http.Server from this request and shut it down
+	// NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown
+	srv := ctx.Req.Context().Value(http.ServerContextKey).(*http.Server)
+	go func() {
+		if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil {
+			log.Error("Unable to shutdown the install server! Error: %v", err)
+		}
+	}()
 }
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index 9f7ff277cf..7f43b3b2b1 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -301,6 +301,15 @@ func NewMacaron() *macaron.Macaron {
 	return m
 }
 
+// RegisterInstallRoute registers the install routes
+func RegisterInstallRoute(m *macaron.Macaron) {
+	m.Combo("/", routers.InstallInit).Get(routers.Install).
+		Post(binding.BindIgnErr(auth.InstallForm{}), routers.InstallPost)
+	m.NotFound(func(ctx *context.Context) {
+		ctx.Redirect(setting.AppURL, 302)
+	})
+}
+
 // RegisterRoutes routes routes to Macaron
 func RegisterRoutes(m *macaron.Macaron) {
 	reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
diff --git a/templates/install.tmpl b/templates/install.tmpl
index 27cf1034c5..f0e4680c32 100644
--- a/templates/install.tmpl
+++ b/templates/install.tmpl
@@ -10,7 +10,7 @@
 
 				<p>{{.i18n.Tr "install.docker_helper" "https://docs.gitea.io/en-us/install-with-docker/" | Safe}}</p>
 
-				<form class="ui form" action="{{AppSubUrl}}/install" method="post">
+				<form class="ui form" action="{{AppSubUrl}}/" method="post">
 					<!-- Database Settings -->
 					<h4 class="ui dividing header">{{.i18n.Tr "install.db_title"}}</h4>
 					<p>{{.i18n.Tr "install.requite_db_desc"}}</p>
@@ -307,4 +307,5 @@
 		</div>
 	</div>
 </div>
+<img style="display: none" src="{{StaticUrlPrefix}}/img/loading.png"/>
 {{template "base/footer" .}}
diff --git a/templates/post-install.tmpl b/templates/post-install.tmpl
new file mode 100644
index 0000000000..c7886a3263
--- /dev/null
+++ b/templates/post-install.tmpl
@@ -0,0 +1,24 @@
+{{template "base/head" .}}
+<div class="install">
+    <div class="ui container">
+		<div class="ui grid">
+			<div class="sixteen wide column content">
+				<div class="home">
+					<div class="ui stackable middle very relaxed page grid">
+						<div id="repo_migrating" class="sixteen wide center aligned centered column">
+							<div>
+								<img src="{{StaticUrlPrefix}}/img/loading.png"/>
+							</div>
+						</div>
+					</div>
+					<div class="ui stackable middle very relaxed page grid">
+						<div class="sixteen wide center aligned centered column">
+							<p><a href="{{AppUrl}}user/login">{{AppUrl}}user/login</a></p>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+{{template "base/footer" .}}