| package loggerutils // import "github.com/docker/docker/daemon/logger/loggerutils" |
| |
| import ( |
| "io" |
| "io/fs" |
| "os" |
| "runtime" |
| ) |
| |
| type fileConvertFn func(dst io.WriteSeeker, src io.ReadSeeker) error |
| |
| type stfID uint64 |
| |
| // sharedTempFileConverter converts files using a user-supplied function and |
| // writes the results to temporary files which are automatically cleaned up on |
| // close. If another request is made to convert the same file, the conversion |
| // result and temporary file are reused if they have not yet been cleaned up. |
| // |
| // A file is considered the same as another file using the os.SameFile function, |
| // which compares file identity (e.g. device and inode numbers on Linux) and is |
| // robust to file renames. Input files are assumed to be immutable; no attempt |
| // is made to ascertain whether the file contents have changed between requests. |
| // |
| // One file descriptor is used per source file, irrespective of the number of |
| // concurrent readers of the converted contents. |
| type sharedTempFileConverter struct { |
| // The directory where temporary converted files are to be written to. |
| // If set to the empty string, the default directory for temporary files |
| // is used. |
| TempDir string |
| |
| conv fileConvertFn |
| st chan stfcState |
| } |
| |
| type stfcState struct { |
| fl map[stfID]sharedTempFile |
| nextID stfID |
| } |
| |
| type sharedTempFile struct { |
| src os.FileInfo // Info about the source file for path-independent identification with os.SameFile. |
| fd *os.File |
| size int64 |
| ref int // Reference count of open readers on the temporary file. |
| wait []chan<- stfConvertResult // Wait list for the conversion to complete. |
| } |
| |
| type stfConvertResult struct { |
| fr *sharedFileReader |
| err error |
| } |
| |
| func newSharedTempFileConverter(conv fileConvertFn) *sharedTempFileConverter { |
| st := make(chan stfcState, 1) |
| st <- stfcState{fl: make(map[stfID]sharedTempFile)} |
| return &sharedTempFileConverter{conv: conv, st: st} |
| } |
| |
| // Do returns a reader for the contents of f as converted by the c.C function. |
| // It is the caller's responsibility to close the returned reader. |
| // |
| // This function is safe for concurrent use by multiple goroutines. |
| func (c *sharedTempFileConverter) Do(f *os.File) (*sharedFileReader, error) { |
| stat, err := f.Stat() |
| if err != nil { |
| return nil, err |
| } |
| |
| st := <-c.st |
| for id, tf := range st.fl { |
| // os.SameFile can have false positives if one of the files was |
| // deleted before the other file was created -- such as during |
| // log rotations... https://github.com/golang/go/issues/36895 |
| // Weed out those false positives by also comparing the files' |
| // ModTime, which conveniently also handles the case of true |
| // positives where the file has also been modified since it was |
| // first converted. |
| if os.SameFile(tf.src, stat) && tf.src.ModTime().Equal(stat.ModTime()) { |
| return c.openExisting(st, id, tf) |
| } |
| } |
| return c.openNew(st, f, stat) |
| } |
| |
| func (c *sharedTempFileConverter) openNew(st stfcState, f *os.File, stat os.FileInfo) (*sharedFileReader, error) { |
| // Record that we are starting to convert this file so that any other |
| // requests for the same source file while the conversion is in progress |
| // can join. |
| id := st.nextID |
| st.nextID++ |
| st.fl[id] = sharedTempFile{src: stat} |
| c.st <- st |
| |
| dst, size, convErr := c.convert(f) |
| |
| st = <-c.st |
| flid := st.fl[id] |
| |
| if convErr != nil { |
| // Conversion failed. Delete it from the state so that future |
| // requests to convert the same file can try again fresh. |
| delete(st.fl, id) |
| c.st <- st |
| for _, w := range flid.wait { |
| w <- stfConvertResult{err: convErr} |
| } |
| return nil, convErr |
| } |
| |
| flid.fd = dst |
| flid.size = size |
| flid.ref = len(flid.wait) + 1 |
| for _, w := range flid.wait { |
| // Each waiter needs its own reader with an independent read pointer. |
| w <- stfConvertResult{fr: flid.Reader(c, id)} |
| } |
| flid.wait = nil |
| st.fl[id] = flid |
| c.st <- st |
| return flid.Reader(c, id), nil |
| } |
| |
| func (c *sharedTempFileConverter) openExisting(st stfcState, id stfID, v sharedTempFile) (*sharedFileReader, error) { |
| if v.fd != nil { |
| // Already converted. |
| v.ref++ |
| st.fl[id] = v |
| c.st <- st |
| return v.Reader(c, id), nil |
| } |
| // The file has not finished being converted. |
| // Add ourselves to the wait list. "Don't call us; we'll call you." |
| wait := make(chan stfConvertResult, 1) |
| v.wait = append(v.wait, wait) |
| st.fl[id] = v |
| c.st <- st |
| |
| res := <-wait |
| return res.fr, res.err |
| } |
| |
| func (c *sharedTempFileConverter) convert(f *os.File) (converted *os.File, size int64, err error) { |
| dst, err := os.CreateTemp(c.TempDir, "dockerdtemp.*") |
| if err != nil { |
| return nil, 0, err |
| } |
| defer func() { |
| _ = dst.Close() |
| // Delete the temporary file immediately so that final cleanup |
| // of the file on disk is deferred to the OS once we close all |
| // our file descriptors (or the process dies). Assuming no early |
| // returns due to errors, the file will be open by this process |
| // with a read-only descriptor at this point. As we don't care |
| // about being able to reuse the file name -- it's randomly |
| // generated and unique -- we can safely use os.Remove on |
| // Windows. |
| _ = os.Remove(dst.Name()) |
| }() |
| err = c.conv(dst, f) |
| if err != nil { |
| return nil, 0, err |
| } |
| // Close the exclusive read-write file descriptor, catching any delayed |
| // write errors (and on Windows, releasing the share-locks on the file) |
| if err := dst.Close(); err != nil { |
| _ = os.Remove(dst.Name()) |
| return nil, 0, err |
| } |
| // Open the file again read-only (without locking the file against |
| // deletion on Windows). |
| converted, err = open(dst.Name()) |
| if err != nil { |
| return nil, 0, err |
| } |
| |
| // The position of the file's read pointer doesn't matter as all readers |
| // will be accessing the file through its io.ReaderAt interface. |
| size, err = converted.Seek(0, io.SeekEnd) |
| if err != nil { |
| _ = converted.Close() |
| return nil, 0, err |
| } |
| return converted, size, nil |
| } |
| |
| type sharedFileReader struct { |
| *io.SectionReader |
| |
| c *sharedTempFileConverter |
| id stfID |
| closed bool |
| } |
| |
| func (stf sharedTempFile) Reader(c *sharedTempFileConverter, id stfID) *sharedFileReader { |
| rdr := &sharedFileReader{SectionReader: io.NewSectionReader(stf.fd, 0, stf.size), c: c, id: id} |
| runtime.SetFinalizer(rdr, (*sharedFileReader).Close) |
| return rdr |
| } |
| |
| func (r *sharedFileReader) Close() error { |
| if r.closed { |
| return fs.ErrClosed |
| } |
| |
| st := <-r.c.st |
| flid, ok := st.fl[r.id] |
| if !ok { |
| panic("invariant violation: temp file state missing from map") |
| } |
| flid.ref-- |
| lastRef := flid.ref <= 0 |
| if lastRef { |
| delete(st.fl, r.id) |
| } else { |
| st.fl[r.id] = flid |
| } |
| r.closed = true |
| r.c.st <- st |
| |
| if lastRef { |
| return flid.fd.Close() |
| } |
| runtime.SetFinalizer(r, nil) |
| return nil |
| } |