/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ package main import ( "fmt" "io/fs" "os" "sync" ) var FileSysInfoMutex = &sync.Mutex{} /* Try to detect whether the file was rotated. As is the case with logrotate, we assume there are only a couple ways to perform file rotation: rename then create, or copy then, usually, truncate. See `man 8 logrotate` for details. */ func detectFileRotation( path string, file *os.File, ) (rot bool, err error) { /* The order in which we retrieve this file info matters. Suppose an empty file is rotated by copytruncate after 24 hours. The inode is unchanged. The file size is unchanged. Without subscribing to some kernel-level stream of filesystem events, all we can do is check for metadata modification (ctime) to see whether the new file has a ctime that is greater than the ctime of the old file. But what if the file hasn't been rotated? We test for rotation and observe that, again, the inode and size are unchanged. However, the ctime could have been updated by a write while we were awaiting our syscall. Then we would notice a new ctime for the same file, which would prompt us to frivolously reopen the file. Is this false positive so bad? Well, yes, it would be more costly for busier logs. Therefore, we propose to retrieve the "new" ctime, first, then retrieve the "current" ctime for the open file. This way, we never run the risk have a false positive. That said, if the Go compiler or CPU changes the order of these calls, we are properly horked. Therefore we opt to use sync.Mutex to guarantee the correct order. See https://go.dev/ref/mem#locks for details. */ var curInfo, newInfo fs.FileInfo FileSysInfoMutex.Lock() if newInfo, err = os.Stat(path); err != nil { err = fmt.Errorf("unable to retrieve info for file %#v: %w", path, err) return } FileSysInfoMutex.Unlock() FileSysInfoMutex.Lock() if curInfo, err = file.Stat(); err != nil { err = fmt.Errorf("unable to retrieve info for file %#v: %w", path, err) return } FileSysInfoMutex.Unlock() var curStat, newStat FileSysInfo var ok bool if curStat, ok = curInfo.Sys().(FileSysInfo); !ok { panic(fmt.Sprintf("BUG: expected %s for open file but got type %T instead", FileSysInfoStr, curInfo.Sys())) } if newStat, ok = newInfo.Sys().(FileSysInfo); !ok { panic(fmt.Sprintf("BUG: expected %s for rotated file but got type %T instead", FileSysInfoStr, curInfo.Sys())) } return fileWasRotated(newStat, curStat), err }