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.

log.go 4.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package actions
  4. import (
  5. "bufio"
  6. "context"
  7. "fmt"
  8. "io"
  9. "os"
  10. "strings"
  11. "time"
  12. "code.gitea.io/gitea/models/dbfs"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/storage"
  15. runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
  16. "google.golang.org/protobuf/types/known/timestamppb"
  17. )
  18. const (
  19. MaxLineSize = 64 * 1024
  20. DBFSPrefix = "actions_log/"
  21. timeFormat = "2006-01-02T15:04:05.0000000Z07:00"
  22. defaultBufSize = MaxLineSize
  23. )
  24. func WriteLogs(ctx context.Context, filename string, offset int64, rows []*runnerv1.LogRow) ([]int, error) {
  25. flag := os.O_WRONLY
  26. if offset == 0 {
  27. // Create file only if offset is 0, or it could result in content holes if the file doesn't exist.
  28. flag |= os.O_CREATE
  29. }
  30. name := DBFSPrefix + filename
  31. f, err := dbfs.OpenFile(ctx, name, flag)
  32. if err != nil {
  33. return nil, fmt.Errorf("dbfs OpenFile %q: %w", name, err)
  34. }
  35. defer f.Close()
  36. stat, err := f.Stat()
  37. if err != nil {
  38. return nil, fmt.Errorf("dbfs Stat %q: %w", name, err)
  39. }
  40. if stat.Size() < offset {
  41. // If the size is less than offset, refuse to write, or it could result in content holes.
  42. // However, if the size is greater than offset, we can still write to overwrite the content.
  43. return nil, fmt.Errorf("size of %q is less than offset", name)
  44. }
  45. if _, err := f.Seek(offset, io.SeekStart); err != nil {
  46. return nil, fmt.Errorf("dbfs Seek %q: %w", name, err)
  47. }
  48. writer := bufio.NewWriterSize(f, defaultBufSize)
  49. ns := make([]int, 0, len(rows))
  50. for _, row := range rows {
  51. n, err := writer.WriteString(FormatLog(row.Time.AsTime(), row.Content) + "\n")
  52. if err != nil {
  53. return nil, err
  54. }
  55. ns = append(ns, n)
  56. }
  57. if err := writer.Flush(); err != nil {
  58. return nil, err
  59. }
  60. return ns, nil
  61. }
  62. func ReadLogs(ctx context.Context, inStorage bool, filename string, offset, limit int64) ([]*runnerv1.LogRow, error) {
  63. f, err := OpenLogs(ctx, inStorage, filename)
  64. if err != nil {
  65. return nil, err
  66. }
  67. defer f.Close()
  68. if _, err := f.Seek(offset, io.SeekStart); err != nil {
  69. return nil, fmt.Errorf("file seek: %w", err)
  70. }
  71. scanner := bufio.NewScanner(f)
  72. maxLineSize := len(timeFormat) + MaxLineSize + 1
  73. scanner.Buffer(make([]byte, maxLineSize), maxLineSize)
  74. var rows []*runnerv1.LogRow
  75. for scanner.Scan() && (int64(len(rows)) < limit || limit < 0) {
  76. t, c, err := ParseLog(scanner.Text())
  77. if err != nil {
  78. return nil, fmt.Errorf("parse log %q: %w", scanner.Text(), err)
  79. }
  80. rows = append(rows, &runnerv1.LogRow{
  81. Time: timestamppb.New(t),
  82. Content: c,
  83. })
  84. }
  85. if err := scanner.Err(); err != nil {
  86. return nil, fmt.Errorf("scan: %w", err)
  87. }
  88. return rows, nil
  89. }
  90. func TransferLogs(ctx context.Context, filename string) (func(), error) {
  91. name := DBFSPrefix + filename
  92. remove := func() {
  93. if err := dbfs.Remove(ctx, name); err != nil {
  94. log.Warn("dbfs remove %q: %v", name, err)
  95. }
  96. }
  97. f, err := dbfs.Open(ctx, name)
  98. if err != nil {
  99. return nil, fmt.Errorf("dbfs open %q: %w", name, err)
  100. }
  101. defer f.Close()
  102. if _, err := storage.Actions.Save(filename, f, -1); err != nil {
  103. return nil, fmt.Errorf("storage save %q: %w", filename, err)
  104. }
  105. return remove, nil
  106. }
  107. func RemoveLogs(ctx context.Context, inStorage bool, filename string) error {
  108. if !inStorage {
  109. name := DBFSPrefix + filename
  110. err := dbfs.Remove(ctx, name)
  111. if err != nil {
  112. return fmt.Errorf("dbfs remove %q: %w", name, err)
  113. }
  114. return nil
  115. }
  116. err := storage.Actions.Delete(filename)
  117. if err != nil {
  118. return fmt.Errorf("storage delete %q: %w", filename, err)
  119. }
  120. return nil
  121. }
  122. func OpenLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeekCloser, error) {
  123. if !inStorage {
  124. name := DBFSPrefix + filename
  125. f, err := dbfs.Open(ctx, name)
  126. if err != nil {
  127. return nil, fmt.Errorf("dbfs open %q: %w", name, err)
  128. }
  129. return f, nil
  130. }
  131. f, err := storage.Actions.Open(filename)
  132. if err != nil {
  133. return nil, fmt.Errorf("storage open %q: %w", filename, err)
  134. }
  135. return f, nil
  136. }
  137. func FormatLog(timestamp time.Time, content string) string {
  138. // Content shouldn't contain new line, it will break log indexes, other control chars are safe.
  139. content = strings.ReplaceAll(content, "\n", `\n`)
  140. if len(content) > MaxLineSize {
  141. content = content[:MaxLineSize]
  142. }
  143. return fmt.Sprintf("%s %s", timestamp.UTC().Format(timeFormat), content)
  144. }
  145. func ParseLog(in string) (time.Time, string, error) {
  146. index := strings.IndexRune(in, ' ')
  147. if index < 0 {
  148. return time.Time{}, "", fmt.Errorf("invalid log: %q", in)
  149. }
  150. timestamp, err := time.Parse(timeFormat, in[:index])
  151. if err != nil {
  152. return time.Time{}, "", err
  153. }
  154. return timestamp, in[index+1:], nil
  155. }