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 "bytes"
9 "context"
10 "crypto/rand"
11 "encoding/json"
12 "errors"
13 "fmt"
14 "io/fs"
15 "os"
16 "path/filepath"
17 "unicode/utf16"
18
19 "github.com/go-git/go-git/v5/plumbing"
20 "github.com/go-git/go-git/v5/plumbing/object"
21 "github.com/go-git/go-git/v5/plumbing/protocol/packp"
22 "github.com/samber/oops"
23 "github.com/tetratelabs/wazero"
24 "github.com/tetratelabs/wazero/api"
25 "github.com/tetratelabs/wazero/imports/assemblyscript"
26 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
27 "github.com/tetratelabs/wazero/sys"
28 pluginLib "gitroot.dev/libs/golang/plugin/model"
29 "gitroot.dev/server/logger"
30 "gitroot.dev/server/repository"
31 "gitroot.dev/server/user"
32)
33
34type runtimeInputsKind int
35
36const (
37 runtimeInputsKindDiff runtimeInputsKind = iota
38 runtimeInputsKindWorktree runtimeInputsKind = iota
39)
40
41type runtimeInputs struct {
42 ctx context.Context
43 repoName string
44 kind runtimeInputsKind
45 plugins []Plugin
46 commands []CommandForDiff
47}
48
49type memoryGarbage struct {
50 module api.Module
51 ptrSize ptrSize
52}
53
54type runtime struct {
55 ctx context.Context
56 manager *Manager
57 logger *logger.Logger
58 repo *repository.GitRootRepository
59 repoWriter *repository.GitRootRepositoryWrite
60 plugin Plugin
61 pluginRun PluginRun
62 command *CommandForDiff
63 commit *commitForDiffCommit
64 wazRun wazero.Runtime
65 commitHook func(h plumbing.Hash)
66 mergeHook func(cmd *packp.Command, pusher user.SimpleUser, toDeleteBranchName string)
67 reports []pluginLib.Report
68 memoryToGarbage []memoryGarbage
69 modulesToGarbage []api.Module
70}
71
72func newRuntime(ctx context.Context, manager *Manager, logger *logger.Logger, runtimeConfig wazero.RuntimeConfig) (*runtime, error) {
73 r := &runtime{
74 ctx: ctx,
75 manager: manager,
76 logger: logger,
77 wazRun: wazero.NewRuntimeWithConfig(ctx, runtimeConfig),
78 commitHook: nil,
79 mergeHook: nil,
80 memoryToGarbage: []memoryGarbage{},
81 modulesToGarbage: []api.Module{},
82 }
83
84 _, err := assemblyscript.Instantiate(ctx, r.wazRun)
85 if err != nil {
86 return nil, err
87 }
88
89 _, err = r.wazRun.
90 NewHostModuleBuilder("gitroot").
91 NewFunctionBuilder().WithFunc(r.ForgeConf).Export("forgeConf").
92 NewFunctionBuilder().WithFunc(r.ModifyContentBuilder(false)).Export("modifyContent").
93 NewFunctionBuilder().WithFunc(r.ModifyContentBuilder(true)).Export("modifyContentAS").
94 NewFunctionBuilder().WithFunc(r.ModifyWebContentBuilder(false)).Export("modifyWebContent").
95 NewFunctionBuilder().WithFunc(r.ModifyWebContentBuilder(true)).Export("modifyWebContentAS").
96 NewFunctionBuilder().WithFunc(r.ReplaceWebContentBuilder(false)).Export("replaceWebContent").
97 NewFunctionBuilder().WithFunc(r.ReplaceWebContentBuilder(true)).Export("replaceWebContentAS").
98 NewFunctionBuilder().WithFunc(r.ModifyCacheContentBuilder(false)).Export("modifyCacheContent").
99 NewFunctionBuilder().WithFunc(r.ModifyCacheContentBuilder(true)).Export("modifyCacheContentAS").
100 NewFunctionBuilder().WithFunc(r.CommitAllBuilder(false)).Export("commitAll").
101 NewFunctionBuilder().WithFunc(r.CommitAllBuilder(true)).Export("commitAllAS").
102 NewFunctionBuilder().WithFunc(r.DiffWithParentBuilder(false)).Export("diffWithParent").
103 NewFunctionBuilder().WithFunc(r.DiffWithParentBuilder(true)).Export("diffWithParentAS").
104 NewFunctionBuilder().WithFunc(r.LogBuilder(false)).Export("log").
105 NewFunctionBuilder().WithFunc(r.LogBuilder(true)).Export("logAS").
106 NewFunctionBuilder().WithFunc(r.LogErrorBuilder(false)).Export("logError").
107 NewFunctionBuilder().WithFunc(r.LogErrorBuilder(true)).Export("logErrorAS").
108 NewFunctionBuilder().WithFunc(r.MergeBuilder(false)).Export("merge").
109 NewFunctionBuilder().WithFunc(r.MergeBuilder(true)).Export("mergeAS").
110 NewFunctionBuilder().WithFunc(r.GetCommitsBuilder(false)).Export("commits").
111 NewFunctionBuilder().WithFunc(r.GetCommitsBuilder(true)).Export("commitsAS").
112 NewFunctionBuilder().WithFunc(r.ExecBuilder(false)).Export("exec").
113 NewFunctionBuilder().WithFunc(r.ExecBuilder(true)).Export("execAS").
114 NewFunctionBuilder().WithFunc(r.ReportBuilder(false)).Export("report").
115 NewFunctionBuilder().WithFunc(r.ReportBuilder(true)).Export("reportAS").
116 NewFunctionBuilder().WithFunc(r.CallBuilder(false)).Export("call").
117 NewFunctionBuilder().WithFunc(r.CallBuilder(true)).Export("callAS").
118 Instantiate(ctx)
119 if err != nil {
120 return nil, err
121 }
122 _, err = wasi_snapshot_preview1.Instantiate(ctx, r.wazRun)
123 if err != nil {
124 return nil, err
125 }
126 return r, nil
127}
128
129type hasBeenMerged struct {
130 cmd *packp.Command
131 pusher user.SimpleUser
132 toDeleteBranchName string
133}
134
135func (r *runtime) listen(c chan runtimeInputs) {
136 postMergeActions := []hasBeenMerged{}
137 for i := range c {
138 r.logger.Debug("listen call", logger.NewLoggerPair("repo", i.repoName), logger.NewLoggerPair("isDiff", i.kind == runtimeInputsKindDiff), logger.NewLoggerPair("isWorktree", i.kind == runtimeInputsKindWorktree))
139 repo, err := r.manager.repoManager.Open(logger.AddCaller(r.ctx, "runtime.listen"), i.repoName)
140 if err != nil {
141 r.logger.Error("open error in listen", err)
142 repo.Close()
143 continue
144 }
145 repoWriter, err := repo.WillWrite(plumbing.HEAD) //TODO should mount good branch directly inside i.commands
146 if err != nil {
147 r.logger.Error("will write error in listen", err, logger.NewLoggerPair("repo", repo.Name()))
148 repo.Close()
149 continue
150 }
151
152 switch i.kind {
153 case runtimeInputsKindDiff:
154 timerStop := r.logger.Time(fmt.Sprintf("Timer %s all plugins", repo.Name()))
155 err := r.start(i.ctx, repo, repoWriter, i.plugins, i.commands, func(cmd *packp.Command, pusher user.SimpleUser, toDeleteBranchName string) {
156 if cmd != nil {
157 postMergeActions = append(postMergeActions, hasBeenMerged{
158 cmd: cmd,
159 pusher: pusher,
160 toDeleteBranchName: toDeleteBranchName,
161 })
162 }
163 })
164 if err != nil {
165 r.logger.Error("start error", err)
166 }
167 timerStop()
168 case runtimeInputsKindWorktree:
169 r.logger.Info("start worktree", logger.NewLoggerPair("repo", repo.Name()))
170 timerStop := r.logger.Time(fmt.Sprintf("Timer %s all plugins worktree", repo.Name()))
171 if err := r.worktree(i.ctx, repo, repoWriter, i.plugins, i.commands); err != nil {
172 r.logger.Error("start error", err)
173 }
174 timerStop()
175 }
176 r.sendReports(i.ctx, repo, repoWriter, i.plugins)
177 r.garbage()
178 repo.Close()
179 for _, postMerge := range postMergeActions {
180 r.manager.backgroundManager.DeleteBranch(i.repoName, postMerge.toDeleteBranchName)
181 r.manager.backgroundManager.PostPush(postMerge.pusher, i.repoName, []*packp.Command{postMerge.cmd})
182 }
183 postMergeActions = nil
184 }
185}
186
187func (r *runtime) loadModule(ctx context.Context, plugin Plugin, withFs fs.FS) (api.Module, error) {
188 m := r.wazRun.Module(plugin.uuid())
189 if m == nil {
190 r.logger.Debug("instantiate plugin conf", logger.NewLoggerPair("name", plugin.Name), logger.NewLoggerPair("with external fs", withFs != nil))
191 config := wazero.NewModuleConfig().
192 WithStdout(os.Stdout).WithStderr(os.Stderr).
193 WithSysWalltime().
194 WithSysNanotime().
195 WithRandSource(rand.Reader).
196 WithName(plugin.uuid()).
197 WithStartFunctions("_initialize", "install")
198
199 if withFs != nil {
200 config = config.WithFS(withFs)
201 } else {
202 r.logger.Warn("no fs mounted", logger.NewLoggerPair("plugin", plugin.Name), logger.NewLoggerPair("repo is nil", r.repo == nil), logger.NewLoggerPair("repoWrite is nil", r.repoWriter == nil))
203 }
204
205 mod, err := r.wazRun.InstantiateModule(ctx, plugin.compiledModule, config)
206 if err != nil {
207 if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
208 fmt.Fprintf(os.Stderr, "exit_code: %d\n", exitErr.ExitCode())
209 return nil, err
210 } else if !ok {
211 return nil, err
212 }
213 }
214 m = mod
215 } else {
216 r.logger.Debug("already exist conf", logger.NewLoggerPair("name", plugin.Name))
217 }
218 r.modulesToGarbage = append(r.modulesToGarbage, m)
219 return m, nil
220}
221
222func (r *runtime) garbageMemory() {
223 byModule := make(map[api.Module][]ptrSize)
224 for _, d := range r.memoryToGarbage {
225 if all, ok := byModule[d.module]; ok {
226 byModule[d.module] = append(all, d.ptrSize)
227 } else {
228 byModule[d.module] = []ptrSize{d.ptrSize}
229 }
230 }
231 for m, ptr := range byModule {
232 r.free(m, ptr)
233 }
234 r.memoryToGarbage = []memoryGarbage{}
235}
236
237func (r *runtime) garbage() {
238 r.garbageMemory()
239 r.reports = []pluginLib.Report{}
240 for _, m := range r.modulesToGarbage {
241 if err := m.Close(r.ctx); err != nil {
242 r.logger.Error("close module fail", err, logger.NewLoggerPair("name", m.Name()))
243 }
244 }
245 r.modulesToGarbage = []api.Module{}
246}
247
248func (r *runtime) ForgeConf(_ context.Context, m api.Module) uint64 {
249 forgeConf, err := json.Marshal(r.manager.conf.ForgeConf())
250 if err != nil {
251 r.logger.Error("ForgeConf can't be serialized", err)
252 return 0
253 }
254 ptrSize, err := r.sendData(m, string(forgeConf))
255 if err != nil {
256 r.logger.Error("ForgeConf can't send data", err)
257 return 0
258 }
259 return ptrSize
260}
261
262func (r *runtime) ModifyContentBuilder(forAS bool) interface{} {
263 f := func(filename string, content string) {
264 if ok, err := checkWrite(r.pluginRun.write.git, r.repoWriter.ToFs(r.ctx), filename); !ok {
265 r.logger.Error("plugin can't write in git", err, logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", filename))
266 return
267 }
268 r.logger.Debug("modifyContent", logger.NewLoggerPair("file", filename))
269 if err := r.repoWriter.Write(filename, []byte(content)); err != nil {
270 r.logger.Error("modifyContent can't open file", err, logger.NewLoggerPair("filepath", filename))
271 return
272 }
273 }
274
275 if !forAS {
276 return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) {
277 filename, err := r.readString(m, "ModifyContent filename", filenamePtr, filenameSize)
278 if err != nil {
279 return
280 }
281 content, err := r.readString(m, "ModifyContent content", contentPtr, contentSize)
282 if err != nil {
283 return
284 }
285 f(filename, content)
286 }
287 } else {
288 return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) {
289 filename, err := r.readASString(m, "ModifyContent filename", filenamePtr)
290 if err != nil {
291 return
292 }
293 content, err := r.readASString(m, "ModifyContent content", contentPtr)
294 if err != nil {
295 return
296 }
297 f(filename, content)
298 }
299 }
300}
301
302func (r *runtime) ModifyWebContentBuilder(forAS bool) interface{} {
303 f := func(filename string, content string) {
304 if r.repo == nil { //in forconf scenario their is no repo
305 r.logger.Error("ModifyWebContentBuilder can't in forconf", errors.New("no repo"), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("filepath", filename))
306 return
307 }
308 if ok, err := checkWrite(r.pluginRun.write.web, r.manager.conf.DataWeb(r.repo.Name()), filename); !ok {
309 r.logger.Warn("plugin can't write in web", logger.NewLoggerPair("err", err), logger.NewLoggerPair("repo", r.repo.Name()), logger.NewLoggerPair("branch", r.command.branch), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", filename))
310 return
311 }
312 fullPath := r.repo.PathDataWeb(filename)
313 dir, _ := filepath.Split(fullPath)
314 if err := os.MkdirAll(dir, os.ModePerm); err != nil {
315 r.logger.Error("modifyWebContent can't mkdirAll", err, logger.NewLoggerPair("filepath", filename))
316 return
317 }
318 if err := os.WriteFile(fullPath, []byte(content), 0666); err != nil {
319 r.logger.Error("modifyWebContent can't open file", err, logger.NewLoggerPair("filepath", filename))
320 return
321 }
322 r.logger.Debug("Write in web", logger.NewLoggerPair("fullPath", fullPath))
323 }
324
325 if !forAS {
326 return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) {
327 filename, err := r.readString(m, "ModifyWebContent filename", filenamePtr, filenameSize)
328 if err != nil {
329 return
330 }
331 content, err := r.readString(m, "ModifyWebContent content", contentPtr, contentSize)
332 if err != nil {
333 return
334 }
335 f(filename, content)
336 }
337 } else {
338 return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) {
339 filename, err := r.readASString(m, "ModifyWebContent filename", filenamePtr)
340 if err != nil {
341 return
342 }
343 content, err := r.readASString(m, "ModifyWebContent content", contentPtr)
344 if err != nil {
345 return
346 }
347 f(filename, content)
348 }
349 }
350}
351
352func (r *runtime) ReplaceWebContentBuilder(forAS bool) interface{} {
353 f := func(filename string, oldContent string, content string) {
354 if r.repo == nil { //in forconf scenario their is no repo
355 r.logger.Error("ReplaceWebContentBuilder can't in forconf", errors.New("no repo"), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("filepath", filename))
356 return
357 }
358 if ok, err := checkWrite(r.pluginRun.write.web, r.manager.conf.DataWeb(r.repo.Name()), filename); !ok {
359 r.logger.Warn("plugin can't write in web for replace", logger.NewLoggerPair("err", err), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", filename))
360 return
361 }
362 fullPath := r.repo.PathDataWeb(filename)
363 fileContent, err := os.ReadFile(fullPath)
364 if err != nil {
365 r.logger.Error("ReplaceWebContentBuilder can't ReadFile", err, logger.NewLoggerPair("filepath", filename))
366 return
367 }
368 if err := os.WriteFile(fullPath, bytes.Replace(fileContent, []byte(oldContent), []byte(content), 1), 0666); err != nil {
369 r.logger.Error("ReplaceWebContentBuilder can't open file", err, logger.NewLoggerPair("filepath", filename))
370 return
371 }
372 r.logger.Debug("Write in web", logger.NewLoggerPair("fullPath", fullPath))
373 }
374
375 if !forAS {
376 return func(_ context.Context, m api.Module, filenamePtr, filenameSize, oldContentPtr, oldContentSize, contentPtr, contentSize uint32) {
377 filename, err := r.readString(m, "ReplaceWebContentBuilder filename", filenamePtr, filenameSize)
378 if err != nil {
379 return
380 }
381 oldContent, err := r.readString(m, "ReplaceWebContentBuilder oldContent", oldContentPtr, oldContentSize)
382 if err != nil {
383 return
384 }
385 content, err := r.readString(m, "ReplaceWebContentBuilder content", contentPtr, contentSize)
386 if err != nil {
387 return
388 }
389 f(filename, oldContent, content)
390 }
391 } else {
392 return func(_ context.Context, m api.Module, filenamePtr, oldContentPtr, contentPtr uint32) {
393 filename, err := r.readASString(m, "ReplaceWebContentBuilder filename", filenamePtr)
394 if err != nil {
395 return
396 }
397 oldContent, err := r.readASString(m, "ReplaceWebContentBuilder oldContent", oldContentPtr)
398 if err != nil {
399 return
400 }
401 content, err := r.readASString(m, "ReplaceWebContentBuilder content", contentPtr)
402 if err != nil {
403 return
404 }
405 f(filename, oldContent, content)
406 }
407 }
408}
409
410func (r *runtime) ModifyCacheContentBuilder(forAS bool) interface{} {
411 f := func(filename string, content string) {
412 r.logger.Debug("ModifyCacheContent", logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("file", filename))
413 if r.repo == nil { //in forconf scenario their is no repo
414 r.logger.Error("ModifyCacheContent can't in forconf", errors.New("no repo"), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("filepath", filename))
415 return
416 }
417 fullPath := filepath.Join(r.manager.conf.PathCache(), r.repo.Name(), r.plugin.Name, filename)
418 dir, _ := filepath.Split(fullPath)
419 if err := os.MkdirAll(dir, os.ModePerm); err != nil {
420 r.logger.Error("ModifyCacheContent can't mkdirAll", err, logger.NewLoggerPair("filepath", filename))
421 return
422 }
423 if err := os.WriteFile(fullPath, []byte(content), 0666); err != nil {
424 r.logger.Error("ModifyCacheContent can't open file", err, logger.NewLoggerPair("filepath", filename))
425 return
426 }
427 r.logger.Debug("Write in cache", logger.NewLoggerPair("fullPath", fullPath))
428 }
429 if !forAS {
430 return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) {
431 filename, err := r.readString(m, "ModifyCacheContent filename", filenamePtr, filenameSize)
432 if err != nil {
433 return
434 }
435 content, err := r.readString(m, "ModifyCacheContent content", contentPtr, contentSize)
436 if err != nil {
437 return
438 }
439 f(filename, content)
440 }
441 } else {
442 return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) {
443 filename, err := r.readASString(m, "ModifyCacheContent filename", filenamePtr)
444 if err != nil {
445 return
446 }
447 content, err := r.readASString(m, "ModifyCacheContent content", contentPtr)
448 if err != nil {
449 return
450 }
451 f(filename, content)
452 }
453 }
454}
455
456func (r *runtime) CommitAllBuilder(forAS bool) interface{} {
457 f := func(msg string, err error) {
458 if err != nil {
459 return
460 }
461 if h, err := r.repoWriter.CommitAll(msg, r.plugin.commiter); err != nil {
462 r.logger.Error("commitAll can't commit", err)
463 return
464 } else {
465 r.logger.Debug("Commit", logger.NewLoggerPair("message", msg), logger.NewLoggerPair("hash", h.String()))
466 if r.commitHook != nil && !h.IsZero() {
467 r.commitHook(h)
468 }
469 }
470 }
471 if !forAS {
472 return func(_ context.Context, m api.Module, offset, byteCount uint32) {
473 f(r.readString(m, "commitAll", offset, byteCount))
474 }
475 } else {
476 return func(_ context.Context, m api.Module, offset uint32) {
477 f(r.readASString(m, "commitAll", offset))
478 }
479 }
480}
481
482func (r *runtime) DiffWithParentBuilder(forAS bool) interface{} {
483 f := func(m api.Module, hash string, oldfile string, newfile string) uint64 {
484 r.logger.Info("In diffWithParent before found ancestor")
485 diffStr, err := r.repoWriter.GetDiff(plumbing.NewHash(string(hash)), string(oldfile), string(newfile))
486 if err != nil {
487 r.logger.Error("GetDiff", err)
488 return 0
489 }
490 r.logger.Debug("In diffWithParent send diff", logger.NewLoggerPair("diff", diffStr))
491 ptrSize, err := r.sendData(m, diffStr)
492 if err != nil {
493 r.logger.Error("can't sendData", err, logger.NewLoggerPair("message", diffStr))
494 return 0
495 }
496 return ptrSize
497 }
498 if !forAS {
499 return func(_ context.Context, m api.Module, hashPtr, hashSize, oldFilenamePtr, oldFilenameSize, newFilenamePtr, newFilenameSize uint32) uint64 {
500 hash, err := r.readString(m, "DiffWithParent hash", hashPtr, hashSize)
501 if err != nil {
502 return 0
503 }
504 oldFilename, err := r.readString(m, "DiffWithParent oldFilename", oldFilenamePtr, oldFilenameSize)
505 if err != nil {
506 return 0
507 }
508 newFilename, err := r.readString(m, "DiffWithParent newFilename", newFilenamePtr, newFilenameSize)
509 if err != nil {
510 return 0
511 }
512 return f(m, hash, oldFilename, newFilename)
513 }
514 } else {
515 return func(_ context.Context, m api.Module, hashPtr, oldFilenamePtr, newFilenamePtr uint32) uint64 {
516 hash, err := r.readASString(m, "DiffWithParent hash", hashPtr)
517 if err != nil {
518 return 0
519 }
520 oldFilename, err := r.readASString(m, "DiffWithParent oldFilenamePtr", oldFilenamePtr)
521 if err != nil {
522 return 0
523 }
524 newFilename, err := r.readASString(m, "DiffWithParent newFilename", newFilenamePtr)
525 if err != nil {
526 return 0
527 }
528 return f(m, hash, oldFilename, newFilename)
529 }
530 }
531}
532
533func (r *runtime) LogBuilder(forAS bool) interface{} {
534 f := func(msg string, err error) {
535 if err != nil {
536 return
537 }
538 repoName := ""
539 if r.repo != nil { //in forconf scenario their is no repo
540 repoName = r.repo.Name()
541 }
542 r.logger.Debug(msg, logger.NewLoggerPair("repo", repoName), logger.NewLoggerPair("plugin", r.plugin.Name))
543 }
544 if !forAS {
545 return func(_ context.Context, m api.Module, offset, byteCount uint32) {
546 f(r.readString(m, "log", offset, byteCount))
547 }
548 } else {
549 return func(_ context.Context, m api.Module, offset uint32) {
550 f(r.readASString(m, "log", offset))
551 }
552 }
553}
554
555func (r *runtime) LogErrorBuilder(forAS bool) interface{} {
556 f := func(msg string, err string) {
557 repoName := ""
558 if r.repo != nil { //in forconf scenario their is no repo
559 repoName = r.repo.Name()
560 }
561 r.logger.Error(msg, errors.New(err), logger.NewLoggerPair("repo", repoName), logger.NewLoggerPair("plugin", r.plugin.Name))
562 }
563 if !forAS {
564 return func(_ context.Context, m api.Module, offset, byteCount, errPtr, errSize uint32) {
565 msg, err := r.readString(m, "LogError msg", offset, byteCount)
566 if err != nil {
567 return
568 }
569 errMsg, err := r.readString(m, "LogError err", errPtr, errSize)
570 if err != nil {
571 return
572 }
573 f(msg, errMsg)
574 }
575 } else {
576 return func(_ context.Context, m api.Module, offset, errPtr uint32) {
577 msg, err := r.readASString(m, "LogError msg", offset)
578 if err != nil {
579 return
580 }
581 errMsg, err := r.readASString(m, "LogError err", errPtr)
582 if err != nil {
583 return
584 }
585 f(msg, errMsg)
586 }
587 }
588}
589
590func (r *runtime) MergeBuilder(forAS bool) interface{} {
591 f := func(from string, to string) {
592 r.logger.Debug("try to merge", logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
593 cmd, err := r.repoWriter.Merge(from, to, r.plugin.commiter, r.command.pusher)
594 if err != nil {
595 r.logger.Error("can't Merge", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
596 }
597 if r.mergeHook != nil {
598 r.mergeHook(cmd, r.command.pusher, to)
599 }
600 }
601 if !forAS {
602 return func(_ context.Context, m api.Module, fromPtr, fromSize, toPtr, toSize uint32) {
603 from, err := r.readString(m, "Merge from", fromPtr, fromSize)
604 if err != nil {
605 return
606 }
607 to, err := r.readString(m, "Merge to", toPtr, toSize)
608 if err != nil {
609 return
610 }
611 f(from, to)
612 }
613 } else {
614 return func(_ context.Context, m api.Module, fromPtr, toPtr uint32) {
615 from, err := r.readASString(m, "Merge from", fromPtr)
616 if err != nil {
617 return
618 }
619 to, err := r.readASString(m, "Merge to", toPtr)
620 if err != nil {
621 return
622 }
623 f(from, to)
624 }
625 }
626}
627
628func (r *runtime) GetCommitsBuilder(forAS bool) interface{} {
629 f := func(m api.Module, from string, to string) uint64 {
630 commits := []commitForDiffCommit{}
631 if err := r.repoWriter.WalkCommit(plumbing.NewHash(to), plumbing.NewHash(from), func(c *object.Commit) error {
632 r.logger.Debug("found commit", logger.NewLoggerPair("hash", c.Hash.String()), logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
633 commits = append(commits, commitToCommitForDiff(c, nil))
634 return nil
635 }); err != nil {
636 r.logger.Error("can't WalkCommit", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
637 }
638 r.logger.Debug("found commits", logger.NewLoggerPair("nb", len(commits)), logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
639 j, err := Marshall(r.command.branch.Short(), commits)
640 if err != nil {
641 r.logger.Error("can't Marshall", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to))
642 }
643 ptrSize, err := r.sendData(m, j)
644 if err != nil {
645 r.logger.Error("can't sendData", err, logger.NewLoggerPair("message", j))
646 return 0
647 }
648 return ptrSize
649 }
650 if !forAS {
651 return func(_ context.Context, m api.Module, fromPtr, fromSize, toPtr, toSize uint32) uint64 {
652 from, err := r.readString(m, "Merge from", fromPtr, fromSize)
653 if err != nil {
654 return 0
655 }
656 to, err := r.readString(m, "Merge to", toPtr, toSize)
657 if err != nil {
658 return 0
659 }
660 return f(m, from, to)
661 }
662 } else {
663 return func(_ context.Context, m api.Module, fromPtr, toPtr uint32) uint64 {
664 from, err := r.readASString(m, "Merge from", fromPtr)
665 if err != nil {
666 return 0
667 }
668 to, err := r.readASString(m, "Merge to", toPtr)
669 if err != nil {
670 return 0
671 }
672 return f(m, from, to)
673 }
674 }
675}
676
677func (r *runtime) ExecBuilder(forAS bool) interface{} {
678 f := func(m api.Module, cmd string) uint64 {
679 exec := pluginLib.Exec{}
680 err := json.Unmarshal([]byte(cmd), &exec)
681 if err != nil {
682 r.logger.Error("can't exec bad format", err)
683 return 0
684 }
685 if ok, err := checkExec(r.pluginRun.write.exec, exec); !ok {
686 r.logger.Warn("plugin can't exec", logger.NewLoggerPair("err", err), logger.NewLoggerPair("repo", r.repo.Name()), logger.NewLoggerPair("branch", r.command.branch), logger.NewLoggerPair("plugin", r.plugin.Name))
687 return 0
688 }
689 j, err := r.manager.execManager.Exec(r.repo, r.command.branch.Short(), exec)
690 if err != nil {
691 r.logger.Error("can't exec", err)
692 return 0
693 }
694 jjson, err := json.Marshal(j)
695 if err != nil {
696 r.logger.Error("can't marshal execStatus", err)
697 return 0
698 }
699 ptrSize, err := r.sendData(m, string(jjson))
700 if err != nil {
701 r.logger.Error("can't sendData", err, logger.NewLoggerPair("message", j))
702 return 0
703 }
704 return ptrSize
705 }
706 if !forAS {
707 return func(_ context.Context, m api.Module, commandPtr, commandSize uint32) uint64 {
708 cmd, err := r.readString(m, "exec cmd", commandPtr, commandSize)
709 if err != nil {
710 return 0
711 }
712 return f(m, cmd)
713 }
714 } else {
715 return func(_ context.Context, m api.Module, commandPtr uint32) uint64 {
716 cmd, err := r.readASString(m, "exec cmd", commandPtr)
717 if err != nil {
718 return 0
719 }
720 return f(m, cmd)
721 }
722 }
723}
724
725func (r *runtime) ReportBuilder(forAS bool) interface{} {
726 f := func(reportJson string) {
727 if r.command == nil || r.commit == nil {
728 r.logger.Error("can't report in conf stage", errors.New("report not allowed"), logger.NewLoggerPair("plugin", r.plugin.Name))
729 return
730 }
731 report := pluginLib.ReportToGitroot{}
732 err := json.Unmarshal([]byte(reportJson), &report)
733 if err != nil {
734 r.logger.Error("can't unmarshal reportJson bad format", err)
735 return
736 }
737 re := pluginLib.Report{
738 Level: report.Level,
739 Content: report.Content,
740 FromPlugin: r.plugin.Name,
741 FromBranch: r.command.branch.Short(),
742 FromCommit: r.commit.hash.String(),
743 }
744 reJson, err := json.Marshal(re)
745 if err != nil {
746 r.logger.Error("can't marshal reJson bad format", err)
747 return
748 }
749 r.logger.Debug("report added", logger.NewLoggerPair("report", reJson))
750 r.reports = append(r.reports, re)
751 }
752 if !forAS {
753 return func(_ context.Context, m api.Module, reportPtr, reportSize uint32) {
754 reportJson, err := r.readString(m, "report json", reportPtr, reportSize)
755 if err != nil {
756 return
757 }
758 f(reportJson)
759 }
760 } else {
761 return func(_ context.Context, m api.Module, reportPtr uint32) {
762 reportJson, err := r.readASString(m, "report json", reportPtr)
763 if err != nil {
764 return
765 }
766 f(reportJson)
767 }
768 }
769}
770
771func (r *runtime) CallBuilder(forAS bool) interface{} {
772 f := func(callJson string) (uint64, api.Module) {
773 call := pluginLib.Call{}
774 err := json.Unmarshal([]byte(callJson), &call)
775 if err != nil {
776 r.logger.Error("can't unmarshal callJson bad format", err)
777 return 0, nil
778 }
779 plugins, err := r.manager.Availables(r.ctx)
780 if err != nil {
781 r.logger.Error("can't get availables plugins", err)
782 return 0, nil
783 }
784 for _, plugin := range plugins {
785 if plugin.Name == call.Plugin {
786 m, err := r.loadModule(r.ctx, plugin, nil) //TODO where to find fs?
787 if err != nil {
788 r.logger.Error("can't loadModule", err, logger.NewLoggerPair("plugin", plugin.Name))
789 return 0, nil
790 }
791 funcToCall := m.ExportedFunction("call")
792 malloc := m.ExportedFunction("gitrootAlloc")
793 if malloc == nil {
794 malloc = m.ExportedFunction("malloc")
795 }
796 callResPtrSize, err := r.writeMemoryAndCallWithRes(m, funcToCall, malloc, callJson)
797 if err != nil {
798 r.logger.Error("can't writeMemoryAndCallWithRes", err, logger.NewLoggerPair("plugin", plugin.Name))
799 return 0, nil
800 }
801 return callResPtrSize, m
802 }
803 }
804 r.logger.Error("plugin not found", err, logger.NewLoggerPair("plugin", call.Plugin), logger.NewLoggerPair("method", call.Name))
805 return 0, nil
806 }
807 callResJson, _ := json.Marshal(pluginLib.CallRes{Err: "plugin or method not found"})
808 if !forAS {
809 return func(_ context.Context, m api.Module, callPtr, callSize uint32) uint64 {
810 callJson, err := r.readString(m, "call json", callPtr, callSize)
811 if err != nil {
812 r.logger.Error("can't readString call", err)
813 return 0
814 }
815 r.logger.Info("call json", logger.NewLoggerPair("data", string(callJson)))
816 callResPtrSize, newModule := f(callJson)
817 if callResPtrSize > 0 && newModule != nil {
818 ptrCall := uint32(callResPtrSize >> 32)
819 sizeCall := uint32(callResPtrSize)
820 callResJsonStr, err := r.readString(newModule, "can't read string in call", ptrCall, sizeCall)
821 if err != nil {
822 r.logger.Error("can't readString callRes", err)
823 return 0
824 }
825 callResJson = []byte(callResJsonStr)
826 }
827 r.logger.Info("call res data", logger.NewLoggerPair("data", string(callResJson)))
828 ptrSize, err := r.sendData(m, string(callResJson))
829 if err != nil {
830 r.logger.Error("can't sendData callRes", err)
831 return 0
832 }
833 return ptrSize
834 }
835 } else {
836 return func(_ context.Context, m api.Module, callPtr uint32) uint64 {
837 callJson, err := r.readASString(m, "call json", callPtr)
838 if err != nil {
839 r.logger.Error("can't readASString call", err)
840 return 0
841 }
842 callResPtrSize, newModule := f(callJson)
843 if callResPtrSize > 0 && newModule != nil {
844 callResJsonStr, err := r.readASString(newModule, "can't read asstring in call", uint32(callResPtrSize))
845 if err != nil {
846 r.logger.Error("can't readASString callRes", err)
847 return 0
848 }
849 callResJson = []byte(callResJsonStr)
850 }
851 ptrSize, err := r.sendData(m, string(callResJson))
852 if err != nil {
853 r.logger.Error("can't sendData callRes", err)
854 return 0
855 }
856 return ptrSize
857 }
858 }
859}
860
861func (r *runtime) Close() error {
862 return r.wazRun.Close(r.ctx)
863}
864
865func (r *runtime) sendData(module api.Module, message string) (ptrSizeToSend uint64, err error) {
866 malloc := module.ExportedFunction("gitrootAlloc")
867 if malloc == nil {
868 malloc = module.ExportedFunction("malloc")
869 }
870
871 s := uint64(len(message))
872
873 results, err := malloc.Call(r.ctx, s)
874 if err != nil {
875 return 0, oops.Wrapf(err, "can't malloc memory")
876 }
877 ptrA := results[0]
878
879 // The pointer is a linear memory offset, which is where we write the name.
880 if !module.Memory().WriteString(uint32(ptrA), message) {
881 return 0, oops.Errorf("can't write memory")
882 }
883
884 r.memoryToGarbage = append(r.memoryToGarbage, memoryGarbage{module: module, ptrSize: ptrSize{ptr: ptrA, size: s}})
885
886 return (ptrA << uint64(32)) | s, nil
887}
888
889func (r *runtime) readString(m api.Module, errorMsg string, ptr uint32, size uint32) (string, error) {
890 mem := m.Memory()
891 if mem == nil {
892 err := errors.New("memory not exist")
893 r.logger.Error(errorMsg, err, logger.NewLoggerPair("ptr", ptr), logger.NewLoggerPair("size", size))
894 return "", err
895 }
896 txt, ok := mem.Read(ptr, size)
897 if !ok {
898 err := errors.New("memory read fail")
899 r.logger.Error(errorMsg, err, logger.NewLoggerPair("ptr", ptr), logger.NewLoggerPair("size", size))
900 return "", err
901 }
902 return string(txt), nil
903}
904
905func (r *runtime) readASString(m api.Module, errorMsg string, offset uint32) (string, error) {
906 // Length is four bytes before pointer.
907 byteCount, ok := m.Memory().ReadUint32Le(offset - 4)
908 if !ok || byteCount%2 != 0 {
909 err := errors.New("Memory.ReadUint32Le fail")
910 r.logger.Error(errorMsg, err, logger.NewLoggerPair("byteCount", byteCount), logger.NewLoggerPair("offset", offset))
911 return "", err
912 }
913 buf, ok := m.Memory().Read(offset, byteCount)
914 if !ok {
915 err := errors.New("Memory.Read fail")
916 r.logger.Error(errorMsg, err, logger.NewLoggerPair("offset", offset), logger.NewLoggerPair("byteCount", byteCount))
917 return "", err
918 }
919 u16s := make([]uint16, len(buf)/2)
920
921 lb := len(buf)
922 for i := 0; i < lb; i += 2 {
923 u16s[i/2] = uint16(buf[i]) + (uint16(buf[i+1]) << 8)
924 }
925 return string(utf16.Decode(u16s)), nil
926}
927
928type ptrSize struct {
929 ptr uint64
930 size uint64
931}
932
933func (r *runtime) free(module api.Module, ptrSize []ptrSize) error {
934 free := module.ExportedFunction("gitrootFree")
935 if free == nil {
936 free = module.ExportedFunction("free")
937 }
938
939 if free != nil && len(free.Definition().ParamTypes()) == 1 {
940 for _, p := range ptrSize {
941 _, err := free.Call(r.ctx, p.ptr)
942 if err != nil {
943 r.logger.Error("can't free pluginConf 1 param", err, logger.NewLoggerPair("plugin", module.Name()))
944 }
945 }
946 } else if free != nil {
947 for _, p := range ptrSize {
948 _, err := free.Call(r.ctx, p.ptr, p.size)
949 if err != nil {
950 r.logger.Error("can't free pluginConf", err, logger.NewLoggerPair("plugin", module.Name()))
951 }
952 }
953 }
954
955 return nil
956}