From 6393b3d8e9a4e8bb86bcaa41ae9dc2b4d71fa2f2 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sun, 17 Dec 2023 19:54:46 +0100 Subject: [PATCH] wip: hotreload (iter 2) Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/main.go | 40 +++-- contribs/gnodev/pkg/dev/events.go | 26 +++ contribs/gnodev/pkg/dev/node.go | 28 ++- contribs/gnodev/pkg/events/events.go | 17 -- contribs/gnodev/pkg/events/reload_test.go | 7 - contribs/gnodev/pkg/events/server.go | 9 +- .../gnodev/pkg/events/static/hotreload.js | 1 - contribs/gnodev/pkg/watcher/events.go | 37 ++++ contribs/gnodev/pkg/watcher/watch.go | 165 ++++++++++++++++++ 9 files changed, 276 insertions(+), 54 deletions(-) create mode 100644 contribs/gnodev/pkg/dev/events.go delete mode 100644 contribs/gnodev/pkg/events/reload_test.go create mode 100644 contribs/gnodev/pkg/watcher/events.go create mode 100644 contribs/gnodev/pkg/watcher/watch.go diff --git a/contribs/gnodev/main.go b/contribs/gnodev/main.go index b234110fadb..744fb49882d 100644 --- a/contribs/gnodev/main.go +++ b/contribs/gnodev/main.go @@ -15,6 +15,7 @@ import ( gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/contribs/gnodev/pkg/events" "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" + "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" "github.com/gnolang/gno/gno.land/pkg/gnoweb" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnomod" @@ -127,6 +128,8 @@ func execDev(cfg *devCfg, args []string, io commands.IO) error { cancel(nil) }) + emitter := events.NewEmitter() + // Setup Dev Node // XXX: find a good way to export or display node logs devNode, err := setupDevNode(ctx, rt, pkgpaths, gnoroot) @@ -140,13 +143,13 @@ func execDev(cfg *devCfg, args []string, io commands.IO) error { rt.Taskf(NodeLogName, "Chain ID: %s\n", devNode.Config().ChainID()) // Setup packages watcher - pathChangeCh := make(chan []string, 1) - go func() { - defer close(pathChangeCh) + // pathChangeCh := make(chan []string, 1) + // go func() { + // defer close(pathChangeCh) - err := runPkgsWatcher(ctx, cfg, devNode.ListPkgs(), pathChangeCh) - cancel(err) - }() + // err := runPkgsWatcher(ctx, cfg, devNode.ListPkgs(), pathChangeCh) + // cancel(err) + // }() // Create server mux := http.NewServeMux() @@ -177,6 +180,12 @@ func execDev(cfg *devCfg, args []string, io commands.IO) error { rt.Taskf(WebLogName, "Listener: http://%s\n", server.Addr) + watcher, err := watcher.NewPackageWatcher(loggerHotReload, eventsSrv) + if err != nil { + return fmt.Errorf("unable to setup packages watcher") + } + defer watcher.Stop() + // GnoDev should be ready, run event loop rt.Taskf("[Ready]", "for commands and help, press `h`") @@ -198,13 +207,11 @@ Gno Dev Helper: func runEventLoop(ctx context.Context, cfg *devCfg, rt *rawterm.RawTerm, - eventsSrv *events.Server, dnode *dev.Node, - pathsCh <-chan []string, + watch *watcher.PackageWatcher, ) error { nodeOut := rt.NamespacedWriter(NodeLogName) keyOut := rt.NamespacedWriter(KeyPressLogName) - hotOut := rt.NamespacedWriter(HotReloadLogName) keyPressCh := listenForKeyPress(keyOut, rt) for { @@ -213,28 +220,25 @@ func runEventLoop(ctx context.Context, select { case <-ctx.Done(): return context.Cause(ctx) - case paths, ok := <-pathsCh: + case err, ok := <-watch.PackagesUpdate: if !ok { return nil } - if cfg.verbose { - for _, path := range paths { - fmt.Fprintf(hotOut, "path %q has been modified\n", path) - } + return fmt.Errorf("watch errors: %w", err) + case pkgs, ok := <-watch.PackagesUpdate: + if !ok { + return nil } fmt.Fprintln(nodeOut, "Loading package updates...") - if err = dnode.UpdatePackages(paths...); err != nil { + if err = dnode.UpdatePackages(pkgs.PackagesPath()...); err != nil { return fmt.Errorf("unable to update packages: %w", err) } fmt.Fprintln(nodeOut, "Reloading...") err = dnode.Reload(ctx) - // send update files events - eventsSrv.SendJSONEvent(events.NewFilesUpdateEvent(paths)) - checkForError(rt, err) case key, ok := <-keyPressCh: diff --git a/contribs/gnodev/pkg/dev/events.go b/contribs/gnodev/pkg/dev/events.go new file mode 100644 index 00000000000..e5291da1365 --- /dev/null +++ b/contribs/gnodev/pkg/dev/events.go @@ -0,0 +1,26 @@ +package dev + +import "github.com/gnolang/gno/contribs/gnodev/pkg/events" + +const ( + EvtReload events.EventType = "NODE_RELOAD" + EvtReset events.EventType = "NODE_RESET" +) + +type EventReloadData struct{} + +func newReloadEvent() *events.Event { + return &events.Event{ + Type: EvtReload, + Data: &EventReloadData{}, + } +} + +type EventResetdData struct{} + +func newResetEvent() *events.Event { + return &events.Event{ + Type: EvtReload, + Data: &EventResetdData{}, + } +} diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 6624edc13c1..9a551294264 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/gnolang/gno/contribs/gnodev/pkg/events" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/integration" vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" @@ -33,8 +34,9 @@ const gnoDevChainID = "tendermint_test" // XXX: this is hardcoded and cannot be type Node struct { *node.Node - logger log.Logger - pkgs PkgsMap // path -> pkg + evtEmitter events.Emitter + logger log.Logger + pkgs PkgsMap // path -> pkg } var ( @@ -48,7 +50,7 @@ var ( } ) -func NewDevNode(ctx context.Context, logger log.Logger, pkgslist []string) (*Node, error) { +func NewDevNode(ctx context.Context, evtEmitter events.Emitter, logger log.Logger, pkgslist []string) (*Node, error) { mpkgs, err := newPkgsMap(pkgslist) if err != nil { return nil, fmt.Errorf("unable map pkgs list: %w", err) @@ -82,9 +84,10 @@ func NewDevNode(ctx context.Context, logger log.Logger, pkgslist []string) (*Nod } return &Node{ - Node: node, - pkgs: mpkgs, - logger: logger, + evtEmitter: evtEmitter, + Node: node, + pkgs: mpkgs, + logger: logger, }, nil } @@ -142,7 +145,12 @@ func (d *Node) Reset(ctx context.Context) error { Txs: txs, } - return d.reset(ctx, genesis) + if err := d.reset(ctx, genesis); err != nil { + return fmt.Errorf("unable to reset the node: %w", err) + } + + d.evtEmitter.Emit(newResetEvent()) + return nil } func (d *Node) ReloadAll(ctx context.Context) error { @@ -160,7 +168,6 @@ func (d *Node) ReloadAll(ctx context.Context) error { } func (d *Node) Reload(ctx context.Context) error { - // save current state state, err := d.saveState(ctx) if err != nil { @@ -201,6 +208,11 @@ func (d *Node) Reload(ctx context.Context) error { } } + if err != nil { + return fmt.Errorf("unable to reload the node: %w", err) + } + + d.evtEmitter.Emit(newReloadEvent()) return nil } diff --git a/contribs/gnodev/pkg/events/events.go b/contribs/gnodev/pkg/events/events.go index 7d098da7e40..c3201983b8b 100644 --- a/contribs/gnodev/pkg/events/events.go +++ b/contribs/gnodev/pkg/events/events.go @@ -2,24 +2,7 @@ package events type EventType string -const ( - FileUpdate EventType = "FILE_UPDATE" -) - type Event struct { Type EventType `json:"type"` Data interface{} `json:"data"` } - -type FileUpdateData struct { - Files []string `json:"files"` -} - -func NewFilesUpdateEvent(files []string) *Event { - return &Event{ - Type: FileUpdate, - Data: &FileUpdateData{ - Files: files, - }, - } -} diff --git a/contribs/gnodev/pkg/events/reload_test.go b/contribs/gnodev/pkg/events/reload_test.go deleted file mode 100644 index 87caf8df923..00000000000 --- a/contribs/gnodev/pkg/events/reload_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package events - -import "testing" - -func TestInjection(t *testing.T) { - t -} diff --git a/contribs/gnodev/pkg/events/server.go b/contribs/gnodev/pkg/events/server.go index 255cfbb22a3..7e65b457861 100644 --- a/contribs/gnodev/pkg/events/server.go +++ b/contribs/gnodev/pkg/events/server.go @@ -8,6 +8,10 @@ import ( "github.com/gorilla/websocket" ) +type Emitter interface { + Emit(evt *Event) +} + type Server struct { logger log.Logger upgrader websocket.Upgrader @@ -21,7 +25,7 @@ func NewServer(logger log.Logger) *Server { clients: make(map[*websocket.Conn]struct{}), upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { - return true // Adjust the origin policy as needed + return true // XXX: adjust this }, }, } @@ -51,7 +55,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -func (s *Server) SendJSONEvent(evt *Event) { +func (s *Server) Emit(evt *Event) { if len(s.clients) == 0 { return } @@ -61,7 +65,6 @@ func (s *Server) SendJSONEvent(evt *Event) { s.logger.Info("sending json", "clients", len(s.clients), "event", evt.Type, "data", evt.Data) for conn := range s.clients { - err := conn.WriteJSON(evt) if err != nil { s.logger.Error("write json", "error", err) diff --git a/contribs/gnodev/pkg/events/static/hotreload.js b/contribs/gnodev/pkg/events/static/hotreload.js index 0a0f9023a98..a7a91dc6246 100644 --- a/contribs/gnodev/pkg/events/static/hotreload.js +++ b/contribs/gnodev/pkg/events/static/hotreload.js @@ -1,4 +1,3 @@ - (function() { var ws = new WebSocket('ws://{{- .Remote -}}'); diff --git a/contribs/gnodev/pkg/watcher/events.go b/contribs/gnodev/pkg/watcher/events.go new file mode 100644 index 00000000000..26cc71f6f32 --- /dev/null +++ b/contribs/gnodev/pkg/watcher/events.go @@ -0,0 +1,37 @@ +package watcher + +import ( + events "github.com/gnolang/gno/contribs/gnodev/pkg/events" +) + +const ( + EvtPackagesUpdate events.EventType = "PACKAGES_UPDATE" +) + +type PackageUpdate struct { + Package string `json:"package"` + Files []string `json:"files"` +} + +type PackagesUpdate []PackageUpdate + +func (pkgsu PackagesUpdate) PackagesPath() []string { + pkgs := make([]string, len(pkgsu)) + for i, pkg := range pkgsu { + pkgs[i] = pkg.Package + } + return pkgs +} + +type EventPackagesUpdateData struct { + Pkgs []PackageUpdate `json:"packages"` +} + +func newPackagesUpdateEvent(pkgs []PackageUpdate) *events.Event { + return &events.Event{ + Type: EvtPackagesUpdate, + Data: &EventPackagesUpdateData{ + Pkgs: pkgs, + }, + } +} diff --git a/contribs/gnodev/pkg/watcher/watch.go b/contribs/gnodev/pkg/watcher/watch.go new file mode 100644 index 00000000000..a83313f75e6 --- /dev/null +++ b/contribs/gnodev/pkg/watcher/watch.go @@ -0,0 +1,165 @@ +package wather + +import ( + "context" + "fmt" + "log" + "path/filepath" + "sort" + "strings" + "time" + + events "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/tm2/pkg/log" + + "github.com/fsnotify/fsnotify" + "github.com/gnolang/gno/gnovm/pkg/gnomod" +) + +type PackageWatcher struct { + PackagesUpdate <-chan PackagesUpdate + Errors <-chan error + + logger log.Logger + watcher *fsnotify.Watcher + pkgs []string + ctx context.Context + stop context.CancelFunc + emitter events.Emitter +} + +type PackageUpdate struct { + Package gnomod.Pkg + Files []string +} + +func NewPackageWatcher(logger log.Logger, emitter events.Emitter) (*PackageWatcher, error) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, fmt.Errorf("unable to watch files: %w", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + p := &PackageWatcher{ + pkgs: []string{}, + logger: logger, + watcher: watcher, + ctx: ctx, + stop: cancel, + emitter: emitter, + } + + p.startWatching() + + return p, nil +} + +func (p *PackageWatcher) Stop() { + p.stop() +} + +func (p *PackageWatcher) AddPackages(pkgs ...gnomod.Pkg) error { + for _, pkg := range pkgs { + dir := pkg.Dir + + abs, err := filepath.Abs(dir) + if err != nil { + return fmt.Errorf("unable to get absolute path of %q: %w", err) + } + + // Find the correct insertion point using sort.Search + index := sort.Search(len(p.pkgs), func(i int) bool { + return len(p.pkgs[i]) <= len(dir) + }) + + // Check if the string already exists at the insertion point + if index < len(p.pkgs) && (p.pkgs)[index] == dir { + continue // Skip as it's a duplicate + } + + // Add the pakcage to the watcher + if err := p.watcher.Add(abs); err != nil { + return fmt.Errorf("unable to watch %q: %w", pkg.Dir, err) + } + } + + return nil +} + +func (p *PackageWatcher) startWatching() { + const timeout = time.Millisecond * 500 + + cerrs := make(chan error, 1) + defer close(cerrs) + + cwatch := make(chan []string) + defer close(cwatch) + + go func() { + var debounceTimer <-chan time.Time + var pathList = []string{} + var err error + + for err == nil { + select { + case <-p.ctx.Done(): + err = p.ctx.Err() + case watchErr := <-p.watcher.Errors: + err = fmt.Errorf("watch error: %w", watchErr) + case <-debounceTimer: + updates := p.generatePackagesUpdate(pathList) + + cwatch <- updates + + // Notify that we have some updates + p.emitter.Emit(newPackagesUpdateEvent(updates)) + + // Reset pathList and debounceTimer + pathList = []string{} + debounceTimer = nil + case evt := <-p.watcher.Events: + if evt.Op != fsnotify.Write { + continue + } + + pathList = append(pathList, evt.Name) + debounceTimer = time.After(timeout) + } + } + + cerrs <- err + }() + + p.PackagesUpdate = cwatch + p.Errors = cerrs +} + +func (p *PackageWatcher) generatePackagesUpdate(paths []string) PackagesUpdate { + pkgsUpdate := []PackageUpdate{} + + mpkgs := map[string]*PackageUpdate{} // pkg -> update + for _, path := range paths { + for _, pkg := range p.pkgs { + if !strings.HasPrefix(pkg, path) { + continue + } + + pkgu, ok := mpkgs[pkg] + if !ok { + pkgsUpdate = append(pkgsUpdate, PackageUpdate{ + Package: pkg, + Files: []string{}, + }) + pkgu = &pkgsUpdate[len(pkgsUpdate)-1] + } + + if len(pkg) == len(path) { + continue + } + + pkgu.Files = append(pkgu.Files, path) + } + } + + return pkgsUpdate +}