You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

doctor.go 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package cmd
  5. import (
  6. "bufio"
  7. "bytes"
  8. "context"
  9. "fmt"
  10. "io/ioutil"
  11. golog "log"
  12. "os"
  13. "os/exec"
  14. "path/filepath"
  15. "strings"
  16. "text/tabwriter"
  17. "code.gitea.io/gitea/models"
  18. "code.gitea.io/gitea/models/migrations"
  19. "code.gitea.io/gitea/modules/git"
  20. "code.gitea.io/gitea/modules/log"
  21. "code.gitea.io/gitea/modules/options"
  22. "code.gitea.io/gitea/modules/setting"
  23. "xorm.io/builder"
  24. "github.com/urfave/cli"
  25. )
  26. // CmdDoctor represents the available doctor sub-command.
  27. var CmdDoctor = cli.Command{
  28. Name: "doctor",
  29. Usage: "Diagnose problems",
  30. Description: "A command to diagnose problems with the current Gitea instance according to the given configuration.",
  31. Action: runDoctor,
  32. Flags: []cli.Flag{
  33. cli.BoolFlag{
  34. Name: "list",
  35. Usage: "List the available checks",
  36. },
  37. cli.BoolFlag{
  38. Name: "default",
  39. Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)",
  40. },
  41. cli.StringSliceFlag{
  42. Name: "run",
  43. Usage: "Run the provided checks - (if --default is set, the default checks will also run)",
  44. },
  45. cli.BoolFlag{
  46. Name: "all",
  47. Usage: "Run all the available checks",
  48. },
  49. cli.BoolFlag{
  50. Name: "fix",
  51. Usage: "Automatically fix what we can",
  52. },
  53. cli.StringFlag{
  54. Name: "log-file",
  55. Usage: `Name of the log file (default: "doctor.log"). Set to "-" to output to stdout, set to "" to disable`,
  56. },
  57. },
  58. }
  59. type check struct {
  60. title string
  61. name string
  62. isDefault bool
  63. f func(ctx *cli.Context) ([]string, error)
  64. abortIfFailed bool
  65. skipDatabaseInit bool
  66. }
  67. // checklist represents list for all checks
  68. var checklist = []check{
  69. {
  70. // NOTE: this check should be the first in the list
  71. title: "Check paths and basic configuration",
  72. name: "paths",
  73. isDefault: true,
  74. f: runDoctorPathInfo,
  75. abortIfFailed: true,
  76. skipDatabaseInit: true,
  77. },
  78. {
  79. title: "Check Database Version",
  80. name: "check-db-version",
  81. isDefault: true,
  82. f: runDoctorCheckDBVersion,
  83. abortIfFailed: true,
  84. },
  85. {
  86. title: "Check if OpenSSH authorized_keys file is up-to-date",
  87. name: "authorized_keys",
  88. isDefault: true,
  89. f: runDoctorAuthorizedKeys,
  90. },
  91. {
  92. title: "Check if SCRIPT_TYPE is available",
  93. name: "script-type",
  94. isDefault: false,
  95. f: runDoctorScriptType,
  96. },
  97. {
  98. title: "Check if hook files are up-to-date and executable",
  99. name: "hooks",
  100. isDefault: false,
  101. f: runDoctorHooks,
  102. },
  103. {
  104. title: "Recalculate merge bases",
  105. name: "recalculate_merge_bases",
  106. isDefault: false,
  107. f: runDoctorPRMergeBase,
  108. },
  109. {
  110. title: "Check consistency of database",
  111. name: "check-db-consistency",
  112. isDefault: true,
  113. f: runDoctorCheckDBConsistency,
  114. },
  115. // more checks please append here
  116. }
  117. func runDoctor(ctx *cli.Context) error {
  118. // Silence the default loggers
  119. log.DelNamedLogger("console")
  120. log.DelNamedLogger(log.DEFAULT)
  121. // Now setup our own
  122. logFile := ctx.String("log-file")
  123. if !ctx.IsSet("log-file") {
  124. logFile = "doctor.log"
  125. }
  126. if len(logFile) == 0 {
  127. log.NewLogger(1000, "doctor", "console", `{"level":"NONE","stacktracelevel":"NONE","colorize":"%t"}`)
  128. } else if logFile == "-" {
  129. log.NewLogger(1000, "doctor", "console", `{"level":"trace","stacktracelevel":"NONE"}`)
  130. } else {
  131. log.NewLogger(1000, "doctor", "file", fmt.Sprintf(`{"filename":%q,"level":"trace","stacktracelevel":"NONE"}`, logFile))
  132. }
  133. // Finally redirect the default golog to here
  134. golog.SetFlags(0)
  135. golog.SetPrefix("")
  136. golog.SetOutput(log.NewLoggerAsWriter("INFO", log.GetLogger(log.DEFAULT)))
  137. if ctx.IsSet("list") {
  138. w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
  139. _, _ = w.Write([]byte("Default\tName\tTitle\n"))
  140. for _, check := range checklist {
  141. if check.isDefault {
  142. _, _ = w.Write([]byte{'*'})
  143. }
  144. _, _ = w.Write([]byte{'\t'})
  145. _, _ = w.Write([]byte(check.name))
  146. _, _ = w.Write([]byte{'\t'})
  147. _, _ = w.Write([]byte(check.title))
  148. _, _ = w.Write([]byte{'\n'})
  149. }
  150. return w.Flush()
  151. }
  152. var checks []check
  153. if ctx.Bool("all") {
  154. checks = checklist
  155. } else if ctx.IsSet("run") {
  156. addDefault := ctx.Bool("default")
  157. names := ctx.StringSlice("run")
  158. for i, name := range names {
  159. names[i] = strings.ToLower(strings.TrimSpace(name))
  160. }
  161. for _, check := range checklist {
  162. if addDefault && check.isDefault {
  163. checks = append(checks, check)
  164. continue
  165. }
  166. for _, name := range names {
  167. if name == check.name {
  168. checks = append(checks, check)
  169. break
  170. }
  171. }
  172. }
  173. } else {
  174. for _, check := range checklist {
  175. if check.isDefault {
  176. checks = append(checks, check)
  177. }
  178. }
  179. }
  180. dbIsInit := false
  181. for i, check := range checks {
  182. if !dbIsInit && !check.skipDatabaseInit {
  183. // Only open database after the most basic configuration check
  184. setting.EnableXORMLog = false
  185. if err := initDBDisableConsole(true); err != nil {
  186. fmt.Println(err)
  187. fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
  188. return nil
  189. }
  190. dbIsInit = true
  191. }
  192. fmt.Println("[", i+1, "]", check.title)
  193. messages, err := check.f(ctx)
  194. for _, message := range messages {
  195. fmt.Println("-", message)
  196. }
  197. if err != nil {
  198. fmt.Println("Error:", err)
  199. if check.abortIfFailed {
  200. return nil
  201. }
  202. } else {
  203. fmt.Println("OK.")
  204. }
  205. fmt.Println()
  206. }
  207. return nil
  208. }
  209. func runDoctorPathInfo(ctx *cli.Context) ([]string, error) {
  210. res := make([]string, 0, 10)
  211. if fi, err := os.Stat(setting.CustomConf); err != nil || !fi.Mode().IsRegular() {
  212. res = append(res, fmt.Sprintf("Failed to find configuration file at '%s'.", setting.CustomConf))
  213. res = append(res, fmt.Sprintf("If you've never ran Gitea yet, this is normal and '%s' will be created for you on first run.", setting.CustomConf))
  214. res = append(res, "Otherwise check that you are running this command from the correct path and/or provide a `--config` parameter.")
  215. return res, fmt.Errorf("can't proceed without a configuration file")
  216. }
  217. setting.NewContext()
  218. fail := false
  219. check := func(name, path string, is_dir, required, is_write bool) {
  220. res = append(res, fmt.Sprintf("%-25s '%s'", name+":", path))
  221. fi, err := os.Stat(path)
  222. if err != nil {
  223. if os.IsNotExist(err) && ctx.Bool("fix") && is_dir {
  224. if err := os.MkdirAll(path, 0777); err != nil {
  225. res = append(res, fmt.Sprintf(" ERROR: %v", err))
  226. fail = true
  227. return
  228. }
  229. fi, err = os.Stat(path)
  230. }
  231. }
  232. if err != nil {
  233. if required {
  234. res = append(res, fmt.Sprintf(" ERROR: %v", err))
  235. fail = true
  236. return
  237. }
  238. res = append(res, fmt.Sprintf(" NOTICE: not accessible (%v)", err))
  239. return
  240. }
  241. if is_dir && !fi.IsDir() {
  242. res = append(res, " ERROR: not a directory")
  243. fail = true
  244. return
  245. } else if !is_dir && !fi.Mode().IsRegular() {
  246. res = append(res, " ERROR: not a regular file")
  247. fail = true
  248. } else if is_write {
  249. if err := runDoctorWritableDir(path); err != nil {
  250. res = append(res, fmt.Sprintf(" ERROR: not writable: %v", err))
  251. fail = true
  252. }
  253. }
  254. }
  255. // Note print paths inside quotes to make any leading/trailing spaces evident
  256. check("Configuration File Path", setting.CustomConf, false, true, false)
  257. check("Repository Root Path", setting.RepoRootPath, true, true, true)
  258. check("Data Root Path", setting.AppDataPath, true, true, true)
  259. check("Custom File Root Path", setting.CustomPath, true, false, false)
  260. check("Work directory", setting.AppWorkPath, true, true, false)
  261. check("Log Root Path", setting.LogRootPath, true, true, true)
  262. if options.IsDynamic() {
  263. // Do not check/report on StaticRootPath if data is embedded in Gitea (-tags bindata)
  264. check("Static File Root Path", setting.StaticRootPath, true, true, false)
  265. }
  266. if fail {
  267. return res, fmt.Errorf("please check your configuration file and try again")
  268. }
  269. return res, nil
  270. }
  271. func runDoctorWritableDir(path string) error {
  272. // There's no platform-independent way of checking if a directory is writable
  273. // https://stackoverflow.com/questions/20026320/how-to-tell-if-folder-exists-and-is-writable
  274. tmpFile, err := ioutil.TempFile(path, "doctors-order")
  275. if err != nil {
  276. return err
  277. }
  278. if err := os.Remove(tmpFile.Name()); err != nil {
  279. fmt.Printf("Warning: can't remove temporary file: '%s'\n", tmpFile.Name())
  280. }
  281. tmpFile.Close()
  282. return nil
  283. }
  284. const tplCommentPrefix = `# gitea public key`
  285. func runDoctorAuthorizedKeys(ctx *cli.Context) ([]string, error) {
  286. if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
  287. return nil, nil
  288. }
  289. fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
  290. f, err := os.Open(fPath)
  291. if err != nil {
  292. if ctx.Bool("fix") {
  293. return []string{fmt.Sprintf("Error whilst opening authorized_keys: %v. Attempting regeneration", err)}, models.RewriteAllPublicKeys()
  294. }
  295. return nil, err
  296. }
  297. defer f.Close()
  298. linesInAuthorizedKeys := map[string]bool{}
  299. scanner := bufio.NewScanner(f)
  300. for scanner.Scan() {
  301. line := scanner.Text()
  302. if strings.HasPrefix(line, tplCommentPrefix) {
  303. continue
  304. }
  305. linesInAuthorizedKeys[line] = true
  306. }
  307. f.Close()
  308. // now we regenerate and check if there are any lines missing
  309. regenerated := &bytes.Buffer{}
  310. if err := models.RegeneratePublicKeys(regenerated); err != nil {
  311. return nil, err
  312. }
  313. scanner = bufio.NewScanner(regenerated)
  314. for scanner.Scan() {
  315. line := scanner.Text()
  316. if strings.HasPrefix(line, tplCommentPrefix) {
  317. continue
  318. }
  319. if ok := linesInAuthorizedKeys[line]; ok {
  320. continue
  321. }
  322. if ctx.Bool("fix") {
  323. return []string{"authorized_keys is out of date, attempting regeneration"}, models.RewriteAllPublicKeys()
  324. }
  325. return nil, fmt.Errorf("authorized_keys is out of date and should be regenerated with gitea admin regenerate keys")
  326. }
  327. return nil, nil
  328. }
  329. func runDoctorCheckDBVersion(ctx *cli.Context) ([]string, error) {
  330. if err := models.NewEngine(context.Background(), migrations.EnsureUpToDate); err != nil {
  331. if ctx.Bool("fix") {
  332. return []string{fmt.Sprintf("WARN: Got Error %v during ensure up to date", err), "Attempting to migrate to the latest DB version to fix this."}, models.NewEngine(context.Background(), migrations.Migrate)
  333. }
  334. return nil, err
  335. }
  336. return nil, nil
  337. }
  338. func iterateRepositories(each func(*models.Repository) ([]string, error)) ([]string, error) {
  339. results := []string{}
  340. err := models.Iterate(
  341. models.DefaultDBContext(),
  342. new(models.Repository),
  343. builder.Gt{"id": 0},
  344. func(idx int, bean interface{}) error {
  345. res, err := each(bean.(*models.Repository))
  346. results = append(results, res...)
  347. return err
  348. },
  349. )
  350. return results, err
  351. }
  352. func iteratePRs(repo *models.Repository, each func(*models.Repository, *models.PullRequest) ([]string, error)) ([]string, error) {
  353. results := []string{}
  354. err := models.Iterate(
  355. models.DefaultDBContext(),
  356. new(models.PullRequest),
  357. builder.Eq{"base_repo_id": repo.ID},
  358. func(idx int, bean interface{}) error {
  359. res, err := each(repo, bean.(*models.PullRequest))
  360. results = append(results, res...)
  361. return err
  362. },
  363. )
  364. return results, err
  365. }
  366. func runDoctorHooks(ctx *cli.Context) ([]string, error) {
  367. // Need to iterate across all of the repositories
  368. return iterateRepositories(func(repo *models.Repository) ([]string, error) {
  369. results, err := models.CheckDelegateHooks(repo.RepoPath())
  370. if err != nil {
  371. return nil, err
  372. }
  373. if len(results) > 0 && ctx.Bool("fix") {
  374. return []string{fmt.Sprintf("regenerated hooks for %s", repo.FullName())}, models.CreateDelegateHooks(repo.RepoPath())
  375. }
  376. return results, nil
  377. })
  378. }
  379. func runDoctorPRMergeBase(ctx *cli.Context) ([]string, error) {
  380. numRepos := 0
  381. numPRs := 0
  382. numPRsUpdated := 0
  383. results, err := iterateRepositories(func(repo *models.Repository) ([]string, error) {
  384. numRepos++
  385. return iteratePRs(repo, func(repo *models.Repository, pr *models.PullRequest) ([]string, error) {
  386. numPRs++
  387. results := []string{}
  388. pr.BaseRepo = repo
  389. repoPath := repo.RepoPath()
  390. oldMergeBase := pr.MergeBase
  391. if !pr.HasMerged {
  392. var err error
  393. pr.MergeBase, err = git.NewCommand("merge-base", "--", pr.BaseBranch, pr.GetGitRefName()).RunInDir(repoPath)
  394. if err != nil {
  395. var err2 error
  396. pr.MergeBase, err2 = git.NewCommand("rev-parse", git.BranchPrefix+pr.BaseBranch).RunInDir(repoPath)
  397. if err2 != nil {
  398. results = append(results, fmt.Sprintf("WARN: Unable to get merge base for PR ID %d, #%d onto %s in %s/%s", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name))
  399. log.Error("Unable to get merge base for PR ID %d, Index %d in %s/%s. Error: %v & %v", pr.ID, pr.Index, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err, err2)
  400. return results, nil
  401. }
  402. }
  403. } else {
  404. parentsString, err := git.NewCommand("rev-list", "--parents", "-n", "1", pr.MergedCommitID).RunInDir(repoPath)
  405. if err != nil {
  406. results = append(results, fmt.Sprintf("WARN: Unable to get parents for merged PR ID %d, #%d onto %s in %s/%s", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name))
  407. log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err)
  408. return results, nil
  409. }
  410. parents := strings.Split(strings.TrimSpace(parentsString), " ")
  411. if len(parents) < 2 {
  412. return results, nil
  413. }
  414. args := append([]string{"merge-base", "--"}, parents[1:]...)
  415. args = append(args, pr.GetGitRefName())
  416. pr.MergeBase, err = git.NewCommand(args...).RunInDir(repoPath)
  417. if err != nil {
  418. results = append(results, fmt.Sprintf("WARN: Unable to get merge base for merged PR ID %d, #%d onto %s in %s/%s", pr.ID, pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name))
  419. log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, err)
  420. return results, nil
  421. }
  422. }
  423. pr.MergeBase = strings.TrimSpace(pr.MergeBase)
  424. if pr.MergeBase != oldMergeBase {
  425. if ctx.Bool("fix") {
  426. if err := pr.UpdateCols("merge_base"); err != nil {
  427. return results, err
  428. }
  429. } else {
  430. results = append(results, fmt.Sprintf("#%d onto %s in %s/%s: MergeBase should be %s but is %s", pr.Index, pr.BaseBranch, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, oldMergeBase, pr.MergeBase))
  431. }
  432. numPRsUpdated++
  433. }
  434. return results, nil
  435. })
  436. })
  437. if ctx.Bool("fix") {
  438. results = append(results, fmt.Sprintf("%d PR mergebases updated of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos))
  439. } else {
  440. if numPRsUpdated > 0 && err == nil {
  441. return results, fmt.Errorf("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos)
  442. }
  443. results = append(results, fmt.Sprintf("%d PRs with incorrect mergebases of %d PRs total in %d repos", numPRsUpdated, numPRs, numRepos))
  444. }
  445. return results, err
  446. }
  447. func runDoctorScriptType(ctx *cli.Context) ([]string, error) {
  448. path, err := exec.LookPath(setting.ScriptType)
  449. if err != nil {
  450. return []string{fmt.Sprintf("ScriptType %s is not on the current PATH", setting.ScriptType)}, err
  451. }
  452. return []string{fmt.Sprintf("ScriptType %s is on the current PATH at %s", setting.ScriptType, path)}, nil
  453. }
  454. func runDoctorCheckDBConsistency(ctx *cli.Context) ([]string, error) {
  455. var results []string
  456. // make sure DB version is uptodate
  457. if err := models.NewEngine(context.Background(), migrations.EnsureUpToDate); err != nil {
  458. return nil, fmt.Errorf("model version on the database does not match the current Gitea version. Model consistency will not be checked until the database is upgraded")
  459. }
  460. //find tracked times without existing issues/pulls
  461. count, err := models.CountOrphanedObjects("tracked_time", "issue", "tracked_time.issue_id=issue.id")
  462. if err != nil {
  463. return nil, err
  464. }
  465. if count > 0 {
  466. if ctx.Bool("fix") {
  467. if err = models.DeleteOrphanedObjects("tracked_time", "issue", "tracked_time.issue_id=issue.id"); err != nil {
  468. return nil, err
  469. }
  470. results = append(results, fmt.Sprintf("%d tracked times without existing issue deleted", count))
  471. } else {
  472. results = append(results, fmt.Sprintf("%d tracked times without existing issue", count))
  473. }
  474. }
  475. return results, nil
  476. }