The Goal:
I am attempting to monitor a file that is subject to being moved or deleted at any time. If and when it is, I’d like to re-generate this file so that an app can continue to write to it.
Attempted:
I have attempted to do this by implementing two functions, monitorFile()
to listen for fsnotify
events and send the removed filename over a channel to listen()
which upon receiving the filepath string over un-buffered channel mvrm
(move or rename), will recursively re-generate the file.
Observed behavior:
I can echo 'foo' >> ./inlogs/test.log
and see a write notification, and can even rm ./inlogs/test.log
(or mv
) and see that the file is re-generated… but only once. If I rm
or mv
the file a second time, the file is not re-generated.
- Strangely, the undesired behavior does not occur on local Mac OSx (System Version: macOS 10.13.2 (17C88), Kernel Version: Darwin 17.3.0), but does on two different Linux machines with builds:
Linux 3.13.0-32-generic #57-Ubuntu SMP x86_64 x86_64 x86_64 GNU/Linux
Linux 4.9.51-10.52.amzn1.x86_64 #1 SMP x86_64 x86_64 x86_64 GNU/Linux
Diagnostics Attempted:
The differing behavior makes me think I have a race condition. However go build -race
provides no output.
I wonder if done
chan is receiving due to such a race condition?
Apologies that this is not ‘Playground-able’, but any advice or observation of where this might by racy or buggy would be welcome.
watcher.go:
package main
import (
"os"
"log"
"fmt"
"github.com/go-fsnotify/fsnotify"
)
//Globals
var mvrm chan string
func main() {
mvrm = make(chan string)
listen(mvrm)
monitorFile("./inlogs/test.log", mvrm)
}
func listen(mvrm chan string) {
go func() {
for {
select {
case fileName := <-mvrm :
fmt.Println(fileName)
newFile, err := os.OpenFile(fileName, os.O_RDWR | os.O_CREATE | os.O_APPEND , 0666)
if err == nil {
defer newFile.Close()
// Recursively re-spawn monitoring
go listen(mvrm)
go monitorFile(fileName, mvrm)
} else {
log.Fatal("Err re-spawning file")
}
default:
continue
}
}
}()
}
func monitorFile(filepath string, mvrm chan string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
switch event.Op {
case fsnotify.Write :
log.Println("Write!")
continue
case fsnotify.Chmod :
log.Println("Chmod!")
continue
case fsnotify.Remove, fsnotify.Rename :
log.Println("Moved or Deleted!")
mvrm <- event.Name
continue
default:
log.Printf("Unknown: %vn", event.Op)
continue
}
case err := <-watcher.Errors:
log.Println("Error:", err)
}
}
}()
err = watcher.Add(filepath)
if err != nil {
log.Fatal(err)
}
<-done
}
EDIT:
With some great feedback, I’ve paired this down. In Linux it is now re-generating the file as intended, but after monitoring with top
, I see it is spawning a new PID every time the file is moved or deleted, so I do still have a leak. Suggestions as to how I might eliminate this behavior welcome.
https://play.golang.org/p/FrlkktoK2-s
Advertisement
Answer
Please see the code comments, most of the discussion in code comments.
https://play.golang.com/p/qxq58h1nQjp
Outside the golang universe, but facebook has a tool that does pretty much what you are looking for, just not as much go code fun :): https://github.com/facebook/watchman
package main
import (
"log"
"os"
// couldn't find the go-fsnotify, this is what pops up on github
"github.com/fsnotify/fsnotify"
)
func main() {
monitorFile("./inlogs/test.log")
}
func monitorFile(filepath string) {
// starting watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// monitor events
go func() {
for {
select {
case event := <-watcher.Events:
switch event.Op {
case fsnotify.Create:
log.Println("Created")
case fsnotify.Write:
log.Println("Write")
case fsnotify.Chmod:
log.Println("Chmod")
case fsnotify.Remove, fsnotify.Rename:
log.Println("Moved or Deleted")
respawnFile(event.Name)
// add the file back to watcher, since it is removed from it
// when file is moved or deleted
log.Printf("add to watcher file: %sn", filepath)
// add appears to be concurrently safe so calling from multiple go routines is ok
err = watcher.Add(filepath)
if err != nil {
log.Fatal(err)
}
// there is not need to break the loop
// we just continue waiting for events from the same watcher
}
case err := <-watcher.Errors:
log.Println("Error:", err)
}
}
}()
// add file to the watcher first time
log.Printf("add to watcher 1st: %sn", filepath)
err = watcher.Add(filepath)
if err != nil {
log.Fatal(err)
}
// to keep waiting forever, to prevent main exit
// this is to replace the done channel
select {}
}
func respawnFile(filepath string) {
log.Printf("re creating file %sn", filepath)
// you just need the os.Create()
respawned, err := os.Create(filepath)
if err != nil {
log.Fatalf("Err re-spawning file: %v", filepath)
}
defer respawned.Close()
// there is no need to call monitorFile again, it never returns
// the call to "go monitorFile(filepath)" was causing another go routine leak
}
Have fun!