Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement ./contribs/gnodev command #1386

Merged
merged 38 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f376198
feat: move gnoweb into his own package
gfanton Nov 6, 2023
895bf8a
wip: add `gno dev` command
gfanton Nov 6, 2023
2ea43f7
wip: gno dev iteration 2
gfanton Nov 27, 2023
6814998
fix: fixup rebase
gfanton Nov 27, 2023
18aa12e
wip: iteration 3
gfanton Nov 27, 2023
6e43265
wip: iteration 4
gfanton Nov 27, 2023
1a2bdc9
wip: iteration 5
gfanton Nov 29, 2023
d17775c
wip: iteration 6
gfanton Nov 29, 2023
9829fe3
chore: lint
gfanton Nov 29, 2023
4c9a748
chore: organize and cleanup
gfanton Nov 29, 2023
6a5134f
chore: cleanup
gfanton Nov 29, 2023
0fd4413
chore: move gnodev to `/contribs` folder
gfanton Nov 29, 2023
92c2e60
chore: reset main go.mod
gfanton Nov 29, 2023
e30488e
chore: add install gnodev rules to contrib makefile
gfanton Nov 29, 2023
563b343
chore: cleanup & lint
gfanton Nov 30, 2023
abee336
fix: revert repl change
gfanton Nov 30, 2023
a16d3d5
fix: chore lint
gfanton Nov 30, 2023
a773f26
Merge remote-tracking branch 'origin/master' into feat/gno-dev
gfanton Dec 4, 2023
64118aa
fix: gnoweb test
gfanton Dec 4, 2023
8f73283
chore: update comment
gfanton Dec 4, 2023
ccb3143
fix: `gnodev` go.mod
gfanton Dec 4, 2023
c07b07b
feat: add gnodev readme
gfanton Dec 7, 2023
c186145
wip: fixes
gfanton Dec 7, 2023
5fafea3
chore: lint
gfanton Dec 9, 2023
517deed
chore: add 'make tidy' in contribs/
moul Dec 12, 2023
383d3af
chore: disable tx-archive to prevent diamond dependency issue with go…
moul Dec 12, 2023
abdd420
chore: more verbose ci check
moul Dec 12, 2023
bbd8b1e
chore: fixup
moul Dec 12, 2023
8069ab4
chore: fixup
moul Dec 13, 2023
19cb498
chore: fixup
moul Dec 13, 2023
a6de147
Merge branch 'master' into feat/gno-dev
gfanton Dec 13, 2023
62fc493
fix: go mod issue
gfanton Dec 13, 2023
4d33260
chore: clenaup main
gfanton Dec 13, 2023
ceadafa
chore: rename `WaitForNodeReadiness` to `GetNodeReadiness`
gfanton Dec 14, 2023
840c839
Merge branch 'master' into feat/gno-dev
gfanton Dec 14, 2023
6035359
chore: rename `WaitForNodeReadiness` to `GetNodeReadiness`
gfanton Dec 14, 2023
4792290
chore: cleanup
gfanton Dec 14, 2023
5173c05
feat: add missing example load folder
gfanton Dec 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions contribs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ help:
@cat Makefile | grep '^[a-z][^:]*:' | cut -d: -f1 | sort | sed 's/^/ /'

.PHONY: install
install: install.gnomd
install: install.gnomd install.gnodev

install.gnomd:; cd gnomd && go install .
install.gnomd:; cd gnomd && go install .
install.gnodev:; $(MAKE) -C ./gnodev install

.PHONY: clean
clean:
Expand Down
9 changes: 9 additions & 0 deletions contribs/gnodev/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
GNOROOT_DIR ?= $(abspath $(lastword $(MAKEFILE_LIST))/../../../)

GOBUILD_FLAGS := -ldflags "-X github.com/gnolang/gno/gnovm/pkg/gnoenv._GNOROOT=$(GNOROOT_DIR)"

install:
go install $(GOBUILD_FLAGS) ./cmd/gnodev

build:
go build $(GOBUILD_FLAGS) -o build/gnodev ./cmd/gno
356 changes: 356 additions & 0 deletions contribs/gnodev/cmd/gnodev/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
package main

import (
"context"
"flag"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"time"

"github.com/fsnotify/fsnotify"
gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev"
"github.com/gnolang/gno/gno.land/pkg/gnoweb"
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
"github.com/gnolang/gno/gnovm/pkg/gnomod"
"github.com/gnolang/gno/tm2/pkg/commands"
tmlog "github.com/gnolang/gno/tm2/pkg/log"
osm "github.com/gnolang/gno/tm2/pkg/os"
)

type devCfg struct {
bindAddr string
minimal bool
verbose bool
noWatch bool
}

var defaultDevOptions = &devCfg{
bindAddr: "127.0.0.1:8888",
}

func main() {
cfg := &devCfg{}

stdio := commands.NewDefaultIO()
cmd := commands.NewCommand(
commands.Metadata{
Name: "gnodev",
ShortUsage: "gnodev [flags] <path>",
ShortHelp: "GnoDev run a node for dev purpose, it will load the given package path",
gfanton marked this conversation as resolved.
Show resolved Hide resolved
},
cfg,
func(_ context.Context, args []string) error {
return execDev(cfg, args, stdio)
})

if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "%+v\n", err)
os.Exit(1)
}
}

func (c *devCfg) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(
&c.bindAddr,
"web-bind",
zivkovicmilos marked this conversation as resolved.
Show resolved Hide resolved
defaultDevOptions.bindAddr,
"verbose output when deving",
gfanton marked this conversation as resolved.
Show resolved Hide resolved
)

fs.BoolVar(
&c.minimal,
"minimal",
defaultDevOptions.verbose,
"don't load example folder packages along default transaction",
gfanton marked this conversation as resolved.
Show resolved Hide resolved
)

fs.BoolVar(
&c.verbose,
"verbose",
defaultDevOptions.verbose,
"verbose output when deving",
)

fs.BoolVar(
&c.noWatch,
"no-watch",
defaultDevOptions.noWatch,
"watch for files change",
)

}

func execDev(cfg *devCfg, args []string, io commands.IO) error {
ctx, cancel := context.WithCancelCause(context.Background())
defer cancel(nil)

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

pkgpaths, err := parseArgsPackages(io, args)
gfanton marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("unable to parse package paths: %w", err)
}

// noopLogger := log.NewTMLogger(log.NewSyncWriter(io.Out))
gfanton marked this conversation as resolved.
Show resolved Hide resolved
noopLogger := tmlog.NewNopLogger()

// RAWTerm setup
rt := gnodev.NewRawTerm()
{
gfanton marked this conversation as resolved.
Show resolved Hide resolved
restore, err := rt.Init()
if err != nil {
return fmt.Errorf("unable to init raw term for dev: %s", err)
}
defer restore()

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

// Setup trap signal
osm.TrapSignal(func() {
restore()
cancel(nil)
})
}

nodeOut := rt.NamespacedWriter("Node")
webOut := rt.NamespacedWriter("GnoWeb")
keyOut := rt.NamespacedWriter("KeyPress")

var dnode *gnodev.Node
{
var err error
// XXX: redirect node to output file
gfanton marked this conversation as resolved.
Show resolved Hide resolved
dnode, err = setupDevNode(ctx, noopLogger, cfg, pkgpaths, gnoroot)
if err != nil {
return err // already formated in setupDevNode
}
defer dnode.Close()
}

fmt.Fprintf(nodeOut, "Listener: %s\n", dnode.GetRemoteAddress())

// setup files watcher
w, err := setupPkgsWatcher(cfg, dnode.ListPkgs())
if err != nil {
return fmt.Errorf("unable to watch for files change: %w", err)
}

ccpath := make(chan []string, 1)
go func() {
defer close(ccpath)

const debounceTimeout = time.Millisecond * 500

if err := handleDebounce(ctx, w, ccpath, debounceTimeout); err != nil {
gfanton marked this conversation as resolved.
Show resolved Hide resolved
cancel(err)
}
}()

// Gnoweb setup
gfanton marked this conversation as resolved.
Show resolved Hide resolved
server := setupGnowebServer(cfg, dnode, rt)

l, err := net.Listen("tcp", cfg.bindAddr)
if err != nil {
return fmt.Errorf("unable to listen to %q: %w", cfg.bindAddr, err)
}

go func() {
var err error
if srvErr := server.Serve(l); srvErr != nil {
gfanton marked this conversation as resolved.
Show resolved Hide resolved
err = fmt.Errorf("HTTP server stopped with error: %w", srvErr)
}
cancel(err)
}()
defer server.Close()

fmt.Fprintf(webOut, "Listener: %s\n", l.Addr())
gfanton marked this conversation as resolved.
Show resolved Hide resolved

// Print basic infos
fmt.Fprintf(nodeOut, "Default Address: %s\n", gnodev.DefaultCreator.String())
fmt.Fprintf(nodeOut, "Chain ID: %s\n", dnode.Config().ChainID())

rt.Taskf("[Ready]", "for commands and help, press `h`")

cckey := listenForKeyPress(keyOut, rt)
for {
gfanton marked this conversation as resolved.
Show resolved Hide resolved
var err error

select {
case <-ctx.Done():
return context.Cause(ctx)
gfanton marked this conversation as resolved.
Show resolved Hide resolved
case paths := <-ccpath:
gfanton marked this conversation as resolved.
Show resolved Hide resolved
for _, path := range paths {
rt.Taskf("HotReload", "path %q has been modified", path)
}

fmt.Fprintln(nodeOut, "Loading package updates...")
if err = dnode.UpdatePackages(paths...); err != nil {
checkForError(rt, err)
continue
}

fmt.Fprintln(nodeOut, "Reloading...")
err = dnode.Reload(ctx)
checkForError(rt, err)
case key, ok := <-cckey:
gfanton marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
cancel(nil)
continue
}

if cfg.verbose {
fmt.Fprintf(keyOut, "<%s>\n", key.String())
}

switch key {
case 'h', 'H':
gfanton marked this conversation as resolved.
Show resolved Hide resolved
printHelper(rt)
case 'r', 'R':
fmt.Fprintln(nodeOut, "Reloading all packages...")
err = dnode.ReloadAll(ctx)
checkForError(nodeOut, err)
gfanton marked this conversation as resolved.
Show resolved Hide resolved
case gnodev.KeyCtrlR:
fmt.Fprintln(nodeOut, "Reseting state...")
err = dnode.Reset(ctx)
checkForError(nodeOut, err)
case gnodev.KeyCtrlC:
gfanton marked this conversation as resolved.
Show resolved Hide resolved
cancel(nil)
default:
}
}
}
}

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

func handleDebounce(ctx context.Context, watcher *fsnotify.Watcher, changedPathsCh chan<- []string, timeout time.Duration) error {
var debounceTimer <-chan time.Time
pathList := []string{}

for {
select {
case <-ctx.Done():
return ctx.Err()
case watchErr := <-watcher.Errors:
return fmt.Errorf("watch error: %w", watchErr)
case <-debounceTimer:
changedPathsCh <- pathList
// Reset pathList and debounceTimer
pathList = []string{}
debounceTimer = nil
case evt := <-watcher.Events:
if evt.Op == fsnotify.Write {
gfanton marked this conversation as resolved.
Show resolved Hide resolved
pathList = append(pathList, evt.Name)
debounceTimer = time.After(timeout)
}
}
}
}

func setupPkgsWatcher(cfg *devCfg, pkgs []gnomod.Pkg) (*fsnotify.Watcher, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, fmt.Errorf("unable to watch files: %w", err)
}

if cfg.noWatch {
// noop watcher
return watcher, nil
}

for _, pkg := range pkgs {
if err := watcher.Add(pkg.Dir); err != nil {
return nil, fmt.Errorf("unable to watch %q: %w", pkg.Dir, err)
}
}

return watcher, nil
}

// setupDevNode initializes and returns a new DevNode.
func setupDevNode(ctx context.Context, logger tmlog.Logger, cfg *devCfg, pkgspath []string, gnoroot string) (*gnodev.Node, error) {
if !cfg.minimal {
examplesDir := filepath.Join(gnoroot, "examples")
pkgspath = append(pkgspath, examplesDir)
}

return gnodev.NewDevNode(ctx, logger, pkgspath)
}

// setupGnowebServer initializes and starts the Gnoweb server.
func setupGnowebServer(cfg *devCfg, dnode *gnodev.Node, rt *gnodev.RawTerm) *http.Server {
gfanton marked this conversation as resolved.
Show resolved Hide resolved
var server http.Server

webConfig := gnoweb.NewDefaultConfig()
webConfig.RemoteAddr = dnode.GetRemoteAddress()

loggerweb := tmlog.NewTMLogger(rt.NamespacedWriter("GnoWeb"))
loggerweb.SetLevel(tmlog.LevelDebug)

app := gnoweb.MakeApp(loggerweb, webConfig)

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

return &server
}

func parseArgsPackages(io commands.IO, args []string) (paths []string, err error) {
gfanton marked this conversation as resolved.
Show resolved Hide resolved
paths = make([]string, len(args))
for i, arg := range args {
abspath, err := filepath.Abs(arg)
if err != nil {
return nil, fmt.Errorf("invalid path %q: %w", arg, err)
}

ppath, err := gnomod.FindRootDir(abspath)
if err != nil {
return nil, fmt.Errorf("unable to find root dir of %q: %w", abspath, err)
}

paths[i] = ppath
}

return paths, nil
}

func listenForKeyPress(w io.Writer, rt *gnodev.RawTerm) <-chan gnodev.KeyPress {
cc := make(chan gnodev.KeyPress, 1)
go func() {
gfanton marked this conversation as resolved.
Show resolved Hide resolved
defer close(cc)
for {
key, err := rt.ReadKeyPress()
if err != nil {
fmt.Fprintf(w, "unable to read keypress: %s\n", err.Error())
return
}

cc <- key
}
}()

return cc
}

func checkForError(w io.Writer, err error) {
if err != nil {
fmt.Fprintf(w, "", "[ERROR] - %s\n", err.Error())
} else {
gfanton marked this conversation as resolved.
Show resolved Hide resolved
fmt.Fprintln(w, "", "[DONE]")
}
}
Loading
Loading