rotate.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. /*
  2. * Copyright (c) 2016, Psiphon Inc.
  3. * All rights reserved.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. */
  19. // Package `rotate` provides an io.Writer interface for files that will detect when the open
  20. // file has been rotated away (due to log rotation, or manual move/deletion) and re-open it.
  21. // This allows the standard log.Logger to continue working as expected after log rotation
  22. // happens (without needing to specify the `copytruncate` or equivalient options).
  23. //
  24. // This package is safe to use concurrently from multiple goroutines
  25. package rotate
  26. import (
  27. "os"
  28. "sync"
  29. "time"
  30. )
  31. // RotatableFileWriter implementation that knows when the file has been rotated and re-opens it
  32. type RotatableFileWriter struct {
  33. sync.Mutex
  34. file *os.File
  35. fileInfo *os.FileInfo
  36. name string
  37. retries int
  38. create bool
  39. mode os.FileMode
  40. }
  41. // Close closes the underlying file
  42. func (f *RotatableFileWriter) Close() error {
  43. f.Lock()
  44. err := f.file.Close()
  45. f.Unlock()
  46. return err
  47. }
  48. // reopen provides the (not exported, not concurrency safe) implementation of re-opening the file and updates the struct's fileInfo
  49. func (f *RotatableFileWriter) reopen() error {
  50. if f.file != nil {
  51. f.file.Close()
  52. f.file = nil
  53. f.fileInfo = nil
  54. }
  55. flags := os.O_WRONLY | os.O_APPEND
  56. if f.create {
  57. flags |= os.O_CREATE
  58. }
  59. reopened, err := os.OpenFile(f.name, flags, f.mode)
  60. if err != nil {
  61. return err
  62. }
  63. f.file = reopened
  64. fileInfo, err := os.Stat(f.name)
  65. if err != nil {
  66. return err
  67. }
  68. f.fileInfo = &fileInfo
  69. return nil
  70. }
  71. func (f *RotatableFileWriter) reopenWithRetries() error {
  72. var err error
  73. for i := 0; i <= f.retries; i++ {
  74. err = f.reopen()
  75. if err == nil {
  76. break
  77. }
  78. time.Sleep(1 * time.Millisecond)
  79. }
  80. return err
  81. }
  82. // Reopen provides the concurrency safe implementation of re-opening the file, and updating the struct's fileInfo
  83. func (f *RotatableFileWriter) Reopen() error {
  84. f.Lock()
  85. err := f.reopenWithRetries()
  86. f.Unlock()
  87. return err
  88. }
  89. // Write implements the standard io.Writer interface, but checks whether or not the file
  90. // has changed prior to writing. If it has, it will reopen the file first, then write
  91. func (f *RotatableFileWriter) Write(p []byte) (int, error) {
  92. f.Lock()
  93. defer f.Unlock() // Defer unlock due to the possibility of early return
  94. // If a call to Write fails while attempting to re-open the file, f.fileInfo
  95. // could be left nil, causing subsequent writes to panic. This will attempt
  96. // to re-open the file handle prior to writing in that case
  97. if f.file == nil || f.fileInfo == nil {
  98. err := f.reopenWithRetries()
  99. if err != nil {
  100. return 0, err
  101. }
  102. }
  103. currentFileInfo, err := os.Stat(f.name)
  104. if err != nil || !os.SameFile(*f.fileInfo, currentFileInfo) {
  105. err := f.reopenWithRetries()
  106. if err != nil {
  107. return 0, err
  108. }
  109. }
  110. bytesWritten, err := f.file.Write(p)
  111. // If the write fails with nothing written, attempt to re-open the file and retry the write
  112. if bytesWritten == 0 && err != nil {
  113. err = f.reopenWithRetries()
  114. if err != nil {
  115. return 0, err
  116. }
  117. bytesWritten, err = f.file.Write(p)
  118. }
  119. return bytesWritten, err
  120. }
  121. // NewRotatableFileWriter opens a file for appending and writing that can be
  122. // safely rotated.
  123. //
  124. // If retries is greater than 0, RotatableFileWriter will make that number of
  125. // additional attempts to reopen a rotated file after a file open failure,
  126. // following a 1ms delay. This option can mitigate race conditions that may
  127. // occur when RotatableFileWriter reopens the file while an underlying file
  128. // manager, such as logrotate, is recreating the file and setting its
  129. // properties.
  130. //
  131. // When create is true, RotatableFileWriter will attempt to create the file
  132. // (using mode), if it does not exist, when reopening the file. Set create to
  133. // false to avoid conflicts with an underlying file manager, such as
  134. // logrotate. logrotate, unless configured with nocreate, creates files with
  135. // O_EXCL and re-rotates/retries if another process has created the file.
  136. func NewRotatableFileWriter(
  137. name string,
  138. retries int,
  139. create bool,
  140. mode os.FileMode) (*RotatableFileWriter, error) {
  141. if retries < 0 {
  142. retries = 0
  143. }
  144. rotatableFileWriter := RotatableFileWriter{
  145. name: name,
  146. retries: retries,
  147. create: create,
  148. mode: mode,
  149. }
  150. err := rotatableFileWriter.reopenWithRetries()
  151. if err != nil {
  152. return nil, err
  153. }
  154. return &rotatableFileWriter, nil
  155. }