Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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