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  }