251 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			251 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Go
		
	
	
	
| // Copyright 2019 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package cmd
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	golog "log"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 	"text/tabwriter"
 | |
| 
 | |
| 	"code.gitea.io/gitea/models/db"
 | |
| 	"code.gitea.io/gitea/models/migrations"
 | |
| 	migrate_base "code.gitea.io/gitea/models/migrations/base"
 | |
| 	"code.gitea.io/gitea/modules/doctor"
 | |
| 	"code.gitea.io/gitea/modules/log"
 | |
| 	"code.gitea.io/gitea/modules/setting"
 | |
| 
 | |
| 	"github.com/urfave/cli"
 | |
| 	"xorm.io/xorm"
 | |
| )
 | |
| 
 | |
| // CmdDoctor represents the available doctor sub-command.
 | |
| var CmdDoctor = cli.Command{
 | |
| 	Name:        "doctor",
 | |
| 	Usage:       "Diagnose and optionally fix problems",
 | |
| 	Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
 | |
| 	Action:      runDoctor,
 | |
| 	Flags: []cli.Flag{
 | |
| 		cli.BoolFlag{
 | |
| 			Name:  "list",
 | |
| 			Usage: "List the available checks",
 | |
| 		},
 | |
| 		cli.BoolFlag{
 | |
| 			Name:  "default",
 | |
| 			Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)",
 | |
| 		},
 | |
| 		cli.StringSliceFlag{
 | |
| 			Name:  "run",
 | |
| 			Usage: "Run the provided checks - (if --default is set, the default checks will also run)",
 | |
| 		},
 | |
| 		cli.BoolFlag{
 | |
| 			Name:  "all",
 | |
| 			Usage: "Run all the available checks",
 | |
| 		},
 | |
| 		cli.BoolFlag{
 | |
| 			Name:  "fix",
 | |
| 			Usage: "Automatically fix what we can",
 | |
| 		},
 | |
| 		cli.StringFlag{
 | |
| 			Name:  "log-file",
 | |
| 			Usage: `Name of the log file (default: "doctor.log"). Set to "-" to output to stdout, set to "" to disable`,
 | |
| 		},
 | |
| 		cli.BoolFlag{
 | |
| 			Name:  "color, H",
 | |
| 			Usage: "Use color for outputted information",
 | |
| 		},
 | |
| 	},
 | |
| 	Subcommands: []cli.Command{
 | |
| 		cmdRecreateTable,
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var cmdRecreateTable = cli.Command{
 | |
| 	Name:      "recreate-table",
 | |
| 	Usage:     "Recreate tables from XORM definitions and copy the data.",
 | |
| 	ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)",
 | |
| 	Flags: []cli.Flag{
 | |
| 		cli.BoolFlag{
 | |
| 			Name:  "debug",
 | |
| 			Usage: "Print SQL commands sent",
 | |
| 		},
 | |
| 	},
 | |
| 	Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns.
 | |
| 
 | |
| This command will cause Xorm to recreate tables, copying over the data and deleting the old table.
 | |
| 
 | |
| You should back-up your database before doing this and ensure that your database is up-to-date first.`,
 | |
| 	Action: runRecreateTable,
 | |
| }
 | |
| 
 | |
| func runRecreateTable(ctx *cli.Context) error {
 | |
| 	// Redirect the default golog to here
 | |
| 	golog.SetFlags(0)
 | |
| 	golog.SetPrefix("")
 | |
| 	golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
 | |
| 
 | |
| 	setting.InitProviderFromExistingFile()
 | |
| 	setting.LoadCommonSettings()
 | |
| 	setting.LoadDBSetting()
 | |
| 
 | |
| 	setting.Log.EnableXORMLog = ctx.Bool("debug")
 | |
| 	setting.Database.LogSQL = ctx.Bool("debug")
 | |
| 	// FIXME: don't use CfgProvider directly
 | |
| 	setting.CfgProvider.Section("log").Key("XORM").SetValue(",")
 | |
| 
 | |
| 	setting.InitSQLLog(!ctx.Bool("debug"))
 | |
| 	stdCtx, cancel := installSignals()
 | |
| 	defer cancel()
 | |
| 
 | |
| 	if err := db.InitEngine(stdCtx); err != nil {
 | |
| 		fmt.Println(err)
 | |
| 		fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	args := ctx.Args()
 | |
| 	names := make([]string, 0, ctx.NArg())
 | |
| 	for i := 0; i < ctx.NArg(); i++ {
 | |
| 		names = append(names, args.Get(i))
 | |
| 	}
 | |
| 
 | |
| 	beans, err := db.NamesToBean(names...)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	recreateTables := migrate_base.RecreateTables(beans...)
 | |
| 
 | |
| 	return db.InitEngineWithMigration(stdCtx, func(x *xorm.Engine) error {
 | |
| 		if err := migrations.EnsureUpToDate(x); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		return recreateTables(x)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func setDoctorLogger(ctx *cli.Context) {
 | |
| 	logFile := ctx.String("log-file")
 | |
| 	if !ctx.IsSet("log-file") {
 | |
| 		logFile = "doctor.log"
 | |
| 	}
 | |
| 	colorize := log.CanColorStdout
 | |
| 	if ctx.IsSet("color") {
 | |
| 		colorize = ctx.Bool("color")
 | |
| 	}
 | |
| 
 | |
| 	if len(logFile) == 0 {
 | |
| 		log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"NONE","stacktracelevel":"NONE","colorize":%t}`, colorize))
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	defer func() {
 | |
| 		recovered := recover()
 | |
| 		if recovered == nil {
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		err, ok := recovered.(error)
 | |
| 		if !ok {
 | |
| 			panic(recovered)
 | |
| 		}
 | |
| 		if errors.Is(err, os.ErrPermission) {
 | |
| 			fmt.Fprintf(os.Stderr, "ERROR: Unable to write logs to provided file due to permissions error: %s\n       %v\n", logFile, err)
 | |
| 		} else {
 | |
| 			fmt.Fprintf(os.Stderr, "ERROR: Unable to write logs to provided file: %s\n       %v\n", logFile, err)
 | |
| 		}
 | |
| 		fmt.Fprintf(os.Stderr, "WARN: Logging will be disabled\n       Use `--log-file` to configure log file location\n")
 | |
| 		log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"NONE","stacktracelevel":"NONE","colorize":%t}`, colorize))
 | |
| 	}()
 | |
| 
 | |
| 	if logFile == "-" {
 | |
| 		log.NewLogger(1000, "doctor", "console", fmt.Sprintf(`{"level":"trace","stacktracelevel":"NONE","colorize":%t}`, colorize))
 | |
| 	} else {
 | |
| 		log.NewLogger(1000, "doctor", "file", fmt.Sprintf(`{"filename":%q,"level":"trace","stacktracelevel":"NONE"}`, logFile))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func runDoctor(ctx *cli.Context) error {
 | |
| 	stdCtx, cancel := installSignals()
 | |
| 	defer cancel()
 | |
| 
 | |
| 	// Silence the default loggers
 | |
| 	log.DelNamedLogger("console")
 | |
| 	log.DelNamedLogger(log.DEFAULT)
 | |
| 
 | |
| 	// Now setup our own
 | |
| 	setDoctorLogger(ctx)
 | |
| 
 | |
| 	colorize := log.CanColorStdout
 | |
| 	if ctx.IsSet("color") {
 | |
| 		colorize = ctx.Bool("color")
 | |
| 	}
 | |
| 
 | |
| 	// Finally redirect the default golog to here
 | |
| 	golog.SetFlags(0)
 | |
| 	golog.SetPrefix("")
 | |
| 	golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
 | |
| 
 | |
| 	if ctx.IsSet("list") {
 | |
| 		w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
 | |
| 		_, _ = w.Write([]byte("Default\tName\tTitle\n"))
 | |
| 		for _, check := range doctor.Checks {
 | |
| 			if check.IsDefault {
 | |
| 				_, _ = w.Write([]byte{'*'})
 | |
| 			}
 | |
| 			_, _ = w.Write([]byte{'\t'})
 | |
| 			_, _ = w.Write([]byte(check.Name))
 | |
| 			_, _ = w.Write([]byte{'\t'})
 | |
| 			_, _ = w.Write([]byte(check.Title))
 | |
| 			_, _ = w.Write([]byte{'\n'})
 | |
| 		}
 | |
| 		return w.Flush()
 | |
| 	}
 | |
| 
 | |
| 	var checks []*doctor.Check
 | |
| 	if ctx.Bool("all") {
 | |
| 		checks = doctor.Checks
 | |
| 	} else if ctx.IsSet("run") {
 | |
| 		addDefault := ctx.Bool("default")
 | |
| 		names := ctx.StringSlice("run")
 | |
| 		for i, name := range names {
 | |
| 			names[i] = strings.ToLower(strings.TrimSpace(name))
 | |
| 		}
 | |
| 
 | |
| 		for _, check := range doctor.Checks {
 | |
| 			if addDefault && check.IsDefault {
 | |
| 				checks = append(checks, check)
 | |
| 				continue
 | |
| 			}
 | |
| 			for _, name := range names {
 | |
| 				if name == check.Name {
 | |
| 					checks = append(checks, check)
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		for _, check := range doctor.Checks {
 | |
| 			if check.IsDefault {
 | |
| 				checks = append(checks, check)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Now we can set up our own logger to return information about what the doctor is doing
 | |
| 	if err := log.NewNamedLogger("doctorouter",
 | |
| 		0,
 | |
| 		"console",
 | |
| 		"console",
 | |
| 		fmt.Sprintf(`{"level":"INFO","stacktracelevel":"NONE","colorize":%t,"flags":-1}`, colorize)); err != nil {
 | |
| 		fmt.Println(err)
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	logger := log.GetLogger("doctorouter")
 | |
| 	defer logger.Close()
 | |
| 	return doctor.RunChecks(stdCtx, logger, ctx.Bool("fix"), checks)
 | |
| }
 |