github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/builtins/events/onFileSystemChange/filesystem_unix.go (about) 1 //go:build !windows && !plan9 && !js 2 // +build !windows,!plan9,!js 3 4 package onfilesystemchange 5 6 import ( 7 "errors" 8 "fmt" 9 "os" 10 "path/filepath" 11 "strings" 12 "sync" 13 14 "github.com/fsnotify/fsnotify" 15 "github.com/lmorg/murex/builtins/events" 16 "github.com/lmorg/murex/lang" 17 "github.com/lmorg/murex/lang/ref" 18 ) 19 20 const eventType = "onFileSystemChange" 21 22 // Interrupt is a JSON structure passed to the murex function 23 type Interrupt struct { 24 Path string 25 Operation string 26 } 27 28 func init() { 29 evt, err := newWatch() 30 events.AddEventType(eventType, evt, err) 31 go evt.init() 32 } 33 34 type watch struct { 35 watcher *fsnotify.Watcher 36 mutex sync.Mutex 37 paths map[string]string // map of paths indexed by event name 38 source map[string]source // map of blocks indexed by path 39 } 40 41 type source struct { 42 name string 43 block []rune 44 fileRef *ref.File 45 } 46 47 func newWatch() (w *watch, err error) { 48 w = new(watch) 49 w.watcher, err = fsnotify.NewWatcher() 50 w.paths = make(map[string]string) 51 w.source = make(map[string]source) 52 return 53 } 54 55 // Callback returns the block to execute upon a triggered event 56 func (evt *watch) findCallbackBlock(path string) (source, error) { 57 evt.mutex.Lock() 58 59 for { 60 for len(path) > 1 && path[len(path)-1] == '/' { 61 path = path[:len(path)-1] 62 } 63 64 source := evt.source[path] 65 if len(source.block) > 0 { 66 evt.mutex.Unlock() 67 return source, nil 68 } 69 70 split := strings.Split(path, "/") 71 switch len(split) { 72 case 0: 73 path = "/" 74 case 1: 75 path = strings.Join(split, "/") 76 default: 77 path = strings.Join(split[:len(split)-1], "/") 78 } 79 } 80 81 evt.mutex.Unlock() 82 return source{}, fmt.Errorf("cannot locate source for event '%s'. This is probably a bug in murex, please report to https://github.com/lmorg/murex/issues", path) 83 } 84 85 // Add a path to the watch event list 86 func (evt *watch) Add(name, path string, block []rune, fileRef *ref.File) error { 87 if len(path) == 0 { 88 return errors.New("no path to watch supplied") 89 } 90 91 for len(path) > 1 && path[len(path)-1] == '/' { 92 path = path[:len(path)-1] 93 } 94 95 pwd, err := os.Getwd() 96 if err == nil && path[0] != '/' { 97 path = pwd + "/" + path 98 } 99 100 path = filepath.Clean(path) 101 102 err = evt.watcher.Add(path) 103 if err == nil { 104 evt.mutex.Lock() 105 evt.paths[name] = path 106 evt.source[path] = source{ 107 name: name, 108 block: block, 109 fileRef: fileRef, 110 } 111 evt.mutex.Unlock() 112 } 113 114 return err 115 } 116 117 // Remove a path to the watch event list 118 func (evt *watch) Remove(name string) error { 119 path := evt.paths[name] 120 if path == "" { 121 return fmt.Errorf("no event found for this listener with the name '%s'", name) 122 } 123 124 err := evt.watcher.Remove(path) 125 if err == nil { 126 evt.mutex.Lock() 127 delete(evt.paths, name) 128 delete(evt.source, path) 129 evt.mutex.Unlock() 130 } 131 132 return err 133 } 134 135 // Init starts a new watch event loop 136 func (evt *watch) init() { 137 defer evt.watcher.Close() 138 139 for { 140 select { 141 case event := <-evt.watcher.Events: 142 source, err := evt.findCallbackBlock(event.Name) 143 if err != nil { 144 lang.ShellProcess.Stderr.Writeln([]byte("onFileSystemChange event error: " + err.Error())) 145 continue 146 } 147 events.Callback( 148 source.name, 149 Interrupt{ 150 Path: event.Name, 151 Operation: strings.ToLower(event.Op.String()), 152 }, 153 source.block, 154 source.fileRef, 155 lang.ShellProcess.Stdout, 156 true, 157 ) 158 159 case err := <-evt.watcher.Errors: 160 lang.ShellProcess.Stderr.Writeln([]byte("onFileSystemChange watcher error: " + err.Error())) 161 } 162 } 163 } 164 165 // Dump returns all the events in fsWatch 166 func (evt *watch) Dump() map[string]events.DumpT { 167 dump := make(map[string]events.DumpT) 168 169 evt.mutex.Lock() 170 171 for name, path := range evt.paths { 172 dump[name] = events.DumpT{ 173 Interrupt: path, 174 Block: string(evt.source[path].block), 175 FileRef: evt.source[path].fileRef, 176 } 177 } 178 179 evt.mutex.Unlock() 180 181 return dump 182 }