Skip to content

Commit

Permalink
feat(gnodev): Add automatic page reload on save (gnolang#1457)
Browse files Browse the repository at this point in the history
  • Loading branch information
gfanton authored and leohhhn committed Feb 29, 2024
1 parent 1b03a70 commit b9ad629
Show file tree
Hide file tree
Showing 8 changed files with 684 additions and 132 deletions.
2 changes: 1 addition & 1 deletion contribs/gnodev/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ replace github.com/gnolang/gno => ../..
require (
github.com/fsnotify/fsnotify v1.7.0
github.com/gnolang/gno v0.0.0-00010101000000-000000000000
github.com/gorilla/websocket v1.5.1
go.uber.org/zap v1.26.0
golang.org/x/term v0.16.0
)
Expand Down Expand Up @@ -35,7 +36,6 @@ require (
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/gotuna/gotuna v0.6.0 // indirect
github.com/jaekwon/testify v1.6.1 // indirect
github.com/jmhodges/levigo v1.0.0 // indirect
Expand Down
134 changes: 75 additions & 59 deletions contribs/gnodev/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"flag"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
Expand All @@ -14,27 +13,31 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/gnolang/gno/contribs/gnodev/pkg/dev"
gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev"
"github.com/gnolang/gno/contribs/gnodev/pkg/emitter"
"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/gno.land/pkg/log"
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
"github.com/gnolang/gno/gnovm/pkg/gnomod"
"github.com/gnolang/gno/tm2/pkg/commands"
osm "github.com/gnolang/gno/tm2/pkg/os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

const (
NodeLogName = "Node"
WebLogName = "GnoWeb"
KeyPressLogName = "KeyPress"
HotReloadLogName = "HotReload"
NodeLogName = "Node"
WebLogName = "GnoWeb"
KeyPressLogName = "KeyPress"
EventServerLogName = "Events"
)

type devCfg struct {
webListenerAddr string
minimal bool
verbose bool
hotreload bool
noWatch bool
}

Expand Down Expand Up @@ -97,7 +100,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) error {
ctx, cancel := context.WithCancelCause(context.Background())
defer cancel(nil)

// guess root dir
// Guess root dir
gnoroot := gnoenv.RootDir()

// Check and Parse packages
Expand All @@ -124,9 +127,13 @@ func execDev(cfg *devCfg, args []string, io commands.IO) error {
cancel(nil)
})

zapLoggerEvents := NewZapLogger(rt.NamespacedWriter(EventServerLogName), zapcore.DebugLevel)
loggerEvents := log.ZapLoggerToSlog(zapLoggerEvents)
emitterServer := emitter.NewServer(loggerEvents)

// Setup Dev Node
// XXX: find a good way to export or display node logs
devNode, err := setupDevNode(ctx, rt, pkgpaths)
devNode, err := setupDevNode(ctx, emitterServer, rt, pkgpaths)
if err != nil {
return err
}
Expand All @@ -136,51 +143,66 @@ func execDev(cfg *devCfg, args []string, io commands.IO) error {
rt.Taskf(NodeLogName, "Default Address: %s\n", gnodev.DefaultCreator.String())
rt.Taskf(NodeLogName, "Chain ID: %s\n", devNode.Config().ChainID())

// Setup packages watcher
pathChangeCh := make(chan []string, 1)
go func() {
defer close(pathChangeCh)
// Create server
mux := http.NewServeMux()
server := http.Server{
Handler: mux,
Addr: cfg.webListenerAddr,
}
defer server.Close()

// Setup gnoweb
webhandler := setupGnoWebServer(cfg, devNode, rt)

// Setup HotReload if needed
if !cfg.noWatch {
evtstarget := fmt.Sprintf("%s/_events", server.Addr)
mux.Handle("/_events", emitterServer)
mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler))
} else {
mux.Handle("/", webhandler)
}

cancel(runPkgsWatcher(ctx, cfg, devNode.ListPkgs(), pathChangeCh))
go func() {
err := server.ListenAndServe()
cancel(err)
}()

// Setup GnoWeb listener
l, err := net.Listen("tcp", cfg.webListenerAddr)
rt.Taskf(WebLogName, "Listener: http://%s\n", server.Addr)

watcher, err := watcher.NewPackageWatcher(loggerEvents, emitterServer)
if err != nil {
return fmt.Errorf("unable to listen to %q: %w", cfg.webListenerAddr, err)
return fmt.Errorf("unable to setup packages watcher: %w", err)
}
defer l.Close()
defer watcher.Stop()

// Run GnoWeb server
go func() {
cancel(serveGnoWebServer(l, devNode, rt))
}()

rt.Taskf(WebLogName, "Listener: http://%s\n", l.Addr())
// Add node pkgs to watcher
watcher.AddPackages(devNode.ListPkgs()...)

// GnoDev should be ready, run event loop
rt.Taskf("[Ready]", "for commands and help, press `h`")

// Run the main event loop
return runEventLoop(ctx, cfg, rt, devNode, pathChangeCh)
return runEventLoop(ctx, cfg, rt, devNode, watcher)
}

// XXX: Automatize this the same way command does
func printHelper(rt *rawterm.RawTerm) {
rt.Taskf("Helper", `
Gno Dev Helper:
h, H Help - display this message
r, R Reload - Reload all packages to take change into account.
H Help - display this message
R Reload - Reload all packages to take change into account.
Ctrl+R Reset - Reset application state.
Ctrl+C Exit - Exit the application
`)
}

func runEventLoop(ctx context.Context,
func runEventLoop(
ctx context.Context,
cfg *devCfg,
rt *rawterm.RawTerm,
dnode *dev.Node,
pathsCh <-chan []string,
watch *watcher.PackageWatcher,
) error {
nodeOut := rt.NamespacedWriter(NodeLogName)
keyOut := rt.NamespacedWriter(KeyPressLogName)
Expand All @@ -192,26 +214,21 @@ func runEventLoop(ctx context.Context,
select {
case <-ctx.Done():
return context.Cause(ctx)
case paths, ok := <-pathsCh:
case pkgs, ok := <-watch.PackagesUpdate:
if !ok {
return nil
}

if cfg.verbose {
for _, path := range paths {
rt.Taskf(HotReloadLogName, "path %q has been modified", path)
}
}

fmt.Fprintln(nodeOut, "Loading package updates...")
if err = dnode.UpdatePackages(paths...); err != nil {
checkForError(rt, err)
continue
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)

checkForError(rt, err)

case key, ok := <-keyPressCh:
if !ok {
return nil
Expand Down Expand Up @@ -248,7 +265,7 @@ func runPkgsWatcher(ctx context.Context, cfg *devCfg, pkgs []gnomod.Pkg, changed
}

if cfg.noWatch {
// noop watcher, wait until context has been cancel
// Noop watcher, wait until context has been cancel
<-ctx.Done()
return ctx.Err()
}
Expand Down Expand Up @@ -294,42 +311,28 @@ func setupRawTerm(io commands.IO) (rt *rawterm.RawTerm, restore func() error, er
return nil, nil, err
}

// correctly format output for terminal
// Correctly format output for terminal
io.SetOut(commands.WriteNopCloser(rt))

return rt, restore, nil
}

// setupDevNode initializes and returns a new DevNode.
func setupDevNode(ctx context.Context, rt *rawterm.RawTerm, pkgspath []string) (*gnodev.Node, error) {
func setupDevNode(ctx context.Context, emitter emitter.Emitter, rt *rawterm.RawTerm, pkgspath []string) (*gnodev.Node, error) {
nodeOut := rt.NamespacedWriter("Node")

zapLogger := log.NewZapConsoleLogger(nodeOut, zapcore.ErrorLevel)

return gnodev.NewDevNode(ctx, log.ZapLoggerToSlog(zapLogger), pkgspath)
zapLogger := NewZapLogger(nodeOut, zapcore.ErrorLevel)
return gnodev.NewDevNode(ctx, log.ZapLoggerToSlog(zapLogger), emitter, pkgspath)
}

// setupGnowebServer initializes and starts the Gnoweb server.
func serveGnoWebServer(l net.Listener, dnode *gnodev.Node, rt *rawterm.RawTerm) error {
var server http.Server

func setupGnoWebServer(cfg *devCfg, dnode *gnodev.Node, rt *rawterm.RawTerm) http.Handler {
webConfig := gnoweb.NewDefaultConfig()
webConfig.RemoteAddr = dnode.GetRemoteAddress()
webConfig.HelpChainID = dnode.Config().ChainID()
webConfig.HelpRemote = dnode.GetRemoteAddress()

zapLogger := log.NewZapConsoleLogger(rt.NamespacedWriter("GnoWeb"), zapcore.DebugLevel)

zapLogger := NewZapLogger(rt.NamespacedWriter("GnoWeb"), zapcore.DebugLevel)
app := gnoweb.MakeApp(log.ZapLoggerToSlog(zapLogger), webConfig)

server.ReadHeaderTimeout = 60 * time.Second
server.Handler = app.Router

if err := server.Serve(l); err != nil {
return fmt.Errorf("unable to serve GnoWeb: %w", err)
}

return nil
return app.Router
}

func parseArgsPackages(args []string) (paths []string, err error) {
Expand Down Expand Up @@ -375,3 +378,16 @@ func checkForError(w io.Writer, err error) {

fmt.Fprintln(w, "[DONE]")
}

// NewZapLogger creates a zap logger with a console encoder for development use.
func NewZapLogger(w io.Writer, level zapcore.Level) *zap.Logger {
// Build encoder config
consoleConfig := zap.NewDevelopmentEncoderConfig()
consoleConfig.TimeKey = ""
consoleConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
consoleConfig.EncodeName = zapcore.FullNameEncoder

// Build encoder
enc := zapcore.NewConsoleEncoder(consoleConfig)
return log.NewZapLogger(enc, w, level)
}
Loading

0 comments on commit b9ad629

Please sign in to comment.