GitRoot
craft your forge, build your project, grow your community freely
1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5package plugin
6
7import (
8 "context"
9 "errors"
10 "fmt"
11 "os"
12 "path/filepath"
13
14 "github.com/go-git/go-git/v5/plumbing"
15 "github.com/samber/oops"
16 "github.com/tetratelabs/wazero"
17 "github.com/tetratelabs/wazero/api"
18 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
19 "github.com/tetratelabs/wazero/sys"
20 pluginLib "gitroot.dev/libs/golang/plugin"
21 grfs "gitroot.dev/server/fs"
22 "gitroot.dev/server/logger"
23 "gitroot.dev/server/repository"
24)
25
26type runtimeInputsKind int
27
28const (
29 runtimeInputsKindDiff runtimeInputsKind = iota
30 runtimeInputsKindWorktree runtimeInputsKind = iota
31)
32
33type runtimeInputs struct {
34 ctx context.Context
35 repoName string
36 kind runtimeInputsKind
37 plugins []Plugin
38 commands []CommandForDiff
39}
40
41type runtime struct {
42 ctx context.Context
43 manager *Manager
44 logger *logger.Logger
45 repo *repository.GitRootRepository
46 repoWriter *repository.GitRootRepositoryWrite
47 plugin Plugin
48 pluginRun PluginRun
49 command *CommandForDiff
50 wazRun wazero.Runtime
51 fsWorktree grfs.UpdatableFs
52 fsWebcontent grfs.UpdatableFs
53 fsCache grfs.UpdatableFs
54 commitHook func(h plumbing.Hash)
55}
56
57func newRuntime(ctx context.Context, manager *Manager, logger *logger.Logger) (*runtime, error) {
58 r := &runtime{
59 ctx: ctx,
60 manager: manager,
61 logger: logger,
62 wazRun: wazero.NewRuntime(ctx),
63 fsWorktree: grfs.NewUpdatableFs(ctx, nil),
64 fsWebcontent: grfs.NewUpdatableFs(ctx, nil),
65 fsCache: grfs.NewUpdatableFs(ctx, nil),
66 commitHook: nil,
67 }
68 _, err := r.wazRun.
69 NewHostModuleBuilder("gitroot").
70 NewFunctionBuilder().WithFunc(r.ModifyContent).Export("modifyContent").
71 NewFunctionBuilder().WithFunc(r.ModifyWebContent).Export("modifyWebContent").
72 NewFunctionBuilder().WithFunc(r.ModifyCacheContent).Export("modifyCacheContent").
73 NewFunctionBuilder().WithFunc(r.CommitAll).Export("commitAll").
74 NewFunctionBuilder().WithFunc(r.DiffWithParent).Export("diffWithParent").
75 NewFunctionBuilder().WithFunc(r.Log).Export("log").
76 NewFunctionBuilder().WithFunc(r.LogError).Export("logError").
77 NewFunctionBuilder().WithFunc(r.Merge).Export("merge").
78 Instantiate(ctx)
79 if err != nil {
80 return nil, err
81 }
82 _, err = wasi_snapshot_preview1.Instantiate(ctx, r.wazRun)
83 if err != nil {
84 return nil, err
85 }
86 return r, nil
87}
88
89func (r *runtime) listen(c chan runtimeInputs) {
90 for i := range c {
91 repo, err := r.manager.repoManager.Open(logger.AddCaller(r.ctx, "runtime.listen"), i.repoName)
92 if err != nil {
93 r.logger.Error("open error in listen", err)
94 repo.Close()
95 continue
96 }
97 repoWriter, err := repo.WillWrite(plumbing.HEAD) //TODO should mount good branc directly inside i.commands
98 if err != nil {
99 r.logger.Error("will write error in listen", err, logger.NewLoggerPair("repo", repo.Name()))
100 repo.Close()
101 continue
102 }
103
104 if i.kind == runtimeInputsKindDiff {
105 timerStop := r.logger.Time(fmt.Sprintf("Timer %s all plugins", repo.Name()))
106 if err := r.start(i.ctx, repo, repoWriter, i.plugins, i.commands); err != nil {
107 r.logger.Error("start error", err)
108 }
109 timerStop()
110 } else if i.kind == runtimeInputsKindWorktree {
111 r.logger.Info("start worktree", logger.NewLoggerPair("repo", repo.Name()))
112 timerStop := r.logger.Time(fmt.Sprintf("Timer %s all plugins worktree", repo.Name()))
113 if err := r.worktree(i.ctx, repo, repoWriter, i.plugins, i.commands); err != nil {
114 r.logger.Error("start error", err)
115 }
116 timerStop()
117 }
118 repo.Close()
119 }
120}
121
122func (r *runtime) conf(ctx context.Context, plugin Plugin) ([]pluginLib.PluginRun, error) {
123 r.fsWorktree.Clear()
124 r.fsWebcontent.Clear()
125 r.fsCache.Clear()
126 r.repo = nil
127 r.plugin = plugin
128 r.command = nil
129 timerStop := r.logger.Time(fmt.Sprintf("Timer %s", plugin.Name))
130
131 r.logger.Debug("start plugin conf", logger.NewLoggerPair("name", plugin.Name))
132 m := r.wazRun.Module(plugin.Name)
133 if m == nil {
134 r.logger.Debug("instantiate plugin conf", logger.NewLoggerPair("name", plugin.Name))
135 config := wazero.NewModuleConfig().
136 WithStdout(os.Stdout).WithStderr(os.Stderr).
137 WithFSConfig(
138 wazero.NewFSConfig().
139 WithFSMount(r.fsWorktree, "worktree").
140 WithFSMount(r.fsWebcontent, "webcontent").
141 WithFSMount(r.fsCache, "cache"))
142 mod, err := r.wazRun.InstantiateWithConfig(ctx, plugin.content, config.WithName(plugin.Name).WithStartFunctions("_initialize", "install"))
143 if err != nil {
144 if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
145 fmt.Fprintf(os.Stderr, "exit_code: %d\n", exitErr.ExitCode())
146 return nil, err
147 } else if !ok {
148 return nil, err
149 }
150 }
151 m = mod
152 } else {
153 r.logger.Debug("already exist conf", logger.NewLoggerPair("name", plugin.Name))
154 }
155 conf, err := callPluginForConf(ctx, m, r.logger.NewSubLogger(plugin.Name))
156 if err != nil {
157 r.logger.Error("finish plugin conf with error", err, logger.NewLoggerPair("name", plugin.Name))
158 }
159
160 r.logger.Debug("finish plugin conf", logger.NewLoggerPair("name", plugin.Name))
161 timerStop()
162 return conf, err
163}
164
165func (r *runtime) start(ctx context.Context, repo *repository.GitRootRepository, repoWriter *repository.GitRootRepositoryWrite, plugins []Plugin, commands []CommandForDiff) error {
166 r.fsWorktree.Update(repoWriter.ToFs(ctx))
167 r.fsWebcontent.Update(r.manager.conf.DataWeb(repo.Name()))
168
169 r.repo = repo
170 r.repoWriter = repoWriter
171 r.command = nil
172
173 r.commitHook = func(hash plumbing.Hash) {
174 command, err := repoWriter.GetLastCommit(hash)
175 if err != nil {
176 r.logger.Error("can't GetLastCommit in start runtime", err)
177 return
178 }
179 newCmd, err := CommandForDiffFromCommitCmd(ctx, repo, r.plugin, command, *r.command)
180 if err != nil {
181 r.logger.Error("can't CommandForDiffFromCommitCmd in start runtime", err)
182 return
183 }
184 commands = append(commands, newCmd)
185 }
186 defer func() {
187 r.commitHook = nil
188 }()
189
190 for _, plugin := range plugins {
191 r.fsCache.Update(r.manager.conf.Cache(repo.Name(), plugin.Name))
192 r.plugin = plugin
193 timerStop := r.logger.Time(fmt.Sprintf("Timer %s", plugin.Name))
194 r.logger.Debug("start plugin", logger.NewLoggerPair("name", plugin.Name))
195 m := r.wazRun.Module(plugin.Name)
196 if m == nil {
197 r.logger.Debug("instantiate plugin", logger.NewLoggerPair("name", plugin.Name))
198 config := wazero.NewModuleConfig().
199 WithStdout(os.Stdout).WithStderr(os.Stderr).
200 WithFSConfig(wazero.NewFSConfig().
201 WithFSMount(r.fsWorktree, "worktree").
202 WithFSMount(r.fsWebcontent, "webcontent").
203 WithFSMount(r.fsCache, "cache"))
204 mod, err := r.wazRun.InstantiateWithConfig(ctx, plugin.content, config.WithName(plugin.Name).WithStartFunctions("_initialize", "install"))
205 if err != nil {
206 if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
207 fmt.Fprintf(os.Stderr, "exit_code: %d\n", exitErr.ExitCode())
208 return nil
209 } else if !ok {
210 return err
211 }
212 }
213 m = mod
214 } else {
215 r.logger.Debug("already exist", logger.NewLoggerPair("name", plugin.Name))
216 }
217 l := logger.NewLogger(logger.WASM)
218 l.Debug("memory before", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Name))
219 cp := callPlugin{
220 manager: r.manager,
221 plugin: plugin,
222 repo: repo,
223 repoWriter: repoWriter,
224 module: m,
225 logger: r.logger.NewSubLogger(plugin.Name),
226 }
227 if err := cp.callPluginForDiff(ctx, r, commands); err != nil {
228 r.logger.Error("finish plugin with error", err, logger.NewLoggerPair("name", plugin.Name))
229 }
230 l.Debug("memory after", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Name))
231 r.logger.Debug("finish plugin", logger.NewLoggerPair("name", plugin.Name))
232 timerStop()
233 }
234 return nil
235}
236
237func (r *runtime) ModifyContent(_ context.Context, m api.Module, offset, byteCount, offset2, byteCount2 uint32) {
238 pathByte, ok := m.Memory().Read(offset, byteCount)
239 if !ok {
240 r.logger.Error("modifyContent can't read filepath", errors.New("Memory.Read out of range"), logger.NewLoggerPair("offset", offset), logger.NewLoggerPair("byteCount", byteCount))
241 return
242 }
243 path := string(pathByte)
244 if ok, err := checkWrite(r.pluginRun.write.git, r.fsWorktree, path); !ok {
245 r.logger.Error("plugin can't write in git", err, logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", path))
246 return
247 }
248 content, ok2 := m.Memory().Read(offset2, byteCount2)
249 if !ok2 {
250 r.logger.Error("modifyContent can't read file content", errors.New("Memory.Read out of range"), logger.NewLoggerPair("offset2", offset2), logger.NewLoggerPair("byteCount2", byteCount2))
251 return
252 }
253 r.logger.Debug("modifyContent", logger.NewLoggerPair("file", path))
254 if err := r.repoWriter.Write(path, content); err != nil {
255 r.logger.Error("modifyContent can't open file", err, logger.NewLoggerPair("filepath", path))
256 return
257 }
258}
259
260func (r *runtime) ModifyWebContent(_ context.Context, m api.Module, offset, byteCount, offset2, byteCount2 uint32) {
261 pathByte, ok := m.Memory().Read(offset, byteCount)
262 if !ok {
263 r.logger.Error("modifyWebContent can't read filepath", errors.New("Memory.Read out of range"), logger.NewLoggerPair("offset", offset), logger.NewLoggerPair("byteCount", byteCount))
264 return
265 }
266 path := string(pathByte)
267 if ok, err := checkWrite(r.pluginRun.write.web, r.fsWebcontent, path); !ok {
268 r.logger.Error("plugin can't write in web", err, logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", path))
269 return
270 }
271 content, ok2 := m.Memory().Read(offset2, byteCount2)
272 if !ok2 {
273 r.logger.Error("modifyWebContent can't read file content", errors.New("Memory.Read out of range"), logger.NewLoggerPair("offset2", offset2), logger.NewLoggerPair("byteCount2", byteCount2))
274 return
275 }
276 fullPath := r.repo.PathDataWeb(path)
277 dir, _ := filepath.Split(fullPath)
278 if err := os.MkdirAll(dir, os.ModePerm); err != nil {
279 r.logger.Error("modifyWebContent can't mkdirAll", err, logger.NewLoggerPair("filepath", path))
280 return
281 }
282 if err := os.WriteFile(fullPath, content, 0666); err != nil {
283 r.logger.Error("modifyWebContent can't open file", err, logger.NewLoggerPair("filepath", path))
284 return
285 }
286 r.logger.Debug("Write in web", logger.NewLoggerPair("fullPath", fullPath))
287}
288
289func (r *runtime) ModifyCacheContent(_ context.Context, m api.Module, offset, byteCount, offset2, byteCount2 uint32) {
290 buf, ok := m.Memory().Read(offset, byteCount)
291 if !ok {
292 r.logger.Error("ModifyCacheContent can't read filepath", errors.New("Memory.Read out of range"), logger.NewLoggerPair("offset", offset), logger.NewLoggerPair("byteCount", byteCount))
293 return
294 }
295 buf2, ok2 := m.Memory().Read(offset2, byteCount2)
296 if !ok2 {
297 r.logger.Error("ModifyCacheContent can't read file content", errors.New("Memory.Read out of range"), logger.NewLoggerPair("offset2", offset2), logger.NewLoggerPair("byteCount2", byteCount2))
298 return
299 }
300 fullPath := filepath.Join(r.manager.conf.PathCache(), r.repo.Name(), r.plugin.Name, string(buf))
301 dir, _ := filepath.Split(fullPath)
302 if err := os.MkdirAll(dir, os.ModePerm); err != nil {
303 r.logger.Error("ModifyCacheContent can't mkdirAll", err, logger.NewLoggerPair("filepath", string(buf)))
304 return
305 }
306 if err := os.WriteFile(fullPath, buf2, 0666); err != nil {
307 r.logger.Error("ModifyCacheContent can't open file", err, logger.NewLoggerPair("filepath", string(buf)))
308 return
309 }
310 r.logger.Debug("Write in cache", logger.NewLoggerPair("fullPath", fullPath), logger.NewLoggerPair("content", string(buf2)))
311}
312
313func (r *runtime) CommitAll(_ context.Context, m api.Module, offset, byteCount uint32) {
314 buf, ok := m.Memory().Read(offset, byteCount)
315 if !ok {
316 r.logger.Error("commitAll can't read message", errors.New("Memory.Read out of range"), logger.NewLoggerPair("offset", offset), logger.NewLoggerPair("byteCount", byteCount))
317 return
318 }
319
320 if h, err := r.repoWriter.CommitAll(string(buf), r.plugin.commiter); err != nil {
321 r.logger.Error("commitAll can't commit", err)
322 return
323 } else {
324 r.logger.Debug("Commit", logger.NewLoggerPair("message", string(buf)), logger.NewLoggerPair("hash", h.String()))
325 if r.commitHook != nil && !h.IsZero() {
326 r.commitHook(h)
327 }
328 }
329}
330
331func (r *runtime) DiffWithParent(ctx context.Context, m api.Module, filePtr, fileSize, hashPtr, hashSize, resPtr uint32) (resSize uint32) {
332 r.logger.Debug("In diffWithParent server side")
333 file, ok := m.Memory().Read(filePtr, fileSize)
334 if !ok {
335 r.logger.Error("diffWithParent can't read file", errors.New("Memory.Read out of range"), logger.NewLoggerPair("ptr", filePtr), logger.NewLoggerPair("size", fileSize))
336 return
337 }
338 hash, ok := m.Memory().Read(hashPtr, hashSize)
339 if !ok {
340 r.logger.Error("diffWithParent can't read hash", errors.New("Memory.Read out of range"), logger.NewLoggerPair("ptr", hashPtr), logger.NewLoggerPair("size", hashSize))
341 return
342 }
343 r.logger.Info("In diffWithParent before found ancestor")
344 diffStr, err := r.repoWriter.GetDiff(plumbing.NewHash(string(hash)), string(file))
345 if err != nil {
346 r.logger.Error("GetDiff", err)
347 return
348 }
349 r.logger.Debug("In diffWithParent send diff", logger.NewLoggerPair("diff", diffStr))
350 size, err := sendData(m, diffStr, resPtr)
351 if err != nil {
352 r.logger.Error("can't sendData", err, logger.NewLoggerPair("message", diffStr))
353 return 0
354 }
355 return size
356}
357
358func (r *runtime) Log(_ context.Context, m api.Module, offset, byteCount uint32) {
359 buf, ok := m.Memory().Read(offset, byteCount)
360 if !ok {
361 r.logger.Error("log can't read message", errors.New("Memory.Read out of range"), logger.NewLoggerPair("offset", offset), logger.NewLoggerPair("byteCount", byteCount))
362 return
363 }
364 r.logger.Debug(string(buf), logger.NewLoggerPair("repo", r.repo.Name()), logger.NewLoggerPair("plugin", r.plugin.Name))
365}
366
367func (r *runtime) LogError(_ context.Context, m api.Module, offset, byteCount, errPtr, errSize uint32) {
368 buf, ok := m.Memory().Read(offset, byteCount)
369 if !ok {
370 r.logger.Error("logError can't read message", errors.New("Memory.Read out of range"), logger.NewLoggerPair("offset", offset), logger.NewLoggerPair("byteCount", byteCount))
371 return
372 }
373 bufErr, ok2 := m.Memory().Read(errPtr, errSize)
374 if !ok2 {
375 r.logger.Error("logError can't read message", errors.New("Memory.Read out of range err"), logger.NewLoggerPair("errPtr", errPtr), logger.NewLoggerPair("errSize", errSize))
376 return
377 }
378 r.logger.Error(string(buf), errors.New(string(bufErr)), logger.NewLoggerPair("repo", r.repo.Name()), logger.NewLoggerPair("plugin", r.plugin.Name))
379}
380
381func (r *runtime) Merge(_ context.Context, m api.Module, fromPtr, fromSize, toPtr, toSize uint32) {
382 from, ok := m.Memory().Read(fromPtr, fromSize)
383 if !ok {
384 r.logger.Error("merge can't read message", errors.New("Memory.Read out of range"), logger.NewLoggerPair("fromPtr", fromPtr), logger.NewLoggerPair("fromSize", fromSize))
385 return
386 }
387 to, ok := m.Memory().Read(toPtr, toSize)
388 if !ok {
389 r.logger.Error("merge can't read message", errors.New("Memory.Read out of range"), logger.NewLoggerPair("toPtr", toPtr), logger.NewLoggerPair("toSize", toSize))
390 return
391 }
392 r.logger.Debug("try to merge", logger.NewLoggerPair("from", string(from)), logger.NewLoggerPair("to", string(to)))
393 if err := r.repoWriter.Merge(string(from), string(to), r.plugin.commiter, r.command.pusher); err != nil {
394 r.logger.Error("can't Merge", err, logger.NewLoggerPair("from", string(from)), logger.NewLoggerPair("to", string(to)))
395 }
396}
397
398func (r *runtime) Close() error {
399 return r.wazRun.Close(r.ctx)
400}
401
402func sendData(module api.Module, message string, ptr uint32) (size uint32, err error) {
403 s := uint32(len(message))
404
405 // The pointer is a linear memory offset, which is where we write the name.
406 if !module.Memory().WriteString(ptr, message) {
407 return 0, oops.Errorf("can't write memory")
408 }
409 return s, nil
410}