-
Notifications
You must be signed in to change notification settings - Fork 2
/
main.go
200 lines (173 loc) · 5.32 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package main
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"strings"
extism "github.com/extism/go-sdk"
"github.com/fsnotify/fsnotify"
)
type EventInput struct {
EventFileName string `json:"event_file_name"`
EventFileData string `json:"event_file_data"`
}
type EventOutput struct {
Op string `json:"op"`
OutputFileName string `json:"output_file_name"`
OutputFileData string `json:"output_file_data"`
}
func main() {
// Create new watcher.
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// Caller sets the target path
path := "."
if len(os.Args) > 2 {
path = os.Args[2]
}
log.Println("watching at:", path)
// Look for other directories within the path and watch those too
dirs := make([]string, 0)
fs.WalkDir(os.DirFS(path), ".", func(name string, entry os.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
dirs = append(dirs, entry.Name())
err := watcher.Add(filepath.Join(path, name))
catch(err, fmt.Sprintf("add nested path: %s", entry.Name()))
}
return nil
})
log.Println("watching dirs:", dirs)
// Create a collection to store our plug-ins throughout the apps lifetime
plugins := make(map[string]*extism.Plugin)
// Start listening for events.
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op != fsnotify.Create {
continue
}
// find relevant files and add/remove watcher paths
dir := filepath.Dir(event.Name)
entries, err := os.ReadDir(dir)
catch(err, fmt.Sprintf("read dir from %s", event.Name))
files := make([]string, 0)
for _, file := range entries {
if !file.IsDir() {
files = append(files, file.Name())
} else {
if event.Op&fsnotify.Create == fsnotify.Create {
msg := fmt.Sprintf("dynamicly add watch: %s", event.Name)
err := watcher.Add(event.Name)
catch(err, msg)
log.Println(msg)
}
continue
}
}
for _, name := range files {
if strings.HasSuffix(name, ".wasm") && !strings.HasSuffix(event.Name, ".wasm") {
path := filepath.Join(dir, name)
pluginManifest := extism.Manifest{
Wasm: []extism.Wasm{extism.WasmFile{
Path: path,
}},
}
// load the wasm as an extism plug-in (if cached, use existing plug-in)
var plugin *extism.Plugin
if preloaded, ok := plugins[path]; ok {
plugin = preloaded
} else {
plugin, err = extism.NewPlugin(context.Background(), pluginManifest, extism.PluginConfig{}, nil)
catch(err, fmt.Sprintf("load plugin from wasm: %s", path))
plugin.SetLogLevel(extism.LogLevelDebug)
plugin.SetLogger(func(level extism.LogLevel, msg string) {
log.Printf("[%s] %s", level, msg)
})
plugins[path] = plugin
log.Println("loaded module:", path)
}
// read event trigger file
info, err := os.Stat(event.Name)
catch(err, "stat trigger file")
if info.IsDir() {
continue
}
// if the plug-in doesn't want to use the file from the event, skip the
// event altogether
_, _, err = plugin.Call("should_handle_file", []byte(event.Name))
if err != nil {
// presence of err here indicates to skip the file (avoid copying file)
fmt.Println("should_handle_file:", err)
continue
}
// create input data to share with plug-in
eventFileData, err := os.ReadFile(event.Name)
catch(err, "get target file data")
eventInput := EventInput{
EventFileData: base64.StdEncoding.EncodeToString(eventFileData),
EventFileName: event.Name,
}
input, err := json.Marshal(&eventInput)
catch(err, "serialize event input to json")
// use input bytes and invoke the plug-in function
_, output, err := plugin.Call("on_file_write", input)
catch(err, "calling on_file_write")
log.Printf(
"called on_file_write in plugin: %s [%s]\n", name, event.Name,
)
// take the output bytes from the plug-in and write them to the trigger file
if len(output) != 0 {
var out EventOutput
err := json.Unmarshal(output, &out)
catch(err, "unmarshal plug-in output")
// rather than giving the plug-in access to modify files directly, allow
// it to give the host an instruction, which the host can follow or not
b64file := out.OutputFileData
switch out.Op {
case "overwrite":
data, err := base64.StdEncoding.WithPadding(base64.StdPadding).DecodeString(b64file)
catch(err, "decode output file data for overwrite")
catch(os.WriteFile(event.Name, data, 0755), "writing output to file")
case "create":
data, err := base64.StdEncoding.WithPadding(base64.StdPadding).DecodeString(b64file)
catch(err, "decode output file data for create")
catch(
os.WriteFile(out.OutputFileName, data, 0755),
"create and write to output file",
)
}
}
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
// Block main goroutine forever.
<-make(chan struct{})
}
func catch(err error, msg string) {
if err != nil {
fmt.Println(err.Error(), msg)
os.Exit(1)
}
}