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}