From 16ee7d960b0cc431824b224a78dd09c252e480da Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sat, 6 Jul 2024 16:36:52 +0200 Subject: [PATCH 01/24] wip: lazy loading & custom resolver support Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/main.go | 108 +++++++-- contribs/gnodev/cmd/gnodev/setup_node.go | 6 +- contribs/gnodev/pkg/dev/node.go | 216 ++++++++++-------- contribs/gnodev/pkg/dev/node_pkgs.go | 1 + contribs/gnodev/pkg/dev/node_state.go | 8 +- contribs/gnodev/pkg/dev/node_test.go | 6 +- contribs/gnodev/pkg/dev/packages.go | 170 -------------- contribs/gnodev/pkg/dev/packages_test.go | 2 +- contribs/gnodev/pkg/dev/query_path.go | 65 ++++++ contribs/gnodev/pkg/packages/loader.go | 95 ++++++++ contribs/gnodev/pkg/packages/meta.go | 1 + contribs/gnodev/pkg/packages/resolver.go | 44 ++++ contribs/gnodev/pkg/packages/resolver_fs.go | 43 ++++ .../gnodev/pkg/packages/resolver_local.go | 54 +++++ contribs/gnodev/pkg/packages/utils.go | 67 ++++++ contribs/gnodev/pkg/watcher/watch.go | 66 +++--- examples/gno.land/r/demo/boards/render.gno | 4 +- gnovm/stdlibs/regexp/syntax/parse_test.gno | 2 +- 18 files changed, 629 insertions(+), 329 deletions(-) create mode 100644 contribs/gnodev/pkg/dev/node_pkgs.go delete mode 100644 contribs/gnodev/pkg/dev/packages.go create mode 100644 contribs/gnodev/pkg/dev/query_path.go create mode 100644 contribs/gnodev/pkg/packages/loader.go create mode 100644 contribs/gnodev/pkg/packages/meta.go create mode 100644 contribs/gnodev/pkg/packages/resolver.go create mode 100644 contribs/gnodev/pkg/packages/resolver_fs.go create mode 100644 contribs/gnodev/pkg/packages/resolver_local.go create mode 100644 contribs/gnodev/pkg/packages/utils.go diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 2c694b608bb..964d913464f 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -7,13 +7,16 @@ import ( "fmt" "log/slog" "net/http" + "net/url" "os" "path/filepath" + "strings" "time" "github.com/gnolang/gno/contribs/gnodev/pkg/address" gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" "github.com/gnolang/gno/gno.land/pkg/integration" @@ -40,6 +43,8 @@ var ( ) type devCfg struct { + chdir string + // Listeners nodeRPCListenerAddr string nodeP2PListenerAddr string @@ -106,6 +111,13 @@ func main() { } func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.chdir, + "C", + defaultDevOptions.chdir, + "change directory before running gnodev", + ) + fs.StringVar( &c.home, "home", @@ -244,6 +256,38 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { ctx, cancel := context.WithCancelCause(context.Background()) defer cancel(nil) + if cfg.chdir != "" { + if err := os.Chdir(cfg.chdir); err != nil { + return fmt.Errorf("unable to change directory: %w", err) + } + } + + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("unable to guess current dir: %w", err) + } + + var resolvers []packages.Resolver + gnoroot := gnoenv.RootDir() + localresolver := packages.GuessLocalResolverGnoMod(dir) + if localresolver == nil { + localresolver = packages.GuessLocalResolverFromRoots(dir, []string{gnoroot}) + if localresolver == nil { + return fmt.Errorf("unable to guess current package: %w", err) + } + } + + path := localresolver.Path + resolvers = append(resolvers, localresolver) + + exampleRoot := filepath.Join(gnoroot, "examples") + fsResolver := packages.NewFSResolver(exampleRoot) + resolvers = append(resolvers, fsResolver) + loader := &packages.Loader{ + Paths: []string{path}, + Resolver: packages.ChainedResolver(resolvers), + } + if err := cfg.validateConfigFlags(); err != nil { return fmt.Errorf("validate error: %w", err) } @@ -272,10 +316,10 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { } // Check and Parse packages - pkgpaths, err := resolvePackagesPathFromArgs(cfg, book, args) - if err != nil { - return fmt.Errorf("unable to parse package paths: %w", err) - } + // pkgpaths, err := resolvePackagesPathFromArgs(cfg, book, args) + // if err != nil { + // return fmt.Errorf("unable to parse package paths: %w", err) + // } // generate balances balances, err := generateBalances(book, cfg) @@ -287,7 +331,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { // Setup Dev Node // XXX: find a good way to export or display node logs nodeLogger := logger.WithGroup(NodeLogName) - nodeCfg := setupDevNodeConfig(cfg, logger, emitterServer, balances, pkgpaths) + nodeCfg := setupDevNodeConfig(cfg, logger, emitterServer, balances, loader) devNode, err := setupDevNode(ctx, cfg, nodeCfg) if err != nil { return err @@ -325,13 +369,29 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { }) } + // XXX: TODO: + // - create a map of known path that are allowed + // - move this + u, err := url.Parse("http://" + path) + if err != nil { + return fmt.Errorf("malformed path %q: %w", path, err) + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, "/static") && !strings.HasPrefix(r.URL.Path, u.Path) { + http.Redirect(w, r, u.Path, http.StatusFound) + return + } + + webhandler.ServeHTTP(w, r) + }) + // Setup HotReload if needed if !cfg.noWatch { evtstarget := fmt.Sprintf("%s/_events", server.Addr) mux.Handle("/_events", emitterServer) - mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler)) + mux.Handle("/", emitter.NewMiddleware(evtstarget, handler)) } else { - mux.Handle("/", webhandler) + mux.Handle("/", handler) } go func() { @@ -350,7 +410,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { defer watcher.Stop() // Add node pkgs to watcher - watcher.AddPackages(devNode.ListPkgs()...) + watcher.UpdatePackagesWatch(devNode.ListPkgs()...) if !cfg.serverMode { logger.WithGroup("--- READY").Info("for commands and help, press `h`") @@ -360,7 +420,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { return runEventLoop(ctx, logger, book, rt, devNode, watcher) } -var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: +var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev P Previous TX - Go to the previous tx @@ -403,15 +463,14 @@ func runEventLoop( select { case <-ctx.Done(): return context.Cause(ctx) - case pkgs, ok := <-watch.PackagesUpdate: + case _, ok := <-watch.PackagesUpdate: if !ok { return nil } - // fmt.Fprintln(nodeOut, "Loading package updates...") - if err = dnode.UpdatePackages(pkgs.PackagesPath()...); err != nil { - return fmt.Errorf("unable to update packages: %w", err) - } + // if err = dnode.UpdatePackages(pkgs.PackagesPath()...); err != nil { + // return fmt.Errorf("unable to update packages: %w", err) + // } logger.WithGroup(NodeLogName).Info("reloading...") if err = dnode.Reload(ctx); err != nil { @@ -419,6 +478,9 @@ func runEventLoop( Error("unable to reload node", "err", err) } + listpkgs := dnode.ListPkgs() + watch.UpdatePackagesWatch(listpkgs...) + case key, ok := <-keyPressCh: if !ok { return nil @@ -516,8 +578,8 @@ func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm. return cc } -func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ([]gnodev.PackagePath, error) { - paths := make([]gnodev.PackagePath, 0, len(args)) +func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ([]gnodev.PackageModifier, error) { + modifiers := make([]gnodev.PackageModifier, 0, len(args)) if cfg.deployKey == "" { return nil, fmt.Errorf("default deploy key cannot be empty") @@ -528,8 +590,12 @@ func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ( return nil, fmt.Errorf("unable to get deploy key %q", cfg.deployKey) } + if len(args) == 0 { + args = append(args, ".") // add current dir if none are provided + } + for _, arg := range args { - path, err := gnodev.ResolvePackagePathQuery(bk, arg) + path, err := gnodev.ResolvePackageModifierQuery(bk, arg) if err != nil { return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) } @@ -539,17 +605,17 @@ func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ( path.Creator = defaultKey } - paths = append(paths, path) + modifiers = append(modifiers, path) } // Add examples folder if minimal is set to false - if !cfg.minimal { - paths = append(paths, gnodev.PackagePath{ + if cfg.minimal { + modifiers = append(modifiers, gnodev.PackageModifier{ Path: filepath.Join(cfg.root, "examples"), Creator: defaultKey, Deposit: nil, }) } - return paths, nil + return modifiers, nil } diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index a2b1970d0ef..b993af8b04e 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -9,6 +9,7 @@ import ( gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/types" ) @@ -55,14 +56,15 @@ func setupDevNodeConfig( logger *slog.Logger, emitter emitter.Emitter, balances gnoland.Balances, - pkgspath []gnodev.PackagePath, + loader *packages.Loader, ) *gnodev.NodeConfig { config := gnodev.DefaultNodeConfig(cfg.root) + config.Loader = loader config.Logger = logger config.Emitter = emitter config.BalancesList = balances.List() - config.PackagesPathList = pkgspath + // config.PackagesModifier = pkgspath config.TMConfig.RPC.ListenAddress = resolveUnixOrTCPAddr(cfg.nodeRPCListenerAddr) config.NoReplay = cfg.noReplay config.MaxGasPerBlock = cfg.maxGas diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index e0ed64aad36..4697132e6b0 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -5,7 +5,6 @@ import ( "fmt" "log/slog" "os" - "path/filepath" "strings" "sync" "time" @@ -13,10 +12,11 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/integration" - "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/amino" tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" @@ -33,9 +33,11 @@ import ( type NodeConfig struct { Logger *slog.Logger - DefaultDeployer crypto.Address + Loader *packages.Loader + DefaultCreator crypto.Address + DefaultDeposit std.Coins BalancesList []gnoland.Balance - PackagesPathList []PackagePath + PackagesModifier []PackageModifier Emitter emitter.Emitter InitialTxs []gnoland.TxWithMetadata TMConfig *tmcfg.Config @@ -62,7 +64,8 @@ func DefaultNodeConfig(rootdir string) *NodeConfig { return &NodeConfig{ Logger: log.NewNoopLogger(), Emitter: &emitter.NoopServer{}, - DefaultDeployer: defaultDeployer, + DefaultCreator: defaultDeployer, + DefaultDeposit: nil, BalancesList: balances, ChainID: tmc.ChainID(), TMConfig: tmc, @@ -80,7 +83,8 @@ type Node struct { emitter emitter.Emitter client client.Client logger *slog.Logger - pkgs PackagesMap // path -> pkg + loader packages.Loader + pkgs []packages.Package // keep track of number of loaded package to be able to skip them on restore loadedPackages int @@ -96,38 +100,23 @@ type Node struct { var DefaultFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { - mpkgs, err := NewPackagesMap(cfg.PackagesPathList) - if err != nil { - return nil, fmt.Errorf("unable map pkgs list: %w", err) - } - startTime := time.Now() - pkgsTxs, err := mpkgs.Load(DefaultFee, startTime) - if err != nil { - return nil, fmt.Errorf("unable to load genesis packages: %w", err) - } - cfg.Logger.Info("pkgs loaded", "path", cfg.PackagesPathList) devnode := &Node{ + loader: *cfg.Loader, config: cfg, client: client.NewLocal(), emitter: cfg.Emitter, - pkgs: mpkgs, logger: cfg.Logger, - loadedPackages: len(pkgsTxs), startTime: startTime, state: cfg.InitialTxs, initialState: cfg.InitialTxs, currentStateIndex: len(cfg.InitialTxs), + // pkgsModifier: pkgsModifier, } - // generate genesis state - genesis := gnoland.GnoGenesisState{ - Balances: cfg.BalancesList, - Txs: append(pkgsTxs, cfg.InitialTxs...), - } - - if err := devnode.rebuildNode(ctx, genesis); err != nil { + // XXX: MOVE THIS + if err := devnode.Reset(ctx); err != nil { return nil, fmt.Errorf("unable to initialize the node: %w", err) } @@ -141,11 +130,11 @@ func (n *Node) Close() error { return n.Node.Stop() } -func (n *Node) ListPkgs() []gnomod.Pkg { +func (n *Node) ListPkgs() []packages.Package { n.muNode.RLock() defer n.muNode.RUnlock() - return n.pkgs.toList() + return n.pkgs } func (n *Node) Client() client.Client { @@ -212,57 +201,57 @@ func (n *Node) getLatestBlockNumber() uint64 { // UpdatePackages updates the currently known packages. It will be taken into // consideration in the next reload of the node. -func (n *Node) UpdatePackages(paths ...string) error { - n.muNode.Lock() - defer n.muNode.Unlock() - - return n.updatePackages(paths...) -} - -func (n *Node) updatePackages(paths ...string) error { - var pkgsUpdated int - for _, path := range paths { - abspath, err := filepath.Abs(path) - if err != nil { - return fmt.Errorf("unable to resolve abs path of %q: %w", path, err) - } - - // Check if we already know the path (or its parent) and set - // associated deployer and deposit - deployer := n.config.DefaultDeployer - var deposit std.Coins - for _, ppath := range n.config.PackagesPathList { - if !strings.HasPrefix(abspath, ppath.Path) { - continue - } - - deployer = ppath.Creator - deposit = ppath.Deposit - } - - // List all packages from target path - pkgslist, err := gnomod.ListPkgs(abspath) - if err != nil { - return fmt.Errorf("failed to list gno packages for %q: %w", path, err) - } - - // Update or add package in the current known list. - for _, pkg := range pkgslist { - n.pkgs[pkg.Dir] = Package{ - Pkg: pkg, - Creator: deployer, - Deposit: deposit, - } - - n.logger.Debug("pkgs update", "name", pkg.Name, "path", pkg.Dir) - } - - pkgsUpdated += len(pkgslist) - } - - n.logger.Info(fmt.Sprintf("updated %d packages", pkgsUpdated)) - return nil -} +// func (n *Node) UpdatePackages(paths ...string) error { +// n.muNode.Lock() +// defer n.muNode.Unlock() + +// return n.updatePackages(paths...) +// } + +// func (n *Node) updatePackages(paths ...string) error { +// var pkgsUpdated int +// for _, path := range paths { +// abspath, err := filepath.Abs(path) +// if err != nil { +// return fmt.Errorf("unable to resolve abs path of %q: %w", path, err) +// } + +// // Check if we already know the path (or its parent) and set +// // associated deployer and deposit +// deployer := n.config.DefaultCreator +// var deposit std.Coins +// for _, ppath := range n.config.PackagesPathList { +// if !strings.HasPrefix(abspath, ppath.Path) { +// continue +// } + +// deployer = ppath.Creator +// deposit = ppath.Deposit +// } + +// // List all packages from target path +// pkgslist, err := gnomod.ListPkgs(abspath) +// if err != nil { +// return fmt.Errorf("failed to list gno packages for %q: %w", path, err) +// } + +// // Update or add package in the current known list. +// for _, pkg := range pkgslist { +// n.pkgs[pkg.Dir] = ModPackage{ +// Pkg: pkg, +// Creator: deployer, +// Deposit: deposit, +// } + +// n.logger.Debug("pkgs update", "name", pkg.Name, "path", pkg.Dir) +// } + +// pkgsUpdated += len(pkgslist) +// } + +// n.logger.Info(fmt.Sprintf("updated %d packages", pkgsUpdated)) +// return nil +// } // Reset stops the node, if running, and reloads it with a new genesis state, // effectively ignoring the current state. @@ -279,12 +268,13 @@ func (n *Node) Reset(ctx context.Context) error { startTime := time.Now() // Generate a new genesis state based on the current packages - pkgsTxs, err := n.pkgs.Load(DefaultFee, startTime) + pkgs, err := n.loader.LoadPackages() if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } // Append initialTxs + pkgsTxs := n.generateTxs(DefaultFee, pkgs) txs := append(pkgsTxs, n.initialState...) genesis := gnoland.GnoGenesisState{ Balances: n.config.BalancesList, @@ -297,6 +287,7 @@ func (n *Node) Reset(ctx context.Context) error { return fmt.Errorf("unable to initialize a new node: %w", err) } + n.pkgs = pkgs n.loadedPackages = len(pkgsTxs) n.currentStateIndex = len(n.initialState) n.startTime = startTime @@ -310,15 +301,15 @@ func (n *Node) ReloadAll(ctx context.Context) error { n.muNode.Lock() defer n.muNode.Unlock() - pkgs := n.pkgs.toList() - paths := make([]string, len(pkgs)) - for i, pkg := range pkgs { - paths[i] = pkg.Dir - } + // pkgs := n.pkgs.toList() + // paths := make([]string, len(pkgs)) + // for i, pkg := range pkgs { + // paths[i] = pkg.Dir + // } - if err := n.updatePackages(paths...); err != nil { - return fmt.Errorf("unable to reload packages: %w", err) - } + // if err := n.updatePackages(paths...); err != nil { + // return fmt.Errorf("unable to reload packages: %w", err) + // } return n.rebuildNodeFromState(ctx) } @@ -392,6 +383,43 @@ func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata return state, nil } +func (n *Node) generateTxs(fee std.Fee, pkgs []packages.Package) []gnoland.TxWithMetadata { + metatxs := make([]gnoland.TxWithMetadata, 0, len(pkgs)) + for _, pkg := range pkgs { + msg := vm.MsgAddPackage{ + Creator: n.config.DefaultCreator, + Deposit: n.config.DefaultDeposit, + Package: &pkg.MemPackage, + } + + // XXX: + // if m, ok := n.pkgsModifier[pkg.Path]; ok { + // if !m.Creator.IsZero() { + // msg.Creator = m.Creator + // } + + // if m.Deposit != nil { + // msg.Deposit = m.Deposit + // } + // } + + // Create transaction + tx := std.Tx{Fee: fee, Msgs: []std.Msg{msg}} + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + + // Wrap it with metadata + metatx := gnoland.TxWithMetadata{ + Tx: tx, + Metadata: &gnoland.GnoTxMetadata{ + Timestamp: n.startTime.Unix(), + }, + } + metatxs = append(metatxs, metatx) + } + + return metatxs +} + func (n *Node) stopIfRunning() error { if n.Node != nil && n.Node.IsRunning() { if err := n.Node.Stop(); err != nil { @@ -407,13 +435,14 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { // If NoReplay is true, simply reset the node to its initial state n.logger.Warn("replay disabled") - txs, err := n.pkgs.Load(DefaultFee, n.startTime) + pkgs, err := n.loader.LoadPackages() if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } return n.rebuildNode(ctx, gnoland.GnoGenesisState{ - Balances: n.config.BalancesList, Txs: txs, + Balances: n.config.BalancesList, + Txs: n.generateTxs(DefaultFee, pkgs), }) } @@ -423,12 +452,13 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) + pkgs, err := n.loader.LoadPackages() if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } // Create genesis with loaded pkgs + previous state + pkgsTxs := n.generateTxs(DefaultFee, pkgs) genesis := gnoland.GnoGenesisState{ Balances: n.config.BalancesList, Txs: append(pkgsTxs, state...), @@ -439,8 +469,10 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { n.logger.Info("reload done", "pkgs", len(pkgsTxs), "state applied", len(state)) // Update node infos + n.pkgs = pkgs n.loadedPackages = len(pkgsTxs) + // Emit reload event n.emitter.Emit(&events.Reload{}) return nil } @@ -514,6 +546,7 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) node, nodeErr := gnoland.NewInMemoryNode(noopLogger, nodeConfig) if nodeErr != nil { return fmt.Errorf("unable to create a new node: %w", err) + } node.EventSwitch().AddListener("dev-emitter", n.handleEventTX) @@ -541,7 +574,8 @@ func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result // XXX: for now, this is only way to catch the error before, after, found := strings.Cut(res.Log, "\n") if !found { - n.logger.Error("unable to send tx", "err", res.Error, "log", res.Log) + err := fmt.Sprintf("%#v\n", res.Error) + n.logger.Error("unable to send tx", "err", err, "log", res.Log) return } diff --git a/contribs/gnodev/pkg/dev/node_pkgs.go b/contribs/gnodev/pkg/dev/node_pkgs.go new file mode 100644 index 00000000000..89455dfe862 --- /dev/null +++ b/contribs/gnodev/pkg/dev/node_pkgs.go @@ -0,0 +1 @@ +package dev diff --git a/contribs/gnodev/pkg/dev/node_state.go b/contribs/gnodev/pkg/dev/node_state.go index 73362a5f1c8..f5cd2776fe7 100644 --- a/contribs/gnodev/pkg/dev/node_state.go +++ b/contribs/gnodev/pkg/dev/node_state.go @@ -84,14 +84,10 @@ func (n *Node) MoveBy(ctx context.Context, x int) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) - if err != nil { - return fmt.Errorf("unable to load pkgs: %w", err) - } - - newState := n.state[:newIndex] + pkgsTxs := n.generateTxs(DefaultFee, n.pkgs) // Create genesis with loaded pkgs + previous state + newState := n.state[:newIndex] genesis := gnoland.GnoGenesisState{ Balances: n.config.BalancesList, Txs: append(pkgsTxs, newState...), diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index e05e5a996fa..3fe2c773e57 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -67,7 +67,7 @@ func Render(_ string) string { return "foo" } // Call NewDevNode with no package should work cfg := DefaultNodeConfig(gnoenv.RootDir()) - cfg.PackagesPathList = []PackagePath{pkgpath} + cfg.PackagesModifier = []PackagePath{pkgpath} cfg.Logger = logger node, err := NewDevNode(ctx, cfg) require.NoError(t, err) @@ -476,7 +476,7 @@ func generateTestingPackage(t *testing.T, nameFile ...string) PackagePath { func createDefaultTestingNodeConfig(pkgslist ...PackagePath) *NodeConfig { cfg := DefaultNodeConfig(gnoenv.RootDir()) - cfg.PackagesPathList = pkgslist + cfg.PackagesModifier = pkgslist return cfg } @@ -499,7 +499,7 @@ func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.Se node, err := NewDevNode(ctx, cfg) require.NoError(t, err) - assert.Len(t, node.ListPkgs(), len(cfg.PackagesPathList)) + assert.Len(t, node.ListPkgs(), len(cfg.PackagesModifier)) t.Cleanup(func() { node.Close() diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go deleted file mode 100644 index cccbf316525..00000000000 --- a/contribs/gnodev/pkg/dev/packages.go +++ /dev/null @@ -1,170 +0,0 @@ -package dev - -import ( - "errors" - "fmt" - "net/url" - "path/filepath" - "time" - - "github.com/gnolang/gno/contribs/gnodev/pkg/address" - "github.com/gnolang/gno/gno.land/pkg/gnoland" - vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - gno "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/pkg/gnomod" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/std" -) - -type PackagePath struct { - Path string - Creator crypto.Address - Deposit std.Coins -} - -func ResolvePackagePathQuery(bk *address.Book, path string) (PackagePath, error) { - var ppath PackagePath - - upath, err := url.Parse(path) - if err != nil { - return ppath, fmt.Errorf("malformed path/query: %w", err) - } - ppath.Path = filepath.Clean(upath.Path) - - // Check for creator option - creator := upath.Query().Get("creator") - if creator != "" { - address, err := crypto.AddressFromBech32(creator) - if err != nil { - var ok bool - address, ok = bk.GetByName(creator) - if !ok { - return ppath, fmt.Errorf("invalid name or address for creator %q", creator) - } - } - - ppath.Creator = address - } - - // Check for deposit option - deposit := upath.Query().Get("deposit") - if deposit != "" { - coins, err := std.ParseCoins(deposit) - if err != nil { - return ppath, fmt.Errorf( - "unable to parse deposit amount %q (should be in the form xxxugnot): %w", deposit, err, - ) - } - - ppath.Deposit = coins - } - - return ppath, nil -} - -type Package struct { - gnomod.Pkg - Creator crypto.Address - Deposit std.Coins -} - -type PackagesMap map[string]Package - -var ( - ErrEmptyCreatorPackage = errors.New("no creator specified for package") - ErrEmptyDepositPackage = errors.New("no deposit specified for package") -) - -func NewPackagesMap(ppaths []PackagePath) (PackagesMap, error) { - pkgs := make(map[string]Package) - for _, ppath := range ppaths { - if ppath.Creator.IsZero() { - return nil, fmt.Errorf("unable to load package %q: %w", ppath.Path, ErrEmptyCreatorPackage) - } - - abspath, err := filepath.Abs(ppath.Path) - if err != nil { - return nil, fmt.Errorf("unable to guess absolute path for %q: %w", ppath.Path, err) - } - - // list all packages from target path - pkgslist, err := gnomod.ListPkgs(abspath) - if err != nil { - return nil, fmt.Errorf("listing gno packages: %w", err) - } - - for _, pkg := range pkgslist { - if pkg.Dir == "" { - continue - } - - if _, ok := pkgs[pkg.Dir]; ok { - continue // skip - } - pkgs[pkg.Dir] = Package{ - Pkg: pkg, - Creator: ppath.Creator, - Deposit: ppath.Deposit, - } - } - } - - return pkgs, nil -} - -func (pm PackagesMap) toList() gnomod.PkgList { - list := make([]gnomod.Pkg, 0, len(pm)) - for _, pkg := range pm { - list = append(list, pkg.Pkg) - } - return list -} - -func (pm PackagesMap) Load(fee std.Fee, start time.Time) ([]gnoland.TxWithMetadata, error) { - pkgs := pm.toList() - - sorted, err := pkgs.Sort() - if err != nil { - return nil, fmt.Errorf("unable to sort pkgs: %w", err) - } - - nonDraft := sorted.GetNonDraftPkgs() - - metatxs := make([]gnoland.TxWithMetadata, 0, len(nonDraft)) - for _, modPkg := range nonDraft { - pkg := pm[modPkg.Dir] - if pkg.Creator.IsZero() { - return nil, fmt.Errorf("no creator set for %q", pkg.Dir) - } - - // Open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(modPkg.Dir, modPkg.Name) - if err := memPkg.Validate(); err != nil { - return nil, fmt.Errorf("invalid package: %w", err) - } - - // Create transaction - tx := std.Tx{ - Fee: fee, - Msgs: []std.Msg{ - vmm.MsgAddPackage{ - Creator: pkg.Creator, - Deposit: pkg.Deposit, - Package: memPkg, - }, - }, - } - - tx.Signatures = make([]std.Signature, len(tx.GetSigners())) - metatx := gnoland.TxWithMetadata{ - Tx: tx, - Metadata: &gnoland.GnoTxMetadata{ - Timestamp: start.Unix(), - }, - } - - metatxs = append(metatxs, metatx) - } - - return metatxs, nil -} diff --git a/contribs/gnodev/pkg/dev/packages_test.go b/contribs/gnodev/pkg/dev/packages_test.go index 151a89a7815..3410c2c66fb 100644 --- a/contribs/gnodev/pkg/dev/packages_test.go +++ b/contribs/gnodev/pkg/dev/packages_test.go @@ -88,7 +88,7 @@ func TestResolvePackagePathQuery(t *testing.T) { t.Run(tc.Path, func(t *testing.T) { t.Parallel() - result, err := ResolvePackagePathQuery(book, tc.Path) + result, err := ResolvePackageModifierQuery(book, tc.Path) if tc.ShouldFail { assert.Error(t, err) return diff --git a/contribs/gnodev/pkg/dev/query_path.go b/contribs/gnodev/pkg/dev/query_path.go new file mode 100644 index 00000000000..f1ac82ac64e --- /dev/null +++ b/contribs/gnodev/pkg/dev/query_path.go @@ -0,0 +1,65 @@ +package dev + +import ( + "fmt" + "net/url" + "path/filepath" + + "github.com/gnolang/gno/contribs/gnodev/pkg/address" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type PackageModifier struct { + Path string + Creator crypto.Address + Deposit std.Coins +} + +type PackageMetaMap struct { + Creator crypto.Address + Deposit std.Coins + + queries map[string]PackageModifier +} + +func ResolvePackageModifierQuery(bk *address.Book, path string) (PackageModifier, error) { + var query PackageModifier + + upath, err := url.Parse(path) + if err != nil { + return query, fmt.Errorf("malformed path/query: %w", err) + } + + path = filepath.Clean(upath.Path) + + // Check for creator option + creator := upath.Query().Get("creator") + if creator != "" { + address, err := crypto.AddressFromBech32(creator) + if err != nil { + var ok bool + address, ok = bk.GetByName(creator) + if !ok { + return query, fmt.Errorf("invalid name or address for creator %q", creator) + } + } + + query.Creator = address + } + + // Check for deposit option + deposit := upath.Query().Get("deposit") + if deposit != "" { + coins, err := std.ParseCoins(deposit) + if err != nil { + return query, fmt.Errorf( + "unable to parse deposit amount %q (should be in the form xxxugnot): %w", deposit, err, + ) + } + + query.Deposit = coins + } + + return query, nil +} diff --git a/contribs/gnodev/pkg/packages/loader.go b/contribs/gnodev/pkg/packages/loader.go new file mode 100644 index 00000000000..ce2ab594646 --- /dev/null +++ b/contribs/gnodev/pkg/packages/loader.go @@ -0,0 +1,95 @@ +package packages + +import ( + "errors" + "fmt" + "go/parser" + "go/token" + "strings" +) + +var ErrNoResolvers = errors.New("no resolvers setup") + +type Loader struct { + Paths []string + Resolver Resolver +} + +func (l Loader) LoadPackages() ([]Package, error) { + if l.Resolver == nil { + return nil, ErrNoResolvers + } + + fset := token.NewFileSet() + visited, stack := map[string]bool{}, map[string]bool{} + pkgs := make([]Package, 0) + for _, root := range l.Paths { + deps, err := loadPackage(root, fset, l.Resolver, visited, stack) + if err != nil { + return nil, fmt.Errorf("unable to load sorted packages: %w", err) + } + pkgs = append(pkgs, deps...) + } + + return pkgs, nil +} + +func loadPackage(path string, fset *token.FileSet, resolver Resolver, visited, stack map[string]bool) ([]Package, error) { + if stack[path] { + return nil, fmt.Errorf("cycle detected: %s", path) + } + if visited[path] { + return nil, nil + } + + visited[path] = true + + // XXX: do not hardcode this + if !strings.HasPrefix(path, "gno.land") { + return nil, nil + } + + mempkg, err := resolver.Resolve(path) + if err != nil { + return nil, fmt.Errorf("unable to resolve package %q: %w", path, err) + } + + var name string + imports := map[string]struct{}{} + for _, file := range mempkg.Files { + f, err := parser.ParseFile(fset, file.Name, file.Body, parser.AllErrors) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", file.Name, err) + } + + if name != "" && name != f.Name.Name { + return nil, fmt.Errorf("conflict package name between %q and %q", name, f.Name.Name) + } + + for _, imp := range f.Imports { + if len(imp.Path.Value) <= 2 { + continue + } + + val := imp.Path.Value[1 : len(imp.Path.Value)-1] + imports[val] = struct{}{} + } + + name = f.Name.Name + } + + pkgs := []Package{} + for imp := range imports { + subDeps, err := loadPackage(imp, fset, resolver, visited, stack) + if err != nil { + return nil, fmt.Errorf("unable to load %q: %w", imp, err) + } + + pkgs = append(pkgs, subDeps...) + } + pkgs = append(pkgs, *mempkg) + + stack[path] = false + + return pkgs, nil +} diff --git a/contribs/gnodev/pkg/packages/meta.go b/contribs/gnodev/pkg/packages/meta.go new file mode 100644 index 00000000000..69347c0ad4d --- /dev/null +++ b/contribs/gnodev/pkg/packages/meta.go @@ -0,0 +1 @@ +package packages diff --git a/contribs/gnodev/pkg/packages/resolver.go b/contribs/gnodev/pkg/packages/resolver.go new file mode 100644 index 00000000000..26aa8ef331a --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver.go @@ -0,0 +1,44 @@ +package packages + +import ( + "errors" + "fmt" + + "github.com/gnolang/gno/gnovm" +) + +var ErrResolverPackageNotFound = errors.New("package not found") + +type PackageKind int + +const ( + PackageKindOther = iota + PackageKindFS +) + +type Package struct { + gnovm.MemPackage + Kind PackageKind + Location string +} + +type Resolver interface { + Resolve(path string) (*Package, error) +} + +type ChainedResolver []Resolver + +func (cr ChainedResolver) Resolve(path string) (*Package, error) { + for _, resolver := range cr { + pkg, err := resolver.Resolve(path) + if err == nil { + return pkg, nil + } else if errors.Is(err, ErrResolverPackageNotFound) { + continue + } + + return nil, fmt.Errorf("unable to resolve %q: %w", path, err) + } + + return nil, ErrResolverPackageNotFound +} diff --git a/contribs/gnodev/pkg/packages/resolver_fs.go b/contribs/gnodev/pkg/packages/resolver_fs.go new file mode 100644 index 00000000000..1ba2a6963f8 --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_fs.go @@ -0,0 +1,43 @@ +package packages + +import ( + "fmt" + "go/token" + "os" + "path/filepath" +) + +type fsResolver struct { + rootsPath []string // Root folder + fset *token.FileSet +} + +func NewFSResolver(rootpath ...string) Resolver { + return &fsResolver{ + rootsPath: rootpath, + fset: token.NewFileSet(), + } +} + +func (res *fsResolver) Resolve(path string) (*Package, error) { + dir, ok := res.findDirForPath(path) + if !ok { + return nil, fmt.Errorf("unable to determine dir for path %q: %w", path, ErrResolverPackageNotFound) + } + + return ReadPackageFromDir(res.fset, path, dir) +} + +func (res *fsResolver) findDirForPath(path string) (dir string, ok bool) { + for _, root := range res.rootsPath { + dir = filepath.Join(root, path) + _, err := os.Stat(dir) + if err != nil { + continue + } + + return dir, true + } + + return "", false +} diff --git a/contribs/gnodev/pkg/packages/resolver_local.go b/contribs/gnodev/pkg/packages/resolver_local.go new file mode 100644 index 00000000000..8340253651a --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_local.go @@ -0,0 +1,54 @@ +package packages + +import ( + "go/token" + "strings" + + "github.com/gnolang/gno/gnovm/pkg/gnomod" +) + +type LocalResolver struct { + Path string + Dir string + + fset *token.FileSet +} + +func NewLocalResolver(path, dir string) *LocalResolver { + return &LocalResolver{ + fset: token.NewFileSet(), + Path: path, + Dir: dir, + } +} + +func GuessLocalResolverFromRoots(dir string, roots []string) *LocalResolver { + for _, root := range roots { + if !strings.HasPrefix(dir, root) { + continue + } + + path := strings.TrimPrefix(dir, root) + return NewLocalResolver(path, dir) + } + + return nil +} + +func GuessLocalResolverGnoMod(dir string) *LocalResolver { + modfile, err := gnomod.ParseAt(dir) + if err != nil { + return nil + } + + path := modfile.Module.Mod.Path + return NewLocalResolver(path, dir) +} + +func (lr LocalResolver) Resolve(path string) (*Package, error) { + if path != lr.Path && path != lr.Dir { + return nil, ErrResolverPackageNotFound + } + + return ReadPackageFromDir(lr.fset, lr.Path, lr.Dir) +} diff --git a/contribs/gnodev/pkg/packages/utils.go b/contribs/gnodev/pkg/packages/utils.go new file mode 100644 index 00000000000..8f4889dd8ba --- /dev/null +++ b/contribs/gnodev/pkg/packages/utils.go @@ -0,0 +1,67 @@ +package packages + +import ( + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" + "strings" + + "github.com/gnolang/gno/gnovm" +) + +func isGnoFile(name string) bool { + return filepath.Ext(name) == ".gno" && !strings.HasPrefix(name, ".") +} + +func isTestFile(name string) bool { + return strings.HasSuffix(name, "_filetest.gno") || strings.HasSuffix(name, "_test.gno") +} + +func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) { + files, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("unable to read dir %q: %w", dir, err) + } + + var name string + memFiles := []*gnovm.MemFile{} + for _, file := range files { + fname := file.Name() + if !isGnoFile(fname) || isTestFile(fname) { + continue + } + + filepath := filepath.Join(dir, fname) + body, err := os.ReadFile(filepath) + if err != nil { + return nil, fmt.Errorf("unable to read file %q: %w", filepath, err) + } + + f, err := parser.ParseFile(fset, fname, body, parser.PackageClauseOnly) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) + } + + if name != "" && name != f.Name.Name { + return nil, fmt.Errorf("conflict package name between %q and %q", name, f.Name.Name) + } + + name = f.Name.Name + memFiles = append(memFiles, &gnovm.MemFile{ + Name: fname, + Body: string(body), + }) + } + + return &Package{ + MemPackage: gnovm.MemPackage{ + Name: name, + Path: path, + Files: memFiles, + }, + Location: dir, + Kind: PackageKindFS, + }, nil +} diff --git a/contribs/gnodev/pkg/watcher/watch.go b/contribs/gnodev/pkg/watcher/watch.go index 63158a06c4b..5f277fd6646 100644 --- a/contribs/gnodev/pkg/watcher/watch.go +++ b/contribs/gnodev/pkg/watcher/watch.go @@ -5,15 +5,14 @@ import ( "fmt" "log/slog" "path/filepath" - "sort" "strings" "time" emitter "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" events "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/fsnotify/fsnotify" - "github.com/gnolang/gno/gnovm/pkg/gnomod" ) type PackageWatcher struct { @@ -25,7 +24,6 @@ type PackageWatcher struct { logger *slog.Logger watcher *fsnotify.Watcher - pkgsDir []string emitter emitter.Emitter } @@ -39,7 +37,6 @@ func NewPackageWatcher(logger *slog.Logger, emitter emitter.Emitter) (*PackageWa p := &PackageWatcher{ ctx: ctx, stop: cancel, - pkgsDir: []string{}, logger: logger, watcher: watcher, emitter: emitter, @@ -114,58 +111,61 @@ func (p *PackageWatcher) Stop() { p.stop() } -// AddPackages adds new packages to the watcher. -// Packages are sorted by their length in descending order to facilitate easier -// and more efficient matching with corresponding paths. The longest paths are -// compared first. -func (p *PackageWatcher) AddPackages(pkgs ...gnomod.Pkg) error { +func (p *PackageWatcher) UpdatePackagesWatch(pkgs ...packages.Package) { + watchList := p.watcher.WatchList() + + oldPkgs := make(map[string]struct{}, len(watchList)) + for _, path := range watchList { + oldPkgs[path] = struct{}{} + } + + newPkgs := make(map[string]struct{}, len(pkgs)) for _, pkg := range pkgs { - dir := pkg.Dir + if pkg.Kind != packages.PackageKindFS { + continue + } - abs, err := filepath.Abs(dir) + path, err := filepath.Abs(pkg.Location) if err != nil { - return fmt.Errorf("unable to get absolute path of %q: %w", dir, err) + p.logger.Error("Unable to get absolute path", "path", pkg.Location, "error", err) + continue } - // Use binary search to find the correct insertion point - index := sort.Search(len(p.pkgsDir), func(i int) bool { - return len(p.pkgsDir[i]) <= len(dir) // Longest paths first - }) + newPkgs[path] = struct{}{} + } - // Check for duplicates - if index < len(p.pkgsDir) && p.pkgsDir[index] == dir { - continue // Skip + for path := range oldPkgs { + if _, exists := newPkgs[path]; !exists { + p.watcher.Remove(path) + p.logger.Debug("Watcher list: removed", "path", path) } + } - // Insert the package - p.pkgsDir = append(p.pkgsDir[:index], append([]string{abs}, p.pkgsDir[index:]...)...) - - // Add the package to the watcher and handle any errors - if err := p.watcher.Add(abs); err != nil { - return fmt.Errorf("unable to watch %q: %w", pkg.Dir, err) + for path := range newPkgs { + if _, exists := oldPkgs[path]; !exists { + p.watcher.Add(path) + p.logger.Debug("Watcher list: added", "path", path) } } - - return nil } func (p *PackageWatcher) generatePackagesUpdateList(paths []string) PackageUpdateList { pkgsUpdate := []events.PackageUpdate{} mpkgs := map[string]*events.PackageUpdate{} // Pkg -> Update + watchList := p.watcher.WatchList() for _, path := range paths { - for _, pkg := range p.pkgsDir { - dirPath := filepath.Dir(path) + for _, pkg := range watchList { + if len(pkg) == len(path) { + continue // Skip if pkg == path + } // Check if a package directory contain our path directory + dirPath := filepath.Dir(path) if !strings.HasPrefix(pkg, dirPath) { continue } - if len(pkg) == len(path) { - continue // Skip if pkg == path - } - // Accumulate file updates for each package pkgu, ok := mpkgs[pkg] if !ok { diff --git a/examples/gno.land/r/demo/boards/render.gno b/examples/gno.land/r/demo/boards/render.gno index 3709ad02e5d..7ef86f0c3dd 100644 --- a/examples/gno.land/r/demo/boards/render.gno +++ b/examples/gno.land/r/demo/boards/render.gno @@ -13,12 +13,13 @@ func RenderBoard(bid BoardID) string { if board == nil { return "missing board" } + return board.RenderBoard() } func Render(path string) string { if path == "" { - str := "These are all the boards of this realm:\n\n" + str := "are all the boards of this realm:\n\n" gBoards.Iterate("", "", func(key string, value interface{}) bool { board := value.(*Board) str += " * [" + board.url + "](" + board.url + ")\n" @@ -26,6 +27,7 @@ func Render(path string) string { }) return str } + parts := strings.Split(path, "/") if len(parts) == 1 { // /r/demo/boards:BOARD_NAME diff --git a/gnovm/stdlibs/regexp/syntax/parse_test.gno b/gnovm/stdlibs/regexp/syntax/parse_test.gno index 0558b95bbe7..68e6a58dacb 100644 --- a/gnovm/stdlibs/regexp/syntax/parse_test.gno +++ b/gnovm/stdlibs/regexp/syntax/parse_test.gno @@ -290,7 +290,7 @@ func testParseDump(t *testing.T, tests []parseTest, flags Flags) { } // dump prints a string representation of the regexp showing -// the structure explicitly. +// the structure exicitly. func dump(re *Regexp) string { var b strings.Builder dumpRegexp(&b, re) From fd13a5a29609e6343ff362ff4ea80ed5ed67eb3b Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:18:20 +0100 Subject: [PATCH 02/24] chore: fixup Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/demo/boards/render.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/boards/render.gno b/examples/gno.land/r/demo/boards/render.gno index 7ef86f0c3dd..1cbf6aaa087 100644 --- a/examples/gno.land/r/demo/boards/render.gno +++ b/examples/gno.land/r/demo/boards/render.gno @@ -19,7 +19,7 @@ func RenderBoard(bid BoardID) string { func Render(path string) string { if path == "" { - str := "are all the boards of this realm:\n\n" + str := "There are all the boards of this realm:\n\n" gBoards.Iterate("", "", func(key string, value interface{}) bool { board := value.(*Board) str += " * [" + board.url + "](" + board.url + ")\n" From 135f8546864e69157751ac958120cd5aafd9295a Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sat, 30 Nov 2024 09:11:28 +0100 Subject: [PATCH 03/24] feat: add remote resolver & fixup Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/main.go | 76 ++++----- contribs/gnodev/cmd/gnodev/setup_resolver.go | 83 ++++++++++ contribs/gnodev/pkg/dev/node.go | 57 +------ contribs/gnodev/pkg/packages/loader.go | 21 +-- contribs/gnodev/pkg/packages/meta.go | 1 - contribs/gnodev/pkg/packages/resolver.go | 150 +++++++++++++++++- contribs/gnodev/pkg/packages/resolver_fs.go | 43 ----- .../gnodev/pkg/packages/resolver_local.go | 31 ++-- .../gnodev/pkg/packages/resolver_remote.go | 97 +++++++++++ contribs/gnodev/pkg/packages/resolver_root.go | 30 ++++ contribs/gnodev/pkg/packages/utils.go | 25 ++- 11 files changed, 437 insertions(+), 177 deletions(-) create mode 100644 contribs/gnodev/cmd/gnodev/setup_resolver.go delete mode 100644 contribs/gnodev/pkg/packages/meta.go delete mode 100644 contribs/gnodev/pkg/packages/resolver_fs.go create mode 100644 contribs/gnodev/pkg/packages/resolver_remote.go create mode 100644 contribs/gnodev/pkg/packages/resolver_root.go diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 964d913464f..9772d025824 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -7,16 +7,13 @@ import ( "fmt" "log/slog" "net/http" - "net/url" "os" "path/filepath" - "strings" "time" "github.com/gnolang/gno/contribs/gnodev/pkg/address" gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" - "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" "github.com/gnolang/gno/gno.land/pkg/integration" @@ -32,6 +29,7 @@ const ( KeyPressLogName = "KeyPress" EventServerLogName = "Event" AccountsLogName = "Accounts" + ResolverLogName = "Resolver" ) var ErrConflictingFileArgs = errors.New("cannot specify `balances-file` or `txs-file` along with `genesis-file`") @@ -65,6 +63,9 @@ type devCfg struct { webListenerAddr string webRemoteHelperAddr string + // Resolver + resolvers varResolver + // Node Configuration minimal bool verbose bool @@ -146,6 +147,12 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "web server help page's remote addr (default to )", ) + fs.Var( + &c.resolvers, + "resolver", + "list of addtional resolvers, will be exectued in the same order", + ) + fs.StringVar( &c.nodeRPCListenerAddr, "node-rpc-listener", @@ -262,32 +269,6 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { } } - dir, err := os.Getwd() - if err != nil { - return fmt.Errorf("unable to guess current dir: %w", err) - } - - var resolvers []packages.Resolver - gnoroot := gnoenv.RootDir() - localresolver := packages.GuessLocalResolverGnoMod(dir) - if localresolver == nil { - localresolver = packages.GuessLocalResolverFromRoots(dir, []string{gnoroot}) - if localresolver == nil { - return fmt.Errorf("unable to guess current package: %w", err) - } - } - - path := localresolver.Path - resolvers = append(resolvers, localresolver) - - exampleRoot := filepath.Join(gnoroot, "examples") - fsResolver := packages.NewFSResolver(exampleRoot) - resolvers = append(resolvers, fsResolver) - loader := &packages.Loader{ - Paths: []string{path}, - Resolver: packages.ChainedResolver(resolvers), - } - if err := cfg.validateConfigFlags(); err != nil { return fmt.Errorf("validate error: %w", err) } @@ -309,6 +290,17 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { loggerEvents := logger.WithGroup(EventServerLogName) emitterServer := emitter.NewServer(loggerEvents) + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("unable to guess current dir: %w", err) + } + + loader, err := setupPackagesLoader(logger.WithGroup(ResolverLogName), cfg, dir) + if err != nil { + return fmt.Errorf("unable to setup resolver: %w", err) + } + loader.Host = "gno.land" + // load keybase book, err := setupAddressBook(logger.WithGroup(AccountsLogName), cfg) if err != nil { @@ -372,26 +364,26 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { // XXX: TODO: // - create a map of known path that are allowed // - move this - u, err := url.Parse("http://" + path) - if err != nil { - return fmt.Errorf("malformed path %q: %w", path, err) - } - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !strings.HasPrefix(r.URL.Path, "/static") && !strings.HasPrefix(r.URL.Path, u.Path) { - http.Redirect(w, r, u.Path, http.StatusFound) - return - } + // u, err := url.Parse("http://" + path) + // if err != nil { + // return fmt.Errorf("malformed path %q: %w", path, err) + // } + // handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // if !strings.HasPrefix(r.URL.Path, "/static") && !strings.HasPrefix(r.URL.Path, u.Path) { + // http.Redirect(w, r, u.Path, http.StatusFound) + // return + // } - webhandler.ServeHTTP(w, r) - }) + // webhandler.ServeHTTP(w, r) + // }) // Setup HotReload if needed if !cfg.noWatch { evtstarget := fmt.Sprintf("%s/_events", server.Addr) mux.Handle("/_events", emitterServer) - mux.Handle("/", emitter.NewMiddleware(evtstarget, handler)) + mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler)) } else { - mux.Handle("/", handler) + mux.Handle("/", webhandler) } go func() { diff --git a/contribs/gnodev/cmd/gnodev/setup_resolver.go b/contribs/gnodev/cmd/gnodev/setup_resolver.go new file mode 100644 index 00000000000..f618c0d28a8 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/setup_resolver.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + "go/scanner" + "log/slog" + "path/filepath" + "strings" + + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" +) + +type varResolver []packages.Resolver + +func (va varResolver) String() string { + resolvers := packages.ChainedResolver(va) + return resolvers.Name() +} + +func (va *varResolver) Set(value string) error { + name, location, found := strings.Cut(value, "=") + if !found { + return fmt.Errorf("invalid resolver format %q, should be `=`", value) + } + + var res packages.Resolver + switch name { + case "remote": + rpc, err := client.NewHTTPClient(location) + if err != nil { + return fmt.Errorf("invalid resolver remote location: %q", location, name) + } + + res = packages.Cache(packages.NewRemoteResolver(rpc)) + case "root": + res = packages.NewRootResolver(location) + // case "pkgdir": + // res = packages.NewLo(location) + default: + return fmt.Errorf("invalid resolver name: %q", name) + } + + *va = append(*va, res) + return nil +} + +func setupPackagesLoader(logger *slog.Logger, cfg *devCfg, dir string) (*packages.Loader, error) { + // Setup first resolver for the current package directory + gnoroot := cfg.root + localresolver, path := packages.GuessLocalResolverGnoMod(dir) + if localresolver == nil { + localresolver, path = packages.GuessLocalResolverFromRoots(dir, []string{gnoroot}) + if localresolver == nil { + return nil, fmt.Errorf("unable to guess current package") + } + } + + // Add root resolvers + exampleRoot := filepath.Join(gnoroot, "examples") + fsResolver := packages.NewRootResolver(exampleRoot) + + resolver := packages.ChainWithLogger(logger, + localresolver, + packages.ChainResolvers(cfg.resolvers...), + fsResolver, + ) + + return &packages.Loader{ + Paths: []string{path}, + Resolver: packages.SyntaxChecker(resolver, resolverErrorHandler(logger)), + }, nil +} + +func resolverErrorHandler(logger *slog.Logger) packages.SyntaxErrorHandler { + return func(path string, filename string, serr *scanner.Error) { + logger.Error("syntax error", + "path", path, + "filename", filename, + "err", serr.Error(), + ) + } +} diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 4697132e6b0..863a3caab53 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -199,60 +199,6 @@ func (n *Node) getLatestBlockNumber() uint64 { return uint64(n.Node.BlockStore().Height()) } -// UpdatePackages updates the currently known packages. It will be taken into -// consideration in the next reload of the node. -// func (n *Node) UpdatePackages(paths ...string) error { -// n.muNode.Lock() -// defer n.muNode.Unlock() - -// return n.updatePackages(paths...) -// } - -// func (n *Node) updatePackages(paths ...string) error { -// var pkgsUpdated int -// for _, path := range paths { -// abspath, err := filepath.Abs(path) -// if err != nil { -// return fmt.Errorf("unable to resolve abs path of %q: %w", path, err) -// } - -// // Check if we already know the path (or its parent) and set -// // associated deployer and deposit -// deployer := n.config.DefaultCreator -// var deposit std.Coins -// for _, ppath := range n.config.PackagesPathList { -// if !strings.HasPrefix(abspath, ppath.Path) { -// continue -// } - -// deployer = ppath.Creator -// deposit = ppath.Deposit -// } - -// // List all packages from target path -// pkgslist, err := gnomod.ListPkgs(abspath) -// if err != nil { -// return fmt.Errorf("failed to list gno packages for %q: %w", path, err) -// } - -// // Update or add package in the current known list. -// for _, pkg := range pkgslist { -// n.pkgs[pkg.Dir] = ModPackage{ -// Pkg: pkg, -// Creator: deployer, -// Deposit: deposit, -// } - -// n.logger.Debug("pkgs update", "name", pkg.Name, "path", pkg.Dir) -// } - -// pkgsUpdated += len(pkgslist) -// } - -// n.logger.Info(fmt.Sprintf("updated %d packages", pkgsUpdated)) -// return nil -// } - // Reset stops the node, if running, and reloads it with a new genesis state, // effectively ignoring the current state. func (n *Node) Reset(ctx context.Context) error { @@ -574,8 +520,7 @@ func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result // XXX: for now, this is only way to catch the error before, after, found := strings.Cut(res.Log, "\n") if !found { - err := fmt.Sprintf("%#v\n", res.Error) - n.logger.Error("unable to send tx", "err", err, "log", res.Log) + n.logger.Error("unable to send tx", "log", res.Log) return } diff --git a/contribs/gnodev/pkg/packages/loader.go b/contribs/gnodev/pkg/packages/loader.go index ce2ab594646..ecc90949670 100644 --- a/contribs/gnodev/pkg/packages/loader.go +++ b/contribs/gnodev/pkg/packages/loader.go @@ -11,6 +11,7 @@ import ( var ErrNoResolvers = errors.New("no resolvers setup") type Loader struct { + Host string Paths []string Resolver Resolver } @@ -24,9 +25,9 @@ func (l Loader) LoadPackages() ([]Package, error) { visited, stack := map[string]bool{}, map[string]bool{} pkgs := make([]Package, 0) for _, root := range l.Paths { - deps, err := loadPackage(root, fset, l.Resolver, visited, stack) + deps, err := l.loadPackage(root, fset, l.Resolver, visited, stack) if err != nil { - return nil, fmt.Errorf("unable to load sorted packages: %w", err) + return nil, err } pkgs = append(pkgs, deps...) } @@ -34,7 +35,7 @@ func (l Loader) LoadPackages() ([]Package, error) { return pkgs, nil } -func loadPackage(path string, fset *token.FileSet, resolver Resolver, visited, stack map[string]bool) ([]Package, error) { +func (l Loader) loadPackage(path string, fset *token.FileSet, resolver Resolver, visited, stack map[string]bool) ([]Package, error) { if stack[path] { return nil, fmt.Errorf("cycle detected: %s", path) } @@ -44,20 +45,20 @@ func loadPackage(path string, fset *token.FileSet, resolver Resolver, visited, s visited[path] = true - // XXX: do not hardcode this - if !strings.HasPrefix(path, "gno.land") { + // do not try to load package that hasn't been prefixed + if l.Host != "" && !strings.HasPrefix(path, l.Host) { return nil, nil } - mempkg, err := resolver.Resolve(path) + mempkg, err := resolver.Resolve(fset, path) if err != nil { - return nil, fmt.Errorf("unable to resolve package %q: %w", path, err) + return nil, fmt.Errorf("unable to resolve package: %w", err) } var name string imports := map[string]struct{}{} for _, file := range mempkg.Files { - f, err := parser.ParseFile(fset, file.Name, file.Body, parser.AllErrors) + f, err := parser.ParseFile(fset, file.Name, file.Body, parser.ImportsOnly) if err != nil { return nil, fmt.Errorf("unable to parse file %q: %w", file.Name, err) } @@ -80,9 +81,9 @@ func loadPackage(path string, fset *token.FileSet, resolver Resolver, visited, s pkgs := []Package{} for imp := range imports { - subDeps, err := loadPackage(imp, fset, resolver, visited, stack) + subDeps, err := l.loadPackage(imp, fset, resolver, visited, stack) if err != nil { - return nil, fmt.Errorf("unable to load %q: %w", imp, err) + return nil, fmt.Errorf("importing %q: %w", imp, err) } pkgs = append(pkgs, subDeps...) diff --git a/contribs/gnodev/pkg/packages/meta.go b/contribs/gnodev/pkg/packages/meta.go deleted file mode 100644 index 69347c0ad4d..00000000000 --- a/contribs/gnodev/pkg/packages/meta.go +++ /dev/null @@ -1 +0,0 @@ -package packages diff --git a/contribs/gnodev/pkg/packages/resolver.go b/contribs/gnodev/pkg/packages/resolver.go index 26aa8ef331a..f6d81437892 100644 --- a/contribs/gnodev/pkg/packages/resolver.go +++ b/contribs/gnodev/pkg/packages/resolver.go @@ -3,6 +3,12 @@ package packages import ( "errors" "fmt" + "go/parser" + "go/scanner" + "go/token" + "log/slog" + "strings" + "time" "github.com/gnolang/gno/gnovm" ) @@ -12,7 +18,8 @@ var ErrResolverPackageNotFound = errors.New("package not found") type PackageKind int const ( - PackageKindOther = iota + PackageKindOther = iota + PackageKindRemote = iota PackageKindFS ) @@ -23,14 +30,76 @@ type Package struct { } type Resolver interface { - Resolve(path string) (*Package, error) + Name() string + Resolve(fset *token.FileSet, path string) (*Package, error) +} + +type logResolver struct { + logger *slog.Logger + Resolver +} + +func LogResolver(l *slog.Logger, r Resolver) Resolver { + return &logResolver{l, r} +} + +func (l logResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + start := time.Now() + pkg, err := l.Resolver.Resolve(fset, path) + if err == nil { + l.logger.Info("path resolved", + "resolver", l.Resolver.Name(), + "took", time.Since(start).String(), + "path", path, + "name", pkg.Name, + "location", pkg.Location) + } else if errors.Is(err, ErrResolverPackageNotFound) { + l.logger.Debug("path not found", + "resolver", l.Resolver.Name(), + "took", time.Since(start).String(), + "path", path) + + } else { + l.logger.Error("unable to resolve path", + "resolver", l.Resolver.Name(), + "took", time.Since(start).String(), + "path", path, + "err", err) + } + + return pkg, err } type ChainedResolver []Resolver -func (cr ChainedResolver) Resolve(path string) (*Package, error) { +func ChainResolvers(rs ...Resolver) Resolver { + return ChainedResolver(rs) +} + +func ChainWithLogger(logger *slog.Logger, rs ...Resolver) Resolver { + loggedResolvers := make([]Resolver, len(rs)) + for i, r := range rs { + loggedResolvers[i] = LogResolver(logger, r) + } + return ChainedResolver(loggedResolvers) +} + +func (cr ChainedResolver) Name() string { + var name strings.Builder + + for i, r := range cr { + if i > 0 { + name.WriteRune('/') + } + name.WriteString(r.Name()) + } + + return name.String() +} + +func (cr ChainedResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { for _, resolver := range cr { - pkg, err := resolver.Resolve(path) + pkg, err := resolver.Resolve(fset, path) if err == nil { return pkg, nil } else if errors.Is(err, ErrResolverPackageNotFound) { @@ -42,3 +111,76 @@ func (cr ChainedResolver) Resolve(path string) (*Package, error) { return nil, ErrResolverPackageNotFound } + +type inMemoryCacheResolver struct { + subr Resolver + cacheMap map[string] /* path */ *Package +} + +func Cache(r Resolver) Resolver { + return &inMemoryCacheResolver{ + subr: r, + cacheMap: map[string]*Package{}, + } +} + +func (r *inMemoryCacheResolver) Name() string { + return "cache_" + r.subr.Name() +} + +func (r *inMemoryCacheResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + if p, ok := r.cacheMap[path]; ok { + return p, nil + } + + p, err := r.subr.Resolve(fset, path) + if err != nil { + return nil, err + } + + r.cacheMap[path] = p + return p, nil +} + +type SyntaxErrorHandler func(path string, filename string, serr *scanner.Error) + +type SyntaxCheckerResolver struct { + SyntaxErrorHandler + Resolver +} + +func SyntaxChecker(r Resolver, handler SyntaxErrorHandler) Resolver { + return &SyntaxCheckerResolver{ + SyntaxErrorHandler: handler, + Resolver: r, + } +} + +func (SyntaxCheckerResolver) Name() string { + return "syntax_checker" +} + +func (r *SyntaxCheckerResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + p, err := r.Resolver.Resolve(fset, path) + if err != nil { + return nil, err + } + + for _, file := range p.Files { + _, err = parser.ParseFile(fset, file.Name, file.Body, parser.AllErrors) + if err == nil { + continue + } + + if el, ok := err.(scanner.ErrorList); ok { + for _, e := range el { + r.SyntaxErrorHandler(path, file.Name, e) + } + } + + return nil, fmt.Errorf("unable to parse %q: %w", + file.Name, err) + } + + return p, err +} diff --git a/contribs/gnodev/pkg/packages/resolver_fs.go b/contribs/gnodev/pkg/packages/resolver_fs.go deleted file mode 100644 index 1ba2a6963f8..00000000000 --- a/contribs/gnodev/pkg/packages/resolver_fs.go +++ /dev/null @@ -1,43 +0,0 @@ -package packages - -import ( - "fmt" - "go/token" - "os" - "path/filepath" -) - -type fsResolver struct { - rootsPath []string // Root folder - fset *token.FileSet -} - -func NewFSResolver(rootpath ...string) Resolver { - return &fsResolver{ - rootsPath: rootpath, - fset: token.NewFileSet(), - } -} - -func (res *fsResolver) Resolve(path string) (*Package, error) { - dir, ok := res.findDirForPath(path) - if !ok { - return nil, fmt.Errorf("unable to determine dir for path %q: %w", path, ErrResolverPackageNotFound) - } - - return ReadPackageFromDir(res.fset, path, dir) -} - -func (res *fsResolver) findDirForPath(path string) (dir string, ok bool) { - for _, root := range res.rootsPath { - dir = filepath.Join(root, path) - _, err := os.Stat(dir) - if err != nil { - continue - } - - return dir, true - } - - return "", false -} diff --git a/contribs/gnodev/pkg/packages/resolver_local.go b/contribs/gnodev/pkg/packages/resolver_local.go index 8340253651a..bfd8d764064 100644 --- a/contribs/gnodev/pkg/packages/resolver_local.go +++ b/contribs/gnodev/pkg/packages/resolver_local.go @@ -1,7 +1,9 @@ package packages import ( + "fmt" "go/token" + "path/filepath" "strings" "github.com/gnolang/gno/gnovm/pkg/gnomod" @@ -10,45 +12,48 @@ import ( type LocalResolver struct { Path string Dir string +} - fset *token.FileSet +func (l *LocalResolver) Name() string { + return fmt.Sprintf("local<%s>", filepath.Base(l.Dir)) } func NewLocalResolver(path, dir string) *LocalResolver { return &LocalResolver{ - fset: token.NewFileSet(), Path: path, Dir: dir, } } -func GuessLocalResolverFromRoots(dir string, roots []string) *LocalResolver { +func GuessLocalResolverFromRoots(dir string, roots []string) (res Resolver, path string) { for _, root := range roots { if !strings.HasPrefix(dir, root) { continue } - path := strings.TrimPrefix(dir, root) - return NewLocalResolver(path, dir) + path = strings.TrimPrefix(dir, root) + return NewLocalResolver(path, dir), path } - return nil + return nil, "" } -func GuessLocalResolverGnoMod(dir string) *LocalResolver { +func GuessLocalResolverGnoMod(dir string) (res Resolver, path string) { modfile, err := gnomod.ParseAt(dir) if err != nil { - return nil + return nil, "" } - path := modfile.Module.Mod.Path - return NewLocalResolver(path, dir) + path = modfile.Module.Mod.Path + return NewLocalResolver(path, dir), path } -func (lr LocalResolver) Resolve(path string) (*Package, error) { - if path != lr.Path && path != lr.Dir { +func (lr LocalResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + after, found := strings.CutPrefix(path, lr.Path) + if !found { return nil, ErrResolverPackageNotFound } - return ReadPackageFromDir(lr.fset, lr.Path, lr.Dir) + dir := filepath.Join(lr.Dir, after) + return ReadPackageFromDir(fset, path, dir) } diff --git a/contribs/gnodev/pkg/packages/resolver_remote.go b/contribs/gnodev/pkg/packages/resolver_remote.go new file mode 100644 index 00000000000..123eb514f60 --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_remote.go @@ -0,0 +1,97 @@ +package packages + +import ( + "bytes" + "errors" + "fmt" + "go/token" + "path/filepath" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" +) + +type remoteCaching interface { + Get(path string) +} + +type remoteResolver struct { + *client.RPCClient // Root folder + fset *token.FileSet +} + +func NewRemoteResolver(cl *client.RPCClient) Resolver { + return &remoteResolver{ + RPCClient: cl, + fset: token.NewFileSet(), + } +} + +func (res *remoteResolver) Name() string { + return fmt.Sprintf("remote") +} + +func (res *remoteResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + const qpath = "vm/qfile" + + // First query files + data := []byte(path) + qres, err := res.RPCClient.ABCIQuery(qpath, data) + if err != nil { + return nil, fmt.Errorf("client unable to query: %w", err) + } + + if err := qres.Response.Error; err != nil { + if errors.Is(err, vm.InvalidPkgPathError{}) { + return nil, ErrResolverPackageNotFound + } + + return nil, fmt.Errorf("querying %q error: %w", path, err) + } + + var name string + memFiles := []*gnovm.MemFile{} + files := bytes.Split(qres.Response.Data, []byte{'\n'}) + for _, filename := range files { + fname := string(filename) + + if !isGnoFile(fname) || isTestFile(fname) { + continue + } + + fpath := filepath.Join(path, fname) + qres, err := res.RPCClient.ABCIQuery(qpath, []byte(fpath)) + if err != nil { + return nil, fmt.Errorf("unable to query path") + } + + if err := qres.Response.Error; err != nil { + return nil, fmt.Errorf("unable to query file %q on path %q: %w", + string(fname), path, err) + } + + body := qres.Response.Data + memfile, pkgname, err := parseFile(fset, string(fname), body) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) + } + + if name != "" && name != pkgname { + return nil, fmt.Errorf("conflict package name between %q and %q", name, memfile.Name) + } + + name = pkgname + memFiles = append(memFiles, memfile) + } + + return &Package{ + MemPackage: gnovm.MemPackage{ + Name: name, + Path: path, + Files: memFiles, + }, + Kind: PackageKindRemote, + Location: path, + }, nil +} diff --git a/contribs/gnodev/pkg/packages/resolver_root.go b/contribs/gnodev/pkg/packages/resolver_root.go new file mode 100644 index 00000000000..b168094d01a --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_root.go @@ -0,0 +1,30 @@ +package packages + +import ( + "fmt" + "go/token" + "os" + "path/filepath" +) + +type rootResolver struct { + root string // Root folder +} + +func (l *rootResolver) Name() string { + return fmt.Sprintf("root<%s>", filepath.Base(l.root)) +} + +func NewRootResolver(rootpath string) Resolver { + return &rootResolver{root: rootpath} +} + +func (r *rootResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + dir := filepath.Join(r.root, path) + _, err := os.Stat(dir) + if err != nil { + return nil, fmt.Errorf("unable to determine dir for path %q: %w", path, ErrResolverPackageNotFound) + } + + return ReadPackageFromDir(fset, path, dir) +} diff --git a/contribs/gnodev/pkg/packages/utils.go b/contribs/gnodev/pkg/packages/utils.go index 8f4889dd8ba..24cc85c342c 100644 --- a/contribs/gnodev/pkg/packages/utils.go +++ b/contribs/gnodev/pkg/packages/utils.go @@ -39,20 +39,17 @@ func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) return nil, fmt.Errorf("unable to read file %q: %w", filepath, err) } - f, err := parser.ParseFile(fset, fname, body, parser.PackageClauseOnly) + memfile, pkgname, err := parseFile(fset, fname, body) if err != nil { return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) } - if name != "" && name != f.Name.Name { - return nil, fmt.Errorf("conflict package name between %q and %q", name, f.Name.Name) + if name != "" && name != pkgname { + return nil, fmt.Errorf("conflict package name between %q and %q", name, memfile.Name) } - name = f.Name.Name - memFiles = append(memFiles, &gnovm.MemFile{ - Name: fname, - Body: string(body), - }) + name = pkgname + memFiles = append(memFiles, memfile) } return &Package{ @@ -65,3 +62,15 @@ func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) Kind: PackageKindFS, }, nil } + +func parseFile(fset *token.FileSet, fname string, body []byte) (*gnovm.MemFile, string, error) { + f, err := parser.ParseFile(fset, fname, body, parser.PackageClauseOnly) + if err != nil { + return nil, "", fmt.Errorf("unable to parse file %q: %w", fname, err) + } + + return &gnovm.MemFile{ + Name: fname, + Body: string(body), + }, f.Name.Name, nil +} From d258fd57a0b1f8abe1baab395a78a61f4c081b36 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:20:27 +0100 Subject: [PATCH 04/24] wip: lazy loader Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/main.go | 18 +- .../{setup_resolver.go => setup_loader.go} | 60 +- contribs/gnodev/cmd/gnodev/setup_node.go | 8 +- contribs/gnodev/pkg/dev/node.go | 26 +- contribs/gnodev/pkg/dev/node_state_test.go | 387 ++++---- contribs/gnodev/pkg/dev/node_test.go | 939 +++++++++--------- contribs/gnodev/pkg/dev/packages_test.go | 185 ++-- contribs/gnodev/pkg/packages/loader.go | 38 +- contribs/gnodev/pkg/packages/package.go | 81 ++ contribs/gnodev/pkg/packages/resolver.go | 16 - .../gnodev/pkg/packages/resolver_local.go | 25 - contribs/gnodev/pkg/packages/resolver_mock.go | 38 + contribs/gnodev/pkg/packages/utils.go | 62 -- 13 files changed, 975 insertions(+), 908 deletions(-) rename contribs/gnodev/cmd/gnodev/{setup_resolver.go => setup_loader.go} (56%) create mode 100644 contribs/gnodev/pkg/packages/package.go create mode 100644 contribs/gnodev/pkg/packages/resolver_mock.go diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 9772d025824..924d88c6791 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -29,7 +29,7 @@ const ( KeyPressLogName = "KeyPress" EventServerLogName = "Event" AccountsLogName = "Accounts" - ResolverLogName = "Resolver" + LoaderLogName = "Loader" ) var ErrConflictingFileArgs = errors.New("cannot specify `balances-file` or `txs-file` along with `genesis-file`") @@ -295,11 +295,11 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { return fmt.Errorf("unable to guess current dir: %w", err) } - loader, err := setupPackagesLoader(logger.WithGroup(ResolverLogName), cfg, dir) - if err != nil { - return fmt.Errorf("unable to setup resolver: %w", err) + path, ok := guessPath(cfg, dir) + if !ok { + return fmt.Errorf("unable to guess path from %q", dir) } - loader.Host = "gno.land" + loader := setupPackagesLoader(logger.WithGroup(LoaderLogName), cfg, path, dir) // load keybase book, err := setupAddressBook(logger.WithGroup(AccountsLogName), cfg) @@ -307,12 +307,6 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { return fmt.Errorf("unable to load keybase: %w", err) } - // Check and Parse packages - // pkgpaths, err := resolvePackagesPathFromArgs(cfg, book, args) - // if err != nil { - // return fmt.Errorf("unable to parse package paths: %w", err) - // } - // generate balances balances, err := generateBalances(book, cfg) if err != nil { @@ -324,7 +318,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { // XXX: find a good way to export or display node logs nodeLogger := logger.WithGroup(NodeLogName) nodeCfg := setupDevNodeConfig(cfg, logger, emitterServer, balances, loader) - devNode, err := setupDevNode(ctx, cfg, nodeCfg) + devNode, err := setupDevNode(ctx, cfg, nodeCfg, path) if err != nil { return err } diff --git a/contribs/gnodev/cmd/gnodev/setup_resolver.go b/contribs/gnodev/cmd/gnodev/setup_loader.go similarity index 56% rename from contribs/gnodev/cmd/gnodev/setup_resolver.go rename to contribs/gnodev/cmd/gnodev/setup_loader.go index f618c0d28a8..3350df42089 100644 --- a/contribs/gnodev/cmd/gnodev/setup_resolver.go +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" ) @@ -45,17 +46,56 @@ func (va *varResolver) Set(value string) error { return nil } -func setupPackagesLoader(logger *slog.Logger, cfg *devCfg, dir string) (*packages.Loader, error) { - // Setup first resolver for the current package directory +func guessPathFromRoots(dir string, roots ...string) (path string, ok bool) { + for _, root := range roots { + if !strings.HasPrefix(dir, root) { + continue + } + + return strings.TrimPrefix(dir, root), true + } + + return "", false +} + +func guessPathGnoMod(dir string) (path string, ok bool) { + modfile, err := gnomod.ParseAt(dir) + if err == nil { + return modfile.Module.Mod.Path, true + + } + + return "", false +} + +func guessPath(cfg *devCfg, dir string) (path string, ok bool) { gnoroot := cfg.root - localresolver, path := packages.GuessLocalResolverGnoMod(dir) - if localresolver == nil { - localresolver, path = packages.GuessLocalResolverFromRoots(dir, []string{gnoroot}) - if localresolver == nil { - return nil, fmt.Errorf("unable to guess current package") + if path, ok := guessPathGnoMod(dir); ok { + return path, true + } + + if path, ok = guessPathFromRoots(dir, gnoroot); ok { + return path, ok + } + + return "", false +} + +func isStdPath(path string) bool { + if i := strings.IndexRune(path, '/'); i > 0 { + if j := strings.IndexRune(path[:i], '.'); i >= 0 { + return false } } + return true +} + +func setupPackagesLoader(logger *slog.Logger, cfg *devCfg, path, dir string) (loader *packages.Loader) { + gnoroot := cfg.root + + localresolver := packages.NewLocalResolver(path, dir) + // Add root resolvers exampleRoot := filepath.Join(gnoroot, "examples") fsResolver := packages.NewRootResolver(exampleRoot) @@ -66,10 +106,8 @@ func setupPackagesLoader(logger *slog.Logger, cfg *devCfg, dir string) (*package fsResolver, ) - return &packages.Loader{ - Paths: []string{path}, - Resolver: packages.SyntaxChecker(resolver, resolverErrorHandler(logger)), - }, nil + syntaxResolver := packages.SyntaxChecker(resolver, resolverErrorHandler(logger)) + return packages.NewLoaderWithFilter(isStdPath, syntaxResolver) } func resolverErrorHandler(logger *slog.Logger) packages.SyntaxErrorHandler { diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index b993af8b04e..93f02efc1d4 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -15,11 +15,7 @@ import ( ) // setupDevNode initializes and returns a new DevNode. -func setupDevNode( - ctx context.Context, - devCfg *devCfg, - nodeConfig *gnodev.NodeConfig, -) (*gnodev.Node, error) { +func setupDevNode(ctx context.Context, devCfg *devCfg, nodeConfig *gnodev.NodeConfig, path string) (*gnodev.Node, error) { logger := nodeConfig.Logger if devCfg.txsFile != "" { // Load txs files @@ -47,7 +43,7 @@ func setupDevNode( logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(stateTxs)) } - return gnodev.NewDevNode(ctx, nodeConfig) + return gnodev.NewDevNode(ctx, nodeConfig, path) } // setupDevNodeConfig creates and returns a new dev.NodeConfig. diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 863a3caab53..603284c1cf1 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -85,6 +85,7 @@ type Node struct { logger *slog.Logger loader packages.Loader pkgs []packages.Package + paths []string // keep track of number of loaded package to be able to skip them on restore loadedPackages int @@ -99,7 +100,7 @@ type Node struct { var DefaultFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) -func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { +func NewDevNode(ctx context.Context, cfg *NodeConfig, pkgpaths ...string) (*Node, error) { startTime := time.Now() devnode := &Node{ @@ -112,10 +113,11 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { state: cfg.InitialTxs, initialState: cfg.InitialTxs, currentStateIndex: len(cfg.InitialTxs), + paths: pkgpaths, // pkgsModifier: pkgsModifier, } - // XXX: MOVE THIS + // XXX: MOVE THIS, passing context here can be confusing if err := devnode.Reset(ctx); err != nil { return nil, fmt.Errorf("unable to initialize the node: %w", err) } @@ -148,6 +150,14 @@ func (n *Node) GetRemoteAddress() string { return n.Node.Config().RPC.ListenAddress } +// AddPackagePaths to load +func (n *Node) AddPackagePaths(paths ...string) { + n.muNode.Lock() + defer n.muNode.Unlock() + + n.paths = append(n.paths, paths...) +} + // GetBlockTransactions returns the transactions contained // within the specified block, if any func (n *Node) GetBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) { @@ -205,16 +215,11 @@ func (n *Node) Reset(ctx context.Context) error { n.muNode.Lock() defer n.muNode.Unlock() - // Stop the node if it's currently running. - if err := n.stopIfRunning(); err != nil { - return fmt.Errorf("unable to stop the node: %w", err) - } - // Reset starting time startTime := time.Now() // Generate a new genesis state based on the current packages - pkgs, err := n.loader.LoadPackages() + pkgs, err := n.loader.Load(n.paths...) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } @@ -233,7 +238,6 @@ func (n *Node) Reset(ctx context.Context) error { return fmt.Errorf("unable to initialize a new node: %w", err) } - n.pkgs = pkgs n.loadedPackages = len(pkgsTxs) n.currentStateIndex = len(n.initialState) n.startTime = startTime @@ -381,7 +385,7 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { // If NoReplay is true, simply reset the node to its initial state n.logger.Warn("replay disabled") - pkgs, err := n.loader.LoadPackages() + pkgs, err := n.loader.Load(n.paths...) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } @@ -398,7 +402,7 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { } // Load genesis packages - pkgs, err := n.loader.LoadPackages() + pkgs, err := n.loader.Load(n.paths...) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } diff --git a/contribs/gnodev/pkg/dev/node_state_test.go b/contribs/gnodev/pkg/dev/node_state_test.go index efaeb979693..ab792b05740 100644 --- a/contribs/gnodev/pkg/dev/node_state_test.go +++ b/contribs/gnodev/pkg/dev/node_state_test.go @@ -1,198 +1,193 @@ package dev -import ( - "context" - "strconv" - "testing" - "time" - - emitter "github.com/gnolang/gno/contribs/gnodev/internal/mock" - "github.com/gnolang/gno/contribs/gnodev/pkg/events" - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const testCounterRealm = "gno.land/r/dev/counter" - -func TestNodeMovePreviousTX(t *testing.T) { - const callInc = 5 - - node, emitter := testingCounterRealm(t, callInc) - - t.Run("Prev TX", func(t *testing.T) { - ctx := testingContext(t) - err := node.MoveToPreviousTX(ctx) - require.NoError(t, err) - assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) - - // Check for correct render update - render, err := testingRenderRealm(t, node, testCounterRealm) - require.NoError(t, err) - require.Equal(t, render, "4") - }) - - t.Run("Next TX", func(t *testing.T) { - ctx := testingContext(t) - err := node.MoveToNextTX(ctx) - require.NoError(t, err) - assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) - - // Check for correct render update - render, err := testingRenderRealm(t, node, testCounterRealm) - require.NoError(t, err) - require.Equal(t, render, "5") - }) - - t.Run("Multi Move TX", func(t *testing.T) { - ctx := testingContext(t) - moves := []struct { - Move int - ExpectedResult string - }{ - {-2, "3"}, - {2, "5"}, - {-5, "0"}, - {5, "5"}, - {-100, "0"}, - {100, "5"}, - {0, "5"}, - } - - t.Logf("initial state %d", callInc) - for _, tc := range moves { - t.Logf("moving from `%d`", tc.Move) - err := node.MoveBy(ctx, tc.Move) - require.NoError(t, err) - if tc.Move != 0 { - assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) - } - - // Check for correct render update - render, err := testingRenderRealm(t, node, testCounterRealm) - require.NoError(t, err) - require.Equal(t, render, tc.ExpectedResult) - } - }) -} - -func TestSaveCurrentState(t *testing.T) { - ctx := testingContext(t) - - node, emitter := testingCounterRealm(t, 2) - - // Save current state - err := node.SaveCurrentState(ctx) - require.NoError(t, err) - - // Send a new tx - msg := vm.MsgCall{ - PkgPath: testCounterRealm, - Func: "Inc", - Args: []string{"10"}, - } - - res, err := testingCallRealm(t, node, msg) - require.NoError(t, err) - require.NoError(t, res.CheckTx.Error) - require.NoError(t, res.DeliverTx.Error) - assert.Equal(t, events.EvtTxResult, emitter.NextEvent().Type()) - - // Test render - render, err := testingRenderRealm(t, node, testCounterRealm) - require.NoError(t, err) - require.Equal(t, render, "12") // 2 + 10 - - // Reset state - err = node.Reset(ctx) - require.NoError(t, err) - assert.Equal(t, events.EvtReset, emitter.NextEvent().Type()) - - render, err = testingRenderRealm(t, node, testCounterRealm) - require.NoError(t, err) - require.Equal(t, render, "2") // Back to the original state -} - -func TestExportState(t *testing.T) { - node, _ := testingCounterRealm(t, 3) - - t.Run("export state", func(t *testing.T) { - ctx := testingContext(t) - state, err := node.ExportCurrentState(ctx) - require.NoError(t, err) - assert.Equal(t, 3, len(state)) - }) - - t.Run("export genesis doc", func(t *testing.T) { - ctx := testingContext(t) - doc, err := node.ExportStateAsGenesis(ctx) - require.NoError(t, err) - require.NotNil(t, doc.AppState) - - state, ok := doc.AppState.(gnoland.GnoGenesisState) - require.True(t, ok) - assert.Equal(t, 3, len(state.Txs)) - }) -} - -func testingCounterRealm(t *testing.T, inc int) (*Node, *emitter.ServerEmitter) { - t.Helper() - - const ( - // foo package - counterGnoMod = "module gno.land/r/dev/counter\n" - counterFile = `package counter -import "strconv" - -var value int = 0 -func Inc(v int) { value += v } // method to increment value -func Render(_ string) string { return strconv.Itoa(value) } -` - ) - - // Generate package counter - counterPkg := generateTestingPackage(t, - "gno.mod", counterGnoMod, - "foo.gno", counterFile) - - // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, counterPkg) - assert.Len(t, node.ListPkgs(), 1) - - // Test rendering - render, err := testingRenderRealm(t, node, testCounterRealm) - require.NoError(t, err) - require.Equal(t, render, "0") - - // Increment the counter 10 times - for i := 0; i < inc; i++ { - t.Logf("call %d", i) - // Craft `Inc` msg - msg := vm.MsgCall{ - PkgPath: testCounterRealm, - Func: "Inc", - Args: []string{"1"}, - } - - res, err := testingCallRealm(t, node, msg) - require.NoError(t, err) - require.NoError(t, res.CheckTx.Error) - require.NoError(t, res.DeliverTx.Error) - assert.Equal(t, events.EvtTxResult, emitter.NextEvent().Type()) - } - - render, err = testingRenderRealm(t, node, testCounterRealm) - require.NoError(t, err) - require.Equal(t, render, strconv.Itoa(inc)) - - return node, emitter -} - -func testingContext(t *testing.T) context.Context { - t.Helper() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*7) - t.Cleanup(cancel) - return ctx -} +// import ( +// emitter "github.com/gnolang/gno/contribs/gnodev/internal/mock" +// "github.com/gnolang/gno/contribs/gnodev/pkg/events" +// "github.com/gnolang/gno/gno.land/pkg/gnoland" +// "github.com/gnolang/gno/gno.land/pkg/sdk/vm" +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/require" +// ) + +// const testCounterRealm = "gno.land/r/dev/counter" + +// func TestNodeMovePreviousTX(t *testing.T) { +// const callInc = 5 + +// node, emitter := testingCounterRealm(t, callInc) + +// t.Run("Prev TX", func(t *testing.T) { +// ctx := testingContext(t) +// err := node.MoveToPreviousTX(ctx) +// require.NoError(t, err) +// assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) + +// // Check for correct render update +// render, err := testingRenderRealm(t, node, testCounterRealm) +// require.NoError(t, err) +// require.Equal(t, render, "4") +// }) + +// t.Run("Next TX", func(t *testing.T) { +// ctx := testingContext(t) +// err := node.MoveToNextTX(ctx) +// require.NoError(t, err) +// assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) + +// // Check for correct render update +// render, err := testingRenderRealm(t, node, testCounterRealm) +// require.NoError(t, err) +// require.Equal(t, render, "5") +// }) + +// t.Run("Multi Move TX", func(t *testing.T) { +// ctx := testingContext(t) +// moves := []struct { +// Move int +// ExpectedResult string +// }{ +// {-2, "3"}, +// {2, "5"}, +// {-5, "0"}, +// {5, "5"}, +// {-100, "0"}, +// {100, "5"}, +// {0, "5"}, +// } + +// t.Logf("initial state %d", callInc) +// for _, tc := range moves { +// t.Logf("moving from `%d`", tc.Move) +// err := node.MoveBy(ctx, tc.Move) +// require.NoError(t, err) +// if tc.Move != 0 { +// assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) +// } + +// // Check for correct render update +// render, err := testingRenderRealm(t, node, testCounterRealm) +// require.NoError(t, err) +// require.Equal(t, render, tc.ExpectedResult) +// } +// }) +// } + +// func TestSaveCurrentState(t *testing.T) { +// ctx := testingContext(t) + +// node, emitter := testingCounterRealm(t, 2) + +// // Save current state +// err := node.SaveCurrentState(ctx) +// require.NoError(t, err) + +// // Send a new tx +// msg := vm.MsgCall{ +// PkgPath: testCounterRealm, +// Func: "Inc", +// Args: []string{"10"}, +// } + +// res, err := testingCallRealm(t, node, msg) +// require.NoError(t, err) +// require.NoError(t, res.CheckTx.Error) +// require.NoError(t, res.DeliverTx.Error) +// assert.Equal(t, events.EvtTxResult, emitter.NextEvent().Type()) + +// // Test render +// render, err := testingRenderRealm(t, node, testCounterRealm) +// require.NoError(t, err) +// require.Equal(t, render, "12") // 2 + 10 + +// // Reset state +// err = node.Reset(ctx) +// require.NoError(t, err) +// assert.Equal(t, events.EvtReset, emitter.NextEvent().Type()) + +// render, err = testingRenderRealm(t, node, testCounterRealm) +// require.NoError(t, err) +// require.Equal(t, render, "2") // Back to the original state +// } + +// func TestExportState(t *testing.T) { +// node, _ := testingCounterRealm(t, 3) + +// t.Run("export state", func(t *testing.T) { +// ctx := testingContext(t) +// state, err := node.ExportCurrentState(ctx) +// require.NoError(t, err) +// assert.Equal(t, 3, len(state)) +// }) + +// t.Run("export genesis doc", func(t *testing.T) { +// ctx := testingContext(t) +// doc, err := node.ExportStateAsGenesis(ctx) +// require.NoError(t, err) +// require.NotNil(t, doc.AppState) + +// state, ok := doc.AppState.(gnoland.GnoGenesisState) +// require.True(t, ok) +// assert.Equal(t, 3, len(state.Txs)) +// }) +// } + +// func testingCounterRealm(t *testing.T, inc int) (*Node, *emitter.ServerEmitter) { +// t.Helper() + +// const ( +// // foo package +// counterGnoMod = "module gno.land/r/dev/counter\n" +// counterFile = `package counter +// import "strconv" + +// var value int = 0 +// func Inc(v int) { value += v } // method to increment value +// func Render(_ string) string { return strconv.Itoa(value) } +// ` +// ) + +// // Generate package counter +// counterPkg := generateTestingPackage(t, +// "gno.mod", counterGnoMod, +// "foo.gno", counterFile) + +// // Call NewDevNode with no package should work +// node, emitter := newTestingDevNode(t, counterPkg) +// assert.Len(t, node.ListPkgs(), 1) + +// // Test rendering +// render, err := testingRenderRealm(t, node, testCounterRealm) +// require.NoError(t, err) +// require.Equal(t, render, "0") + +// // Increment the counter 10 times +// for i := 0; i < inc; i++ { +// t.Logf("call %d", i) +// // Craft `Inc` msg +// msg := vm.MsgCall{ +// PkgPath: testCounterRealm, +// Func: "Inc", +// Args: []string{"1"}, +// } + +// res, err := testingCallRealm(t, node, msg) +// require.NoError(t, err) +// require.NoError(t, res.CheckTx.Error) +// require.NoError(t, res.DeliverTx.Error) +// assert.Equal(t, events.EvtTxResult, emitter.NextEvent().Type()) +// } + +// render, err = testingRenderRealm(t, node, testCounterRealm) +// require.NoError(t, err) +// require.Equal(t, render, strconv.Itoa(inc)) + +// return node, emitter +// } + +// func testingContext(t *testing.T) context.Context { +// t.Helper() + +// ctx, cancel := context.WithTimeout(context.Background(), time.Second*7) +// t.Cleanup(cancel) +// return ctx +// } diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 3fe2c773e57..ce8d1c878ec 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -2,35 +2,21 @@ package dev import ( "context" - "encoding/json" - "os" - "path/filepath" "testing" - "time" - mock "github.com/gnolang/gno/contribs/gnodev/internal/mock" - - "github.com/gnolang/gno/contribs/gnodev/pkg/events" - "github.com/gnolang/gno/gno.land/pkg/gnoclient" - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/gno.land/pkg/integration" - "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/alecthomas/assert" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/gnovm/pkg/gnoenv" - core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/keys" - tm2events "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/jaekwon/testify/require" ) // XXX: We should probably use txtar to test this package. var nodeTestingAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") -// TestNewNode_NoPackages tests the NewDevNode method with no package. +// // TestNewNode_NoPackages tests the NewDevNode method with no package. func TestNewNode_NoPackages(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -48,26 +34,27 @@ func TestNewNode_NoPackages(t *testing.T) { require.NoError(t, node.Close()) } -// TestNewNode_WithPackage tests the NewDevNode with a single package. -func TestNewNode_WithPackage(t *testing.T) { +// // TestNewNode_WithPackage tests the NewDevNode with a single package. +func TestNewNode_WithLoader(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() const ( + path = "gno.land/r/dev/foobar" // foobar package - testGnoMod = "module gno.land/r/dev/foobar\n" - testFile = `package foobar + testFile = `package foobar func Render(_ string) string { return "foo" } ` ) // Generate package - pkgpath := generateTestingPackage(t, "gno.mod", testGnoMod, "foobar.gno", testFile) + pkg := generateMemPackage(t, path, "foobar.gno", testFile) logger := log.NewTestingLogger(t) + loader := packages.NewLoader(filter packages.FilterFunc, res ...packages.Resolver) // Call NewDevNode with no package should work cfg := DefaultNodeConfig(gnoenv.RootDir()) - cfg.PackagesModifier = []PackagePath{pkgpath} + cfg.Loader = &loader cfg.Logger = logger node, err := NewDevNode(ctx, cfg) require.NoError(t, err) @@ -81,448 +68,462 @@ func Render(_ string) string { return "foo" } require.NoError(t, node.Close()) } -func TestNodeAddPackage(t *testing.T) { - // Setup a Node instance - const ( - // foo package - fooGnoMod = "module gno.land/r/dev/foo\n" - fooFile = `package foo -func Render(_ string) string { return "foo" } -` - // bar package - barGnoMod = "module gno.land/r/dev/bar\n" - barFile = `package bar -func Render(_ string) string { return "bar" } -` - ) - - // Generate package foo - foopkg := generateTestingPackage(t, "gno.mod", fooGnoMod, "foo.gno", fooFile) - - // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, foopkg) - assert.Len(t, node.ListPkgs(), 1) - - // Test render - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") - require.NoError(t, err) - require.Equal(t, render, "foo") - - // Generate package bar - barpkg := generateTestingPackage(t, "gno.mod", barGnoMod, "bar.gno", barFile) - err = node.UpdatePackages(barpkg.Path) - require.NoError(t, err) - assert.Len(t, node.ListPkgs(), 2) - - // Render should fail as the node hasn't reloaded - render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") - require.Error(t, err) - - err = node.Reload(context.Background()) - require.NoError(t, err) - assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) - - // After a reload, render should succeed - render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") - require.NoError(t, err) - require.Equal(t, render, "bar") -} - -func TestNodeUpdatePackage(t *testing.T) { - // Setup a Node instance - const ( - // foo package - foobarGnoMod = "module gno.land/r/dev/foobar\n" - fooFile = `package foobar -func Render(_ string) string { return "foo" } -` - barFile = `package foobar -func Render(_ string) string { return "bar" } -` - ) - - // Generate package foo - foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) - - // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, foopkg) - assert.Len(t, node.ListPkgs(), 1) - - // Test that render is correct - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foobar") - require.NoError(t, err) - require.Equal(t, render, "foo") - - // Override `foo.gno` file with bar content - err = os.WriteFile(filepath.Join(foopkg.Path, "foo.gno"), []byte(barFile), 0o700) - require.NoError(t, err) - - err = node.Reload(context.Background()) - require.NoError(t, err) - - // Check reload event - assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) - - // After a reload, render should succeed - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foobar") - require.NoError(t, err) - require.Equal(t, render, "bar") - - assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) -} - -func TestNodeReset(t *testing.T) { - const ( - // foo package - foobarGnoMod = "module gno.land/r/dev/foo\n" - fooFile = `package foo -var str string = "foo" -func UpdateStr(newStr string) { str = newStr } // method to update 'str' variable -func Render(_ string) string { return str } -` - ) - - // Generate package foo - foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) - - // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, foopkg) - assert.Len(t, node.ListPkgs(), 1) - - // Test rendering - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") - require.NoError(t, err) - require.Equal(t, render, "foo") - - // Call `UpdateStr` to update `str` value with "bar" - msg := vm.MsgCall{ - PkgPath: "gno.land/r/dev/foo", - Func: "UpdateStr", - Args: []string{"bar"}, - Send: nil, - } - res, err := testingCallRealm(t, node, msg) - require.NoError(t, err) - require.NoError(t, res.CheckTx.Error) - require.NoError(t, res.DeliverTx.Error) - assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) - - // Check for correct render update - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") - require.NoError(t, err) - require.Equal(t, render, "bar") - - // Reset state - err = node.Reset(context.Background()) - require.NoError(t, err) - assert.Equal(t, emitter.NextEvent().Type(), events.EvtReset) - - // Test rendering should return initial `str` value - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") - require.NoError(t, err) - require.Equal(t, render, "foo") - - assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) -} - -func TestTxTimestampRecover(t *testing.T) { - const ( - // foo package - foobarGnoMod = "module gno.land/r/dev/foo\n" - fooFile = `package foo -import ( - "strconv" - "strings" - "time" -) - -var times = []time.Time{ - time.Now(), // Evaluate at genesis -} - -func SpanTime() { - times = append(times, time.Now()) -} - -func Render(_ string) string { - var strs strings.Builder - - strs.WriteRune('[') - for i, t := range times { - if i > 0 { - strs.WriteRune(',') - } - strs.WriteString(strconv.Itoa(int(t.UnixNano()))) - } - strs.WriteRune(']') - - return strs.String() -} -` - ) - - // Add a hard deadline of 20 seconds to avoid potential deadlock and fail early - ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) - defer cancel() - - parseJSONTimesList := func(t *testing.T, render string) []time.Time { - t.Helper() - - var times []time.Time - var nanos []int64 - - err := json.Unmarshal([]byte(render), &nanos) - require.NoError(t, err) - - for _, nano := range nanos { - sec, nsec := nano/int64(time.Second), nano%int64(time.Second) - times = append(times, time.Unix(sec, nsec)) - } - - return times - } - - // Generate package foo - foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) - - // Call NewDevNode with no package should work - cfg := createDefaultTestingNodeConfig(foopkg) - - // XXX(gfanton): Setting this to `false` somehow makes the time block - // drift from the time spanned by the VM. - cfg.TMConfig.Consensus.SkipTimeoutCommit = false - cfg.TMConfig.Consensus.TimeoutCommit = 500 * time.Millisecond - cfg.TMConfig.Consensus.TimeoutPropose = 100 * time.Millisecond - cfg.TMConfig.Consensus.CreateEmptyBlocks = true - - node, emitter := newTestingDevNodeWithConfig(t, cfg) - - // We need to make sure that blocks are separated by at least 1 second - // (minimal time between blocks). We can ensure this by listening for - // new blocks and comparing timestamps - cc := make(chan types.EventNewBlock) - node.Node.EventSwitch().AddListener("test-timestamp", func(evt tm2events.Event) { - newBlock, ok := evt.(types.EventNewBlock) - if !ok { - return - } - - select { - case cc <- newBlock: - default: - } - }) - - // wait for first block for reference - var refHeight, refTimestamp int64 - - select { - case <-ctx.Done(): - require.FailNow(t, ctx.Err().Error()) - case res := <-cc: - refTimestamp = res.Block.Time.Unix() - refHeight = res.Block.Height - } - - // number of span to process - const nevents = 3 - - // Span multiple time - for i := 0; i < nevents; i++ { - t.Logf("waiting for a bock greater than height(%d) and unix(%d)", refHeight, refTimestamp) - for { - var block types.EventNewBlock - select { - case <-ctx.Done(): - require.FailNow(t, ctx.Err().Error()) - case block = <-cc: - } - - t.Logf("got a block height(%d) and unix(%d)", - block.Block.Height, block.Block.Time.Unix()) - - // Ensure we consume every block before tx block - if refHeight >= block.Block.Height { - continue - } - - // Ensure new block timestamp is before previous reference timestamp - if newRefTimestamp := block.Block.Time.Unix(); newRefTimestamp > refTimestamp { - refTimestamp = newRefTimestamp - break // break the loop - } - } - - t.Logf("found a valid block(%d)! continue", refHeight) - - // Span a new time - msg := vm.MsgCall{ - PkgPath: "gno.land/r/dev/foo", - Func: "SpanTime", - } - - res, err := testingCallRealm(t, node, msg) - - require.NoError(t, err) - require.NoError(t, res.CheckTx.Error) - require.NoError(t, res.DeliverTx.Error) - assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) - - // Set the new height from the tx as reference - refHeight = res.Height - } - - // Render JSON times list - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") - require.NoError(t, err) - - // Parse times list - timesList1 := parseJSONTimesList(t, render) - t.Logf("list of times: %+v", timesList1) - - // Ensure times are correctly expending. - for i, t2 := range timesList1 { - if i == 0 { - continue - } - - t1 := timesList1[i-1] - require.Greater(t, t2.UnixNano(), t1.UnixNano()) - } - - // Reload the node - err = node.Reload(context.Background()) - require.NoError(t, err) - assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) - - // Fetch time list again from render - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") - require.NoError(t, err) - - timesList2 := parseJSONTimesList(t, render) - - // Times list should be identical from the orignal list - require.Len(t, timesList2, len(timesList1)) - for i := 0; i < len(timesList1); i++ { - t1nsec, t2nsec := timesList1[i].UnixNano(), timesList2[i].UnixNano() - assert.Equal(t, t1nsec, t2nsec, - "comparing times1[%d](%d) == times2[%d](%d)", i, t1nsec, i, t2nsec) - } -} - -func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error) { - t.Helper() - - signer := newInMemorySigner(t, node.Config().ChainID()) - cli := gnoclient.Client{ - Signer: signer, - RPCClient: node.Client(), - } - - render, res, err := cli.Render(rlmpath, "") - if err == nil { - err = res.Response.Error - } - - return render, err -} - -func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) { - t.Helper() - - signer := newInMemorySigner(t, node.Config().ChainID()) - cli := gnoclient.Client{ - Signer: signer, - RPCClient: node.Client(), - } - - txcfg := gnoclient.BaseTxCfg{ - GasFee: ugnot.ValueString(1000000), // Gas fee - GasWanted: 2_000_000, // Gas wanted - } - - // Set Caller in the msgs - caller, err := signer.Info() - require.NoError(t, err) - vmMsgs := make([]vm.MsgCall, 0, len(msgs)) - for _, msg := range msgs { - vmMsgs = append(vmMsgs, vm.NewMsgCall(caller.GetAddress(), msg.Send, msg.PkgPath, msg.Func, msg.Args)) - } - - return cli.Call(txcfg, vmMsgs...) -} - -func generateTestingPackage(t *testing.T, nameFile ...string) PackagePath { - t.Helper() - workdir := t.TempDir() - - if len(nameFile)%2 != 0 { - require.FailNow(t, "Generate testing packages require paired arguments.") - } - - for i := 0; i < len(nameFile); i += 2 { - name := nameFile[i] - content := nameFile[i+1] - - err := os.WriteFile(filepath.Join(workdir, name), []byte(content), 0o700) - require.NoError(t, err) - } - - return PackagePath{ - Path: workdir, - Creator: nodeTestingAddress, - } -} - -func createDefaultTestingNodeConfig(pkgslist ...PackagePath) *NodeConfig { - cfg := DefaultNodeConfig(gnoenv.RootDir()) - cfg.PackagesModifier = pkgslist - return cfg -} - -func newTestingDevNode(t *testing.T, pkgslist ...PackagePath) (*Node, *mock.ServerEmitter) { - t.Helper() - - cfg := createDefaultTestingNodeConfig(pkgslist...) - return newTestingDevNodeWithConfig(t, cfg) -} - -func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.ServerEmitter) { - t.Helper() - - ctx, cancel := context.WithCancel(context.Background()) - logger := log.NewTestingLogger(t) - emitter := &mock.ServerEmitter{} - - cfg.Emitter = emitter - cfg.Logger = logger - - node, err := NewDevNode(ctx, cfg) - require.NoError(t, err) - assert.Len(t, node.ListPkgs(), len(cfg.PackagesModifier)) - - t.Cleanup(func() { - node.Close() - cancel() - }) - - return node, emitter -} - -func newInMemorySigner(t *testing.T, chainid string) *gnoclient.SignerFromKeybase { - t.Helper() - - mnemonic := integration.DefaultAccount_Seed - name := integration.DefaultAccount_Name - - kb := keys.NewInMemory() - _, err := kb.CreateAccount(name, mnemonic, "", "", uint32(0), uint32(0)) - require.NoError(t, err) - - return &gnoclient.SignerFromKeybase{ - Keybase: kb, // Stores keys in memory - Account: name, // Account name - Password: "", // Password for encryption - ChainID: chainid, // Chain ID for transaction signing - } -} +// // func TestNodeAddPackage(t *testing.T) { +// // // Setup a Node instance +// // const ( +// // // foo package +// // fooPath = "gno.land/r/dev/foo" +// // fooFile = `package foo +// // func Render(_ string) string { return "foo" } +// // ` +// // // bar package +// // barPath = "gno.land/r/dev/bar" +// // barFile = `package bar +// // func Render(_ string) string { return "bar" } +// // ` +// // ) + +// // resolver := packages.NewMockResolver() + +// // // Generate package foo +// // fooPkg := generateMemPackage(t, fooPath, "foo.gno", fooFile) +// // resolver.AddPackage(fooPkg) + +// // // Call NewDevNode with no package should work +// // node, emitter := newTestingDevNodeWithConfig(t, cfg) +// // assert.Len(t, node.ListPkgs(), 1) + +// // // Test render +// // render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") +// // require.NoError(t, err) +// // require.Equal(t, render, "foo") + +// // // Generate package bar +// // barPkg := generateMemPackage(t, barPath, "bar.gno", barFile) +// // resolver.AddPackage(barPkg) + +// // // Render should fail as the node hasn't reloaded +// // render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") +// // require.Error(t, err) + +// // err = node.Reload(context.Background()) +// // require.NoError(t, err) +// // assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) + +// // // After a reload, render should succeed +// // render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") +// // require.NoError(t, err) +// // require.Equal(t, render, "bar") +// // } + +// func TestNodeUpdatePackage(t *testing.T) { +// // Setup a Node instance +// const ( +// // foo package +// foobarGnoMod = "module gno.land/r/dev/foobar\n" +// fooFile = `package foobar +// func Render(_ string) string { return "foo" } +// ` +// barFile = `package foobar +// func Render(_ string) string { return "bar" } +// ` +// ) + +// // Generate package foo +// foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) + +// // Call NewDevNode with no package should work +// node, emitter := newTestingDevNode(t, foopkg) +// assert.Len(t, node.ListPkgs(), 1) + +// // Test that render is correct +// render, err := testingRenderRealm(t, node, "gno.land/r/dev/foobar") +// require.NoError(t, err) +// require.Equal(t, render, "foo") + +// // Override `foo.gno` file with bar content +// err = os.WriteFile(filepath.Join(foopkg.Path, "foo.gno"), []byte(barFile), 0o700) +// require.NoError(t, err) + +// err = node.Reload(context.Background()) +// require.NoError(t, err) + +// // Check reload event +// assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) + +// // After a reload, render should succeed +// render, err = testingRenderRealm(t, node, "gno.land/r/dev/foobar") +// require.NoError(t, err) +// require.Equal(t, render, "bar") + +// assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) +// } + +// func TestNodeReset(t *testing.T) { +// const ( +// // foo package +// foobarGnoMod = "module gno.land/r/dev/foo\n" +// fooFile = `package foo +// var str string = "foo" +// func UpdateStr(newStr string) { str = newStr } // method to update 'str' variable +// func Render(_ string) string { return str } +// ` +// ) + +// // Generate package foo +// foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) + +// // Call NewDevNode with no package should work +// node, emitter := newTestingDevNode(t, foopkg) +// assert.Len(t, node.ListPkgs(), 1) + +// // Test rendering +// render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") +// require.NoError(t, err) +// require.Equal(t, render, "foo") + +// // Call `UpdateStr` to update `str` value with "bar" +// msg := vm.MsgCall{ +// PkgPath: "gno.land/r/dev/foo", +// Func: "UpdateStr", +// Args: []string{"bar"}, +// Send: nil, +// } +// res, err := testingCallRealm(t, node, msg) +// require.NoError(t, err) +// require.NoError(t, res.CheckTx.Error) +// require.NoError(t, res.DeliverTx.Error) +// assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) + +// // Check for correct render update +// render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") +// require.NoError(t, err) +// require.Equal(t, render, "bar") + +// // Reset state +// err = node.Reset(context.Background()) +// require.NoError(t, err) +// assert.Equal(t, emitter.NextEvent().Type(), events.EvtReset) + +// // Test rendering should return initial `str` value +// render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") +// require.NoError(t, err) +// require.Equal(t, render, "foo") + +// assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) +// } + +// func TestTxTimestampRecover(t *testing.T) { +// const ( +// // foo package +// foobarGnoMod = "module gno.land/r/dev/foo\n" +// fooFile = `package foo +// import ( +// "strconv" +// "strings" +// "time" +// ) + +// var times = []time.Time{ +// time.Now(), // Evaluate at genesis +// } + +// func SpanTime() { +// times = append(times, time.Now()) +// } + +// func Render(_ string) string { +// var strs strings.Builder + +// strs.WriteRune('[') +// for i, t := range times { +// if i > 0 { +// strs.WriteRune(',') +// } +// strs.WriteString(strconv.Itoa(int(t.UnixNano()))) +// } +// strs.WriteRune(']') + +// return strs.String() +// } +// ` +// ) + +// // Add a hard deadline of 20 seconds to avoid potential deadlock and fail early +// ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) +// defer cancel() + +// parseJSONTimesList := func(t *testing.T, render string) []time.Time { +// t.Helper() + +// var times []time.Time +// var nanos []int64 + +// err := json.Unmarshal([]byte(render), &nanos) +// require.NoError(t, err) + +// for _, nano := range nanos { +// sec, nsec := nano/int64(time.Second), nano%int64(time.Second) +// times = append(times, time.Unix(sec, nsec)) +// } + +// return times +// } + +// // Generate package foo +// foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) + +// // Call NewDevNode with no package should work +// cfg := createDefaultTestingNodeConfig(foopkg) + +// // XXX(gfanton): Setting this to `false` somehow makes the time block +// // drift from the time spanned by the VM. +// cfg.TMConfig.Consensus.SkipTimeoutCommit = false +// cfg.TMConfig.Consensus.TimeoutCommit = 500 * time.Millisecond +// cfg.TMConfig.Consensus.TimeoutPropose = 100 * time.Millisecond +// cfg.TMConfig.Consensus.CreateEmptyBlocks = true + +// node, emitter := newTestingDevNodeWithConfig(t, cfg) + +// // We need to make sure that blocks are separated by at least 1 second +// // (minimal time between blocks). We can ensure this by listening for +// // new blocks and comparing timestamps +// cc := make(chan types.EventNewBlock) +// node.Node.EventSwitch().AddListener("test-timestamp", func(evt tm2events.Event) { +// newBlock, ok := evt.(types.EventNewBlock) +// if !ok { +// return +// } + +// select { +// case cc <- newBlock: +// default: +// } +// }) + +// // wait for first block for reference +// var refHeight, refTimestamp int64 + +// select { +// case <-ctx.Done(): +// require.FailNow(t, ctx.Err().Error()) +// case res := <-cc: +// refTimestamp = res.Block.Time.Unix() +// refHeight = res.Block.Height +// } + +// // number of span to process +// const nevents = 3 + +// // Span multiple time +// for i := 0; i < nevents; i++ { +// t.Logf("waiting for a bock greater than height(%d) and unix(%d)", refHeight, refTimestamp) +// for { +// var block types.EventNewBlock +// select { +// case <-ctx.Done(): +// require.FailNow(t, ctx.Err().Error()) +// case block = <-cc: +// } + +// t.Logf("got a block height(%d) and unix(%d)", +// block.Block.Height, block.Block.Time.Unix()) + +// // Ensure we consume every block before tx block +// if refHeight >= block.Block.Height { +// continue +// } + +// // Ensure new block timestamp is before previous reference timestamp +// if newRefTimestamp := block.Block.Time.Unix(); newRefTimestamp > refTimestamp { +// refTimestamp = newRefTimestamp +// break // break the loop +// } +// } + +// t.Logf("found a valid block(%d)! continue", refHeight) + +// // Span a new time +// msg := vm.MsgCall{ +// PkgPath: "gno.land/r/dev/foo", +// Func: "SpanTime", +// } + +// res, err := testingCallRealm(t, node, msg) + +// require.NoError(t, err) +// require.NoError(t, res.CheckTx.Error) +// require.NoError(t, res.DeliverTx.Error) +// assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) + +// // Set the new height from the tx as reference +// refHeight = res.Height +// } + +// // Render JSON times list +// render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") +// require.NoError(t, err) + +// // Parse times list +// timesList1 := parseJSONTimesList(t, render) +// t.Logf("list of times: %+v", timesList1) + +// // Ensure times are correctly expending. +// for i, t2 := range timesList1 { +// if i == 0 { +// continue +// } + +// t1 := timesList1[i-1] +// require.Greater(t, t2.UnixNano(), t1.UnixNano()) +// } + +// // Reload the node +// err = node.Reload(context.Background()) +// require.NoError(t, err) +// assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) + +// // Fetch time list again from render +// render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") +// require.NoError(t, err) + +// timesList2 := parseJSONTimesList(t, render) + +// // Times list should be identical from the orignal list +// require.Len(t, timesList2, len(timesList1)) +// for i := 0; i < len(timesList1); i++ { +// t1nsec, t2nsec := timesList1[i].UnixNano(), timesList2[i].UnixNano() +// assert.Equal(t, t1nsec, t2nsec, +// "comparing times1[%d](%d) == times2[%d](%d)", i, t1nsec, i, t2nsec) +// } +// } + +// func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error) { +// t.Helper() + +// signer := newInMemorySigner(t, node.Config().ChainID()) +// cli := gnoclient.Client{ +// Signer: signer, +// RPCClient: node.Client(), +// } + +// render, res, err := cli.Render(rlmpath, "") +// if err == nil { +// err = res.Response.Error +// } + +// return render, err +// } + +// func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) { +// t.Helper() + +// signer := newInMemorySigner(t, node.Config().ChainID()) +// cli := gnoclient.Client{ +// Signer: signer, +// RPCClient: node.Client(), +// } + +// txcfg := gnoclient.BaseTxCfg{ +// GasFee: ugnot.ValueString(1000000), // Gas fee +// GasWanted: 2_000_000, // Gas wanted +// } + +// // Set Caller in the msgs +// caller, err := signer.Info() +// require.NoError(t, err) +// vmMsgs := make([]vm.MsgCall, 0, len(msgs)) +// for _, msg := range msgs { +// vmMsgs = append(vmMsgs, vm.NewMsgCall(caller.GetAddress(), msg.Send, msg.PkgPath, msg.Func, msg.Args)) +// } + +// return cli.Call(txcfg, vmMsgs...) +// } + +// func generateMemPackage(t *testing.T, path string, pairNameFile ...string) gnovm.MemPackage { +// t.Helper() + +// if len(pairNameFile)%2 != 0 { +// require.FailNow(t, "Generate testing packages require paired arguments.") +// } + +// // Guess the name based on dir +// // We don't bother parsing files to actually guess the name of the package +// name := filepath.Dir(path) + +// files := make([]*gnovm.MemFile, 0, len(pairNameFile)/2) +// for i := 0; i < len(pairNameFile); i += 2 { +// name := pairNameFile[i] +// content := pairNameFile[i+1] +// files = append(files, &gnovm.MemFile{ +// Name: name, +// Body: content, +// }) +// } + +// return gnovm.MemPackage{ +// Name: name, +// Path: path, +// Files: files, +// } +// } + +// func createDefaultTestingNodeConfig(pkgs ...gnovm.MemPackage) *NodeConfig { +// var loader packages.Loader + +// for _, pkg := range pkgs { +// loader.Paths = append(loader.Paths, pkg.Path) +// } +// loader.Resolver = packages.NewMockResolver(pkgs...) + +// cfg := DefaultNodeConfig(gnoenv.RootDir()) +// cfg.Loader = &loader +// return cfg +// } + +// func newTestingDevNode(t *testing.T, pkgs ...gnovm.MemPackage) (*Node, *mock.ServerEmitter) { +// t.Helper() + +// cfg := createDefaultTestingNodeConfig(pkgs...) +// return newTestingDevNodeWithConfig(t, cfg) +// } + +// func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.ServerEmitter) { +// t.Helper() + +// ctx, cancel := context.WithCancel(context.Background()) +// logger := log.NewTestingLogger(t) +// emitter := &mock.ServerEmitter{} + +// cfg.Emitter = emitter +// cfg.Logger = logger + +// node, err := NewDevNode(ctx, cfg) +// require.NoError(t, err) +// assert.Len(t, node.ListPkgs(), len(cfg.PackagesModifier)) + +// t.Cleanup(func() { +// node.Close() +// cancel() +// }) + +// return node, emitter +// } + +// func newInMemorySigner(t *testing.T, chainid string) *gnoclient.SignerFromKeybase { +// t.Helper() + +// mnemonic := integration.DefaultAccount_Seed +// name := integration.DefaultAccount_Name + +// kb := keys.NewInMemory() +// _, err := kb.CreateAccount(name, mnemonic, "", "", uint32(0), uint32(0)) +// require.NoError(t, err) + +// return &gnoclient.SignerFromKeybase{ +// Keybase: kb, // Stores keys in memory +// Account: name, // Account name +// Password: "", // Password for encryption +// ChainID: chainid, // Chain ID for transaction signing +// } +// } diff --git a/contribs/gnodev/pkg/dev/packages_test.go b/contribs/gnodev/pkg/dev/packages_test.go index 3410c2c66fb..f6ccfda877f 100644 --- a/contribs/gnodev/pkg/dev/packages_test.go +++ b/contribs/gnodev/pkg/dev/packages_test.go @@ -1,103 +1,104 @@ package dev -import ( - "testing" - "github.com/gnolang/gno/contribs/gnodev/pkg/address" - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) +// import ( +// "testing" -func TestResolvePackagePathQuery(t *testing.T) { - t.Parallel() +// "github.com/gnolang/gno/contribs/gnodev/pkg/address" +// "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" +// "github.com/gnolang/gno/tm2/pkg/crypto" +// "github.com/gnolang/gno/tm2/pkg/std" +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/require" +// ) - var ( - testingName = "testAccount" - testingAddress = crypto.MustAddressFromString("g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na") - ) +// func TestResolvePackagePathQuery(t *testing.T) { +// t.Parallel() - book := address.NewBook() - book.Add(testingAddress, testingName) +// var ( +// testingName = "testAccount" +// testingAddress = crypto.MustAddressFromString("g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na") +// ) - cases := []struct { - Path string - ExpectedPackagePath PackagePath - ShouldFail bool - }{ - { - Path: ".", - ExpectedPackagePath: PackagePath{ - Path: ".", - }, - }, - { - Path: "/simple/path", - ExpectedPackagePath: PackagePath{ - Path: "/simple/path", - }, - }, - { - Path: "/ambiguo/u//s/path///", - ExpectedPackagePath: PackagePath{ - Path: "/ambiguo/u/s/path", - }, - }, - { - Path: "/path/with/creator?creator=testAccount", - ExpectedPackagePath: PackagePath{ - Path: "/path/with/creator", - Creator: testingAddress, - }, - }, - { - Path: "/path/with/deposit?deposit=" + ugnot.ValueString(100), - ExpectedPackagePath: PackagePath{ - Path: "/path/with/deposit", - Deposit: std.MustParseCoins(ugnot.ValueString(100)), - }, - }, - { - Path: ".?creator=g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na&deposit=" + ugnot.ValueString(100), - ExpectedPackagePath: PackagePath{ - Path: ".", - Creator: testingAddress, - Deposit: std.MustParseCoins(ugnot.ValueString(100)), - }, - }, +// book := address.NewBook() +// book.Add(testingAddress, testingName) - // errors cases - { - Path: "/invalid/account?creator=UnknownAccount", - ShouldFail: true, - }, - { - Path: "/invalid/address?creator=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", - ShouldFail: true, - }, - { - Path: "/invalid/deposit?deposit=abcd", - ShouldFail: true, - }, - } +// cases := []struct { +// Path string +// ExpectedPackagePath PackagePath +// ShouldFail bool +// }{ +// { +// Path: ".", +// ExpectedPackagePath: PackagePath{ +// Path: ".", +// }, +// }, +// { +// Path: "/simple/path", +// ExpectedPackagePath: PackagePath{ +// Path: "/simple/path", +// }, +// }, +// { +// Path: "/ambiguo/u//s/path///", +// ExpectedPackagePath: PackagePath{ +// Path: "/ambiguo/u/s/path", +// }, +// }, +// { +// Path: "/path/with/creator?creator=testAccount", +// ExpectedPackagePath: PackagePath{ +// Path: "/path/with/creator", +// Creator: testingAddress, +// }, +// }, +// { +// Path: "/path/with/deposit?deposit=" + ugnot.ValueString(100), +// ExpectedPackagePath: PackagePath{ +// Path: "/path/with/deposit", +// Deposit: std.MustParseCoins(ugnot.ValueString(100)), +// }, +// }, +// { +// Path: ".?creator=g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na&deposit=" + ugnot.ValueString(100), +// ExpectedPackagePath: PackagePath{ +// Path: ".", +// Creator: testingAddress, +// Deposit: std.MustParseCoins(ugnot.ValueString(100)), +// }, +// }, - for _, tc := range cases { - tc := tc - t.Run(tc.Path, func(t *testing.T) { - t.Parallel() +// // errors cases +// { +// Path: "/invalid/account?creator=UnknownAccount", +// ShouldFail: true, +// }, +// { +// Path: "/invalid/address?creator=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", +// ShouldFail: true, +// }, +// { +// Path: "/invalid/deposit?deposit=abcd", +// ShouldFail: true, +// }, +// } - result, err := ResolvePackageModifierQuery(book, tc.Path) - if tc.ShouldFail { - assert.Error(t, err) - return - } - require.NoError(t, err) +// for _, tc := range cases { +// tc := tc +// t.Run(tc.Path, func(t *testing.T) { +// t.Parallel() - assert.Equal(t, tc.ExpectedPackagePath.Path, result.Path) - assert.Equal(t, tc.ExpectedPackagePath.Creator, result.Creator) - assert.Equal(t, tc.ExpectedPackagePath.Deposit.String(), result.Deposit.String()) - }) - } -} +// result, err := ResolvePackageModifierQuery(book, tc.Path) +// if tc.ShouldFail { +// assert.Error(t, err) +// return +// } +// require.NoError(t, err) + +// assert.Equal(t, tc.ExpectedPackagePath.Path, result.Path) +// assert.Equal(t, tc.ExpectedPackagePath.Creator, result.Creator) +// assert.Equal(t, tc.ExpectedPackagePath.Deposit.String(), result.Deposit.String()) +// }) +// } +// } diff --git a/contribs/gnodev/pkg/packages/loader.go b/contribs/gnodev/pkg/packages/loader.go index ecc90949670..6d110d0fd80 100644 --- a/contribs/gnodev/pkg/packages/loader.go +++ b/contribs/gnodev/pkg/packages/loader.go @@ -5,18 +5,40 @@ import ( "fmt" "go/parser" "go/token" - "strings" ) var ErrNoResolvers = errors.New("no resolvers setup") +type FilterFunc func(path string) bool + +func NoopFilterFunc(path string) bool { return false } +func FilterAllFunc(path string) bool { return true } + type Loader struct { - Host string - Paths []string - Resolver Resolver + Resolver + FilterFunc +} + +func NewLoader(res ...Resolver) *Loader { + loader := Loader{FilterFunc: NoopFilterFunc} + switch len(res) { + case 0: // Skip + case 1: + loader.Resolver = res[0] + default: + loader.Resolver = ChainResolvers(res...) + } + + return &loader +} + +func NewLoaderWithFilter(filter FilterFunc, res ...Resolver) *Loader { + loader := NewLoader(res...) + loader.FilterFunc = filter + return loader } -func (l Loader) LoadPackages() ([]Package, error) { +func (l Loader) Load(paths ...string) ([]Package, error) { if l.Resolver == nil { return nil, ErrNoResolvers } @@ -24,7 +46,7 @@ func (l Loader) LoadPackages() ([]Package, error) { fset := token.NewFileSet() visited, stack := map[string]bool{}, map[string]bool{} pkgs := make([]Package, 0) - for _, root := range l.Paths { + for _, root := range paths { deps, err := l.loadPackage(root, fset, l.Resolver, visited, stack) if err != nil { return nil, err @@ -45,8 +67,8 @@ func (l Loader) loadPackage(path string, fset *token.FileSet, resolver Resolver, visited[path] = true - // do not try to load package that hasn't been prefixed - if l.Host != "" && !strings.HasPrefix(path, l.Host) { + // Apply filter func if any + if l.FilterFunc != nil && l.FilterFunc(path) { return nil, nil } diff --git a/contribs/gnodev/pkg/packages/package.go b/contribs/gnodev/pkg/packages/package.go new file mode 100644 index 00000000000..96fd07cf11b --- /dev/null +++ b/contribs/gnodev/pkg/packages/package.go @@ -0,0 +1,81 @@ +package packages + +import ( + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" + + "github.com/gnolang/gno/gnovm" +) + +type PackageKind int + +const ( + PackageKindOther = iota + PackageKindRemote = iota + PackageKindFS +) + +type Package struct { + gnovm.MemPackage + Kind PackageKind + Location string +} + +func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) { + files, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("unable to read dir %q: %w", dir, err) + } + + var name string + memFiles := []*gnovm.MemFile{} + for _, file := range files { + fname := file.Name() + if !isGnoFile(fname) || isTestFile(fname) { + continue + } + + filepath := filepath.Join(dir, fname) + body, err := os.ReadFile(filepath) + if err != nil { + return nil, fmt.Errorf("unable to read file %q: %w", filepath, err) + } + + memfile, pkgname, err := parseFile(fset, fname, body) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) + } + + if name != "" && name != pkgname { + return nil, fmt.Errorf("conflict package name between %q and %q", name, memfile.Name) + } + + name = pkgname + memFiles = append(memFiles, memfile) + } + + return &Package{ + MemPackage: gnovm.MemPackage{ + Name: name, + Path: path, + Files: memFiles, + }, + Location: dir, + Kind: PackageKindFS, + }, nil +} + +func parseFile(fset *token.FileSet, fname string, body []byte) (*gnovm.MemFile, string, error) { + f, err := parser.ParseFile(fset, fname, body, parser.PackageClauseOnly) + if err != nil { + return nil, "", fmt.Errorf("unable to parse file %q: %w", fname, err) + } + + return &gnovm.MemFile{ + Name: fname, + Body: string(body), + }, f.Name.Name, nil +} diff --git a/contribs/gnodev/pkg/packages/resolver.go b/contribs/gnodev/pkg/packages/resolver.go index f6d81437892..95d64e3523d 100644 --- a/contribs/gnodev/pkg/packages/resolver.go +++ b/contribs/gnodev/pkg/packages/resolver.go @@ -9,26 +9,10 @@ import ( "log/slog" "strings" "time" - - "github.com/gnolang/gno/gnovm" ) var ErrResolverPackageNotFound = errors.New("package not found") -type PackageKind int - -const ( - PackageKindOther = iota - PackageKindRemote = iota - PackageKindFS -) - -type Package struct { - gnovm.MemPackage - Kind PackageKind - Location string -} - type Resolver interface { Name() string Resolve(fset *token.FileSet, path string) (*Package, error) diff --git a/contribs/gnodev/pkg/packages/resolver_local.go b/contribs/gnodev/pkg/packages/resolver_local.go index bfd8d764064..867ff6154ba 100644 --- a/contribs/gnodev/pkg/packages/resolver_local.go +++ b/contribs/gnodev/pkg/packages/resolver_local.go @@ -5,8 +5,6 @@ import ( "go/token" "path/filepath" "strings" - - "github.com/gnolang/gno/gnovm/pkg/gnomod" ) type LocalResolver struct { @@ -25,29 +23,6 @@ func NewLocalResolver(path, dir string) *LocalResolver { } } -func GuessLocalResolverFromRoots(dir string, roots []string) (res Resolver, path string) { - for _, root := range roots { - if !strings.HasPrefix(dir, root) { - continue - } - - path = strings.TrimPrefix(dir, root) - return NewLocalResolver(path, dir), path - } - - return nil, "" -} - -func GuessLocalResolverGnoMod(dir string) (res Resolver, path string) { - modfile, err := gnomod.ParseAt(dir) - if err != nil { - return nil, "" - } - - path = modfile.Module.Mod.Path - return NewLocalResolver(path, dir), path -} - func (lr LocalResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { after, found := strings.CutPrefix(path, lr.Path) if !found { diff --git a/contribs/gnodev/pkg/packages/resolver_mock.go b/contribs/gnodev/pkg/packages/resolver_mock.go new file mode 100644 index 00000000000..239a41f3f18 --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_mock.go @@ -0,0 +1,38 @@ +package packages + +import ( + "go/token" + + "github.com/gnolang/gno/gnovm" +) + +type MockResolver struct { + pkgs map[string]gnovm.MemPackage +} + +func NewMockResolver(pkgs ...gnovm.MemPackage) *MockResolver { + mappkgs := make(map[string]gnovm.MemPackage, len(pkgs)) + for _, pkg := range pkgs { + mappkgs[pkg.Path] = pkg + } + + return &MockResolver{ + pkgs: mappkgs, + } +} + +func (m *MockResolver) Name() string { + return "mock" +} + +func (m *MockResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + if mempkg, ok := m.pkgs[path]; ok { + return &Package{ + MemPackage: mempkg, + Kind: PackageKindOther, + Location: "", + }, nil + } + + return nil, ErrResolverPackageNotFound +} diff --git a/contribs/gnodev/pkg/packages/utils.go b/contribs/gnodev/pkg/packages/utils.go index 24cc85c342c..93160a3a1a5 100644 --- a/contribs/gnodev/pkg/packages/utils.go +++ b/contribs/gnodev/pkg/packages/utils.go @@ -1,14 +1,8 @@ package packages import ( - "fmt" - "go/parser" - "go/token" - "os" "path/filepath" "strings" - - "github.com/gnolang/gno/gnovm" ) func isGnoFile(name string) bool { @@ -18,59 +12,3 @@ func isGnoFile(name string) bool { func isTestFile(name string) bool { return strings.HasSuffix(name, "_filetest.gno") || strings.HasSuffix(name, "_test.gno") } - -func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) { - files, err := os.ReadDir(dir) - if err != nil { - return nil, fmt.Errorf("unable to read dir %q: %w", dir, err) - } - - var name string - memFiles := []*gnovm.MemFile{} - for _, file := range files { - fname := file.Name() - if !isGnoFile(fname) || isTestFile(fname) { - continue - } - - filepath := filepath.Join(dir, fname) - body, err := os.ReadFile(filepath) - if err != nil { - return nil, fmt.Errorf("unable to read file %q: %w", filepath, err) - } - - memfile, pkgname, err := parseFile(fset, fname, body) - if err != nil { - return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) - } - - if name != "" && name != pkgname { - return nil, fmt.Errorf("conflict package name between %q and %q", name, memfile.Name) - } - - name = pkgname - memFiles = append(memFiles, memfile) - } - - return &Package{ - MemPackage: gnovm.MemPackage{ - Name: name, - Path: path, - Files: memFiles, - }, - Location: dir, - Kind: PackageKindFS, - }, nil -} - -func parseFile(fset *token.FileSet, fname string, body []byte) (*gnovm.MemFile, string, error) { - f, err := parser.ParseFile(fset, fname, body, parser.PackageClauseOnly) - if err != nil { - return nil, "", fmt.Errorf("unable to parse file %q: %w", fname, err) - } - - return &gnovm.MemFile{ - Name: fname, - Body: string(body), - }, f.Name.Name, nil -} From 3f92248b172cb451a85fb066a273b2e3fc474440 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 3 Dec 2024 10:24:23 +0100 Subject: [PATCH 05/24] wip: test Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/pkg/dev/node.go | 1 + contribs/gnodev/pkg/dev/node_test.go | 396 +++++++++--------- contribs/gnodev/pkg/packages/resolver_mock.go | 8 +- 3 files changed, 206 insertions(+), 199 deletions(-) diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 603284c1cf1..875e929ca51 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -238,6 +238,7 @@ func (n *Node) Reset(ctx context.Context) error { return fmt.Errorf("unable to initialize a new node: %w", err) } + n.pkgs = pkgs n.loadedPackages = len(pkgsTxs) n.currentStateIndex = len(n.initialState) n.startTime = startTime diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index ce8d1c878ec..161044881e0 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -2,14 +2,24 @@ package dev import ( "context" + "path/filepath" "testing" - "github.com/alecthomas/assert" + mock "github.com/gnolang/gno/contribs/gnodev/internal/mock" + "github.com/gnolang/gno/contribs/gnodev/pkg/events" "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/gno.land/pkg/gnoclient" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" + core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/log" - "github.com/jaekwon/testify/require" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // XXX: We should probably use txtar to test this package. @@ -51,113 +61,109 @@ func Render(_ string) string { return "foo" } pkg := generateMemPackage(t, path, "foobar.gno", testFile) logger := log.NewTestingLogger(t) - loader := packages.NewLoader(filter packages.FilterFunc, res ...packages.Resolver) - // Call NewDevNode with no package should work cfg := DefaultNodeConfig(gnoenv.RootDir()) - cfg.Loader = &loader + cfg.Loader = packages.NewLoader(packages.NewMockResolver(&pkg)) cfg.Logger = logger - node, err := NewDevNode(ctx, cfg) + + node, err := NewDevNode(ctx, cfg, path) require.NoError(t, err) assert.Len(t, node.ListPkgs(), 1) // Test rendering - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foobar") + render, err := testingRenderRealm(t, node, path) require.NoError(t, err) assert.Equal(t, render, "foo") require.NoError(t, node.Close()) } -// // func TestNodeAddPackage(t *testing.T) { -// // // Setup a Node instance -// // const ( -// // // foo package -// // fooPath = "gno.land/r/dev/foo" -// // fooFile = `package foo -// // func Render(_ string) string { return "foo" } -// // ` -// // // bar package -// // barPath = "gno.land/r/dev/bar" -// // barFile = `package bar -// // func Render(_ string) string { return "bar" } -// // ` -// // ) - -// // resolver := packages.NewMockResolver() - -// // // Generate package foo -// // fooPkg := generateMemPackage(t, fooPath, "foo.gno", fooFile) -// // resolver.AddPackage(fooPkg) - -// // // Call NewDevNode with no package should work -// // node, emitter := newTestingDevNodeWithConfig(t, cfg) -// // assert.Len(t, node.ListPkgs(), 1) - -// // // Test render -// // render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") -// // require.NoError(t, err) -// // require.Equal(t, render, "foo") - -// // // Generate package bar -// // barPkg := generateMemPackage(t, barPath, "bar.gno", barFile) -// // resolver.AddPackage(barPkg) - -// // // Render should fail as the node hasn't reloaded -// // render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") -// // require.Error(t, err) - -// // err = node.Reload(context.Background()) -// // require.NoError(t, err) -// // assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) - -// // // After a reload, render should succeed -// // render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") -// // require.NoError(t, err) -// // require.Equal(t, render, "bar") -// // } - -// func TestNodeUpdatePackage(t *testing.T) { -// // Setup a Node instance -// const ( -// // foo package -// foobarGnoMod = "module gno.land/r/dev/foobar\n" -// fooFile = `package foobar -// func Render(_ string) string { return "foo" } -// ` -// barFile = `package foobar -// func Render(_ string) string { return "bar" } -// ` -// ) +func TestNodeAddPackage(t *testing.T) { + // Setup a Node instance + const ( + // foo package + fooPath = "gno.land/r/dev/foo" + fooFile = `package foo +func Render(_ string) string { return "foo" } +` + // bar package + barPath = "gno.land/r/dev/bar" + barFile = `package bar +func Render(_ string) string { return "bar" } +` + ) -// // Generate package foo -// foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) + // Generate package foo + fooPkg := generateMemPackage(t, fooPath, "foo.gno", fooFile) + barPkg := generateMemPackage(t, barPath, "bar.gno", barFile) + cfg := newTestingNodeConfig(&fooPkg, &barPkg) -// // Call NewDevNode with no package should work -// node, emitter := newTestingDevNode(t, foopkg) -// assert.Len(t, node.ListPkgs(), 1) + // Call NewDevNode with no package should work + node, emitter := newTestingDevNodeWithConfig(t, cfg, fooPath) + assert.Len(t, node.ListPkgs(), 1) -// // Test that render is correct -// render, err := testingRenderRealm(t, node, "gno.land/r/dev/foobar") -// require.NoError(t, err) -// require.Equal(t, render, "foo") + // Test render + render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + require.Equal(t, render, "foo") -// // Override `foo.gno` file with bar content -// err = os.WriteFile(filepath.Join(foopkg.Path, "foo.gno"), []byte(barFile), 0o700) -// require.NoError(t, err) + // Render should fail as the node hasn't reloaded + render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") + require.Error(t, err) -// err = node.Reload(context.Background()) -// require.NoError(t, err) + // Add bar package + node.AddPackagePaths(barPath) -// // Check reload event -// assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) + err = node.Reload(context.Background()) + require.NoError(t, err) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) -// // After a reload, render should succeed -// render, err = testingRenderRealm(t, node, "gno.land/r/dev/foobar") -// require.NoError(t, err) -// require.Equal(t, render, "bar") + // After a reload, render should succeed + render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") + require.NoError(t, err) + require.Equal(t, render, "bar") +} -// assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) -// } +func TestNodeUpdatePackage(t *testing.T) { + // Setup a Node instance + const ( + // foo package + foobarPath = "gno.land/r/dev/foobar" + fooFile = `package foobar +func Render(_ string) string { return "foo" }` + barFile = `package foobar +func Render(_ string) string { return "bar" } +` + ) + + // Generate package foo + fooPkg := generateMemPackage(t, foobarPath, "foo.gno", fooFile) + + // Call NewDevNode with no package should work + node, emitter := newTestingDevNode(t, &fooPkg) + assert.Len(t, node.ListPkgs(), 1) + + // Test that render is correct + render, err := testingRenderRealm(t, node, foobarPath) + require.NoError(t, err) + require.Equal(t, render, "foo") + + // Update foo content with bar content + barPkg := generateMemPackage(t, foobarPath, "bar.gno", barFile) + fooPkg.Files = barPkg.Files + + err = node.Reload(context.Background()) + require.NoError(t, err) + + // Check reload event + assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) + + // After a reload, render should succeed + render, err = testingRenderRealm(t, node, foobarPath) + require.NoError(t, err) + require.Equal(t, render, "bar") + + assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) +} // func TestNodeReset(t *testing.T) { // const ( @@ -398,132 +404,132 @@ func Render(_ string) string { return "foo" } // } // } -// func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error) { -// t.Helper() - -// signer := newInMemorySigner(t, node.Config().ChainID()) -// cli := gnoclient.Client{ -// Signer: signer, -// RPCClient: node.Client(), -// } - -// render, res, err := cli.Render(rlmpath, "") -// if err == nil { -// err = res.Response.Error -// } +func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error) { + t.Helper() -// return render, err -// } - -// func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) { -// t.Helper() + signer := newInMemorySigner(t, node.Config().ChainID()) + cli := gnoclient.Client{ + Signer: signer, + RPCClient: node.Client(), + } -// signer := newInMemorySigner(t, node.Config().ChainID()) -// cli := gnoclient.Client{ -// Signer: signer, -// RPCClient: node.Client(), -// } + render, res, err := cli.Render(rlmpath, "") + if err == nil { + err = res.Response.Error + } -// txcfg := gnoclient.BaseTxCfg{ -// GasFee: ugnot.ValueString(1000000), // Gas fee -// GasWanted: 2_000_000, // Gas wanted -// } + return render, err +} -// // Set Caller in the msgs -// caller, err := signer.Info() -// require.NoError(t, err) -// vmMsgs := make([]vm.MsgCall, 0, len(msgs)) -// for _, msg := range msgs { -// vmMsgs = append(vmMsgs, vm.NewMsgCall(caller.GetAddress(), msg.Send, msg.PkgPath, msg.Func, msg.Args)) -// } +func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) { + t.Helper() -// return cli.Call(txcfg, vmMsgs...) -// } + signer := newInMemorySigner(t, node.Config().ChainID()) + cli := gnoclient.Client{ + Signer: signer, + RPCClient: node.Client(), + } -// func generateMemPackage(t *testing.T, path string, pairNameFile ...string) gnovm.MemPackage { -// t.Helper() + txcfg := gnoclient.BaseTxCfg{ + GasFee: ugnot.ValueString(1000000), // Gas fee + GasWanted: 2_000_000, // Gas wanted + } -// if len(pairNameFile)%2 != 0 { -// require.FailNow(t, "Generate testing packages require paired arguments.") -// } - -// // Guess the name based on dir -// // We don't bother parsing files to actually guess the name of the package -// name := filepath.Dir(path) - -// files := make([]*gnovm.MemFile, 0, len(pairNameFile)/2) -// for i := 0; i < len(pairNameFile); i += 2 { -// name := pairNameFile[i] -// content := pairNameFile[i+1] -// files = append(files, &gnovm.MemFile{ -// Name: name, -// Body: content, -// }) -// } + // Set Caller in the msgs + caller, err := signer.Info() + require.NoError(t, err) + vmMsgs := make([]vm.MsgCall, 0, len(msgs)) + for _, msg := range msgs { + vmMsgs = append(vmMsgs, vm.NewMsgCall(caller.GetAddress(), msg.Send, msg.PkgPath, msg.Func, msg.Args)) + } -// return gnovm.MemPackage{ -// Name: name, -// Path: path, -// Files: files, -// } -// } + return cli.Call(txcfg, vmMsgs...) +} -// func createDefaultTestingNodeConfig(pkgs ...gnovm.MemPackage) *NodeConfig { -// var loader packages.Loader +func generateMemPackage(t *testing.T, path string, pairNameFile ...string) gnovm.MemPackage { + t.Helper() + + if len(pairNameFile)%2 != 0 { + require.FailNow(t, "Generate testing packages require paired arguments.") + } + + // Guess the name based on dir + // Don't bother parsing files to actually guess the name of the package + name := filepath.Base(path) + + files := make([]*gnovm.MemFile, 0, len(pairNameFile)/2) + for i := 0; i < len(pairNameFile); i += 2 { + name := pairNameFile[i] + content := pairNameFile[i+1] + files = append(files, &gnovm.MemFile{ + Name: name, + Body: content, + }) + } + + return gnovm.MemPackage{ + Name: name, + Path: path, + Files: files, + } +} -// for _, pkg := range pkgs { -// loader.Paths = append(loader.Paths, pkg.Path) -// } -// loader.Resolver = packages.NewMockResolver(pkgs...) +func newTestingNodeConfig(pkgs ...*gnovm.MemPackage) *NodeConfig { + var loader packages.Loader + loader.Resolver = packages.NewMockResolver(pkgs...) + cfg := DefaultNodeConfig(gnoenv.RootDir()) + cfg.Loader = &loader + return cfg +} -// cfg := DefaultNodeConfig(gnoenv.RootDir()) -// cfg.Loader = &loader -// return cfg -// } +func newTestingDevNode(t *testing.T, pkgs ...*gnovm.MemPackage) (*Node, *mock.ServerEmitter) { + t.Helper() -// func newTestingDevNode(t *testing.T, pkgs ...gnovm.MemPackage) (*Node, *mock.ServerEmitter) { -// t.Helper() + cfg := newTestingNodeConfig(pkgs...) + paths := make([]string, len(pkgs)) + for i, pkg := range pkgs { + paths[i] = pkg.Path + } -// cfg := createDefaultTestingNodeConfig(pkgs...) -// return newTestingDevNodeWithConfig(t, cfg) -// } + return newTestingDevNodeWithConfig(t, cfg, paths...) +} -// func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.ServerEmitter) { -// t.Helper() +func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig, pkgpaths ...string) (*Node, *mock.ServerEmitter) { + t.Helper() -// ctx, cancel := context.WithCancel(context.Background()) -// logger := log.NewTestingLogger(t) -// emitter := &mock.ServerEmitter{} + ctx, cancel := context.WithCancel(context.Background()) + logger := log.NewTestingLogger(t) + emitter := &mock.ServerEmitter{} -// cfg.Emitter = emitter -// cfg.Logger = logger + cfg.Emitter = emitter + cfg.Logger = logger -// node, err := NewDevNode(ctx, cfg) -// require.NoError(t, err) -// assert.Len(t, node.ListPkgs(), len(cfg.PackagesModifier)) + node, err := NewDevNode(ctx, cfg, pkgpaths...) + require.NoError(t, err) + require.Equal(t, emitter.NextEvent().Type(), events.EvtReset) -// t.Cleanup(func() { -// node.Close() -// cancel() -// }) + t.Cleanup(func() { + node.Close() + cancel() + }) -// return node, emitter -// } + return node, emitter +} -// func newInMemorySigner(t *testing.T, chainid string) *gnoclient.SignerFromKeybase { -// t.Helper() +func newInMemorySigner(t *testing.T, chainid string) *gnoclient.SignerFromKeybase { + t.Helper() -// mnemonic := integration.DefaultAccount_Seed -// name := integration.DefaultAccount_Name + mnemonic := integration.DefaultAccount_Seed + name := integration.DefaultAccount_Name -// kb := keys.NewInMemory() -// _, err := kb.CreateAccount(name, mnemonic, "", "", uint32(0), uint32(0)) -// require.NoError(t, err) + kb := keys.NewInMemory() + _, err := kb.CreateAccount(name, mnemonic, "", "", uint32(0), uint32(0)) + require.NoError(t, err) -// return &gnoclient.SignerFromKeybase{ -// Keybase: kb, // Stores keys in memory -// Account: name, // Account name -// Password: "", // Password for encryption -// ChainID: chainid, // Chain ID for transaction signing -// } -// } + return &gnoclient.SignerFromKeybase{ + Keybase: kb, // Stores keys in memory + Account: name, // Account name + Password: "", // Password for encryption + ChainID: chainid, // Chain ID for transaction signing + } +} diff --git a/contribs/gnodev/pkg/packages/resolver_mock.go b/contribs/gnodev/pkg/packages/resolver_mock.go index 239a41f3f18..eef0fe3655b 100644 --- a/contribs/gnodev/pkg/packages/resolver_mock.go +++ b/contribs/gnodev/pkg/packages/resolver_mock.go @@ -7,11 +7,11 @@ import ( ) type MockResolver struct { - pkgs map[string]gnovm.MemPackage + pkgs map[string]*gnovm.MemPackage } -func NewMockResolver(pkgs ...gnovm.MemPackage) *MockResolver { - mappkgs := make(map[string]gnovm.MemPackage, len(pkgs)) +func NewMockResolver(pkgs ...*gnovm.MemPackage) *MockResolver { + mappkgs := make(map[string]*gnovm.MemPackage, len(pkgs)) for _, pkg := range pkgs { mappkgs[pkg.Path] = pkg } @@ -28,7 +28,7 @@ func (m *MockResolver) Name() string { func (m *MockResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { if mempkg, ok := m.pkgs[path]; ok { return &Package{ - MemPackage: mempkg, + MemPackage: *mempkg, Kind: PackageKindOther, Location: "", }, nil From 3e9c300caaafb9eb01006e3eb21c9f73afb1d597 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 12 Dec 2024 14:49:01 +0100 Subject: [PATCH 06/24] wip: staging Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/command_staging.go | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 contribs/gnodev/cmd/gnodev/command_staging.go diff --git a/contribs/gnodev/cmd/gnodev/command_staging.go b/contribs/gnodev/cmd/gnodev/command_staging.go new file mode 100644 index 00000000000..5da6e8fb658 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/command_staging.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "flag" + + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type stagingCfg struct { + devCfg +} + +var defaultStagingOptions = devCfg{ + chainId: "staging", + maxGas: 10_000_000_000, + webListenerAddr: "127.0.0.1:8888", + nodeRPCListenerAddr: "127.0.0.1:26657", + deployKey: DefaultDeployerAddress.String(), + home: gnoenv.HomeDir(), + root: gnoenv.RootDir(), + serverMode: true, + unsafeAPI: false, + + // As we have no reason to configure this yet, set this to random port + // to avoid potential conflict with other app + nodeP2PListenerAddr: "tcp://127.0.0.1:0", + nodeProxyAppListenerAddr: "tcp://127.0.0.1:0", +} + +func NewStagingCmd(io commands.IO) *commands.Command { + cfg := &stagingCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "staging", + ShortUsage: "gnodev staging [flags] ", + ShortHelp: "start gnodev in staging mode", + NoParentFlags: true, + }, + cfg, + func(_ context.Context, args []string) error { + return execStagingCmd(cfg, args, io) + }, + ) +} + +func (c *stagingCfg) RegisterFlags(fs *flag.FlagSet) { + c.devCfg.registerFlagsWithDefault(defaultStagingOptions, fs) +} + +func execStagingCmd(cg *stagingCfg, args []string, io commands.IO) error { + return nil +} From 0e5e5f0336bc864480ac0dfd579a653710b58352 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 13 Dec 2024 21:22:54 +0100 Subject: [PATCH 07/24] wip: glob Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/command_staging.go | 27 +- contribs/gnodev/cmd/gnodev/logger.go | 9 +- contribs/gnodev/cmd/gnodev/main.go | 467 +++++++++--------- contribs/gnodev/cmd/gnodev/setup_loader.go | 68 ++- contribs/gnodev/cmd/gnodev/setup_node.go | 2 +- contribs/gnodev/cmd/gnodev/setup_term.go | 2 +- contribs/gnodev/pkg/dev/node.go | 4 +- contribs/gnodev/pkg/dev/node_test.go | 5 +- contribs/gnodev/pkg/packages/loader.go | 48 +- contribs/gnodev/pkg/packages/loader_glob.go | 55 +++ contribs/gnodev/pkg/packages/package.go | 37 +- contribs/gnodev/pkg/packages/resolver.go | 230 ++++----- .../{resolver_root.go => resolver_fs.go} | 12 +- .../pkg/packages/resolver_middleware.go | 168 +++++++ .../gnodev/pkg/packages/resolver_remote.go | 5 - contribs/gnodev/pkg/packages/utils.go | 7 + tm2/pkg/commands/command.go | 53 +- 17 files changed, 716 insertions(+), 483 deletions(-) create mode 100644 contribs/gnodev/pkg/packages/loader_glob.go rename contribs/gnodev/pkg/packages/{resolver_root.go => resolver_fs.go} (53%) create mode 100644 contribs/gnodev/pkg/packages/resolver_middleware.go diff --git a/contribs/gnodev/cmd/gnodev/command_staging.go b/contribs/gnodev/cmd/gnodev/command_staging.go index 5da6e8fb658..d0d0f839875 100644 --- a/contribs/gnodev/cmd/gnodev/command_staging.go +++ b/contribs/gnodev/cmd/gnodev/command_staging.go @@ -3,13 +3,16 @@ package main import ( "context" "flag" + "fmt" + "path/filepath" + "strings" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/commands" ) type stagingCfg struct { - devCfg + dev devCfg } var defaultStagingOptions = devCfg{ @@ -20,7 +23,7 @@ var defaultStagingOptions = devCfg{ deployKey: DefaultDeployerAddress.String(), home: gnoenv.HomeDir(), root: gnoenv.RootDir(), - serverMode: true, + interactive: false, unsafeAPI: false, // As we have no reason to configure this yet, set this to random port @@ -30,7 +33,7 @@ var defaultStagingOptions = devCfg{ } func NewStagingCmd(io commands.IO) *commands.Command { - cfg := &stagingCfg{} + var cfg stagingCfg return commands.NewCommand( commands.Metadata{ @@ -39,17 +42,27 @@ func NewStagingCmd(io commands.IO) *commands.Command { ShortHelp: "start gnodev in staging mode", NoParentFlags: true, }, - cfg, + &cfg, func(_ context.Context, args []string) error { - return execStagingCmd(cfg, args, io) + return execStagingCmd(&cfg, args, io) }, ) } func (c *stagingCfg) RegisterFlags(fs *flag.FlagSet) { - c.devCfg.registerFlagsWithDefault(defaultStagingOptions, fs) + c.dev.registerFlagsWithDefault(defaultStagingOptions, fs) } -func execStagingCmd(cg *stagingCfg, args []string, io commands.IO) error { +func execStagingCmd(cfg *stagingCfg, args []string, io commands.IO) error { + if len(args) == 0 { + return fmt.Errorf("no argument given") + } + + mathches, err := filepath.Glob(args[0]) + if err != nil { + return fmt.Errorf("invalid glob: %w", err) + } + + io.Println(strings.Join(mathches, "\n")) return nil } diff --git a/contribs/gnodev/cmd/gnodev/logger.go b/contribs/gnodev/cmd/gnodev/logger.go index 9e69654f478..8ed942700db 100644 --- a/contribs/gnodev/cmd/gnodev/logger.go +++ b/contribs/gnodev/cmd/gnodev/logger.go @@ -6,7 +6,6 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/gnolang/gno/contribs/gnodev/pkg/logger" - gnolog "github.com/gnolang/gno/gno.land/pkg/log" "github.com/muesli/termenv" ) @@ -16,10 +15,10 @@ func setuplogger(cfg *devCfg, out io.Writer) *slog.Logger { level = slog.LevelDebug } - if cfg.serverMode { - zaplogger := logger.NewZapLogger(out, level) - return gnolog.ZapLoggerToSlog(zaplogger) - } + // if cfg.serverMode { + // zaplogger := logger.NewZapLogger(out, level) + // return gnolog.ZapLoggerToSlog(zaplogger) + // } // Detect term color profile colorProfile := termenv.DefaultOutput().Profile diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index aab7b4fc7d7..105441993d5 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -14,6 +14,7 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/address" gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" "github.com/gnolang/gno/gno.land/pkg/integration" @@ -75,11 +76,12 @@ type devCfg struct { maxGas int64 chainId string chainDomain string - serverMode bool unsafeAPI bool + interactive bool + loadPath string } -var defaultDevOptions = &devCfg{ +var defaultDevOptions = devCfg{ chainId: "dev", chainDomain: "gno.land", maxGas: 10_000_000_000, @@ -88,6 +90,8 @@ var defaultDevOptions = &devCfg{ deployKey: DefaultDeployerAddress.String(), home: gnoenv.HomeDir(), root: gnoenv.RootDir(), + interactive: true, + unsafeAPI: true, // As we have no reason to configure this yet, set this to random port // to avoid potential conflict with other app @@ -111,28 +115,41 @@ func main() { return execDev(cfg, args, stdio) }) + cmd.AddSubCommands(NewStagingCmd(stdio)) + cmd.Execute(context.Background(), os.Args[1:]) } func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { - fs.StringVar( - &c.chdir, - "C", - defaultDevOptions.chdir, - "change directory before running gnodev", - ) + c.registerFlagsWithDefault(defaultDevOptions, fs) +} +func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { fs.StringVar( &c.home, "home", - defaultDevOptions.home, + defaultCfg.home, "user's local directory for keys", ) + fs.BoolVar( + &c.interactive, + "interactive", + defaultCfg.interactive, + "enable gnodev interactive mode", + ) + + fs.StringVar( + &c.chdir, + "chdir", + defaultCfg.chdir, + "change directory context", + ) + fs.StringVar( &c.root, "root", - defaultDevOptions.root, + defaultCfg.root, "gno root directory", ) @@ -160,13 +177,13 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { fs.Var( &c.resolvers, "resolver", - "list of addtional resolvers, will be exectued in the same order", + "list of addtional resolvers, will be exectued in the given order", ) fs.StringVar( &c.nodeRPCListenerAddr, "node-rpc-listener", - defaultDevOptions.nodeRPCListenerAddr, + defaultCfg.nodeRPCListenerAddr, "listening address for GnoLand RPC node", ) @@ -179,56 +196,42 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.balancesFile, "balance-file", - defaultDevOptions.balancesFile, + defaultCfg.balancesFile, "load the provided balance file (refer to the documentation for format)", ) + fs.StringVar( + &c.balancesFile, + "load-path", + defaultCfg.balancesFile, + "load given dir (glob supported)", + ) + fs.StringVar( &c.txsFile, "txs-file", - defaultDevOptions.txsFile, + defaultCfg.txsFile, "load the provided transactions file (refer to the documentation for format)", ) fs.StringVar( &c.genesisFile, "genesis", - defaultDevOptions.genesisFile, + defaultCfg.genesisFile, "load the given genesis file", ) fs.StringVar( &c.deployKey, "deploy-key", - defaultDevOptions.deployKey, + defaultCfg.deployKey, "default key name or Bech32 address for deploying packages", ) - fs.BoolVar( - &c.minimal, - "minimal", - defaultDevOptions.minimal, - "do not load packages from the examples directory", - ) - - fs.BoolVar( - &c.serverMode, - "server-mode", - defaultDevOptions.serverMode, - "disable interaction, and adjust logging for server use.", - ) - - fs.BoolVar( - &c.verbose, - "v", - defaultDevOptions.verbose, - "enable verbose output for development", - ) - fs.StringVar( &c.chainId, "chain-id", - defaultDevOptions.chainId, + defaultCfg.chainId, "set node ChainID", ) @@ -242,30 +245,38 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { fs.BoolVar( &c.noWatch, "no-watch", - defaultDevOptions.noWatch, + defaultCfg.noWatch, "do not watch for file changes", ) fs.BoolVar( &c.noReplay, "no-replay", - defaultDevOptions.noReplay, + defaultCfg.noReplay, "do not replay previous transactions upon reload", ) fs.Int64Var( &c.maxGas, "max-gas", - defaultDevOptions.maxGas, + defaultCfg.maxGas, "set the maximum gas per block", ) fs.BoolVar( &c.unsafeAPI, "unsafe-api", - defaultDevOptions.unsafeAPI, + defaultCfg.unsafeAPI, "enable /reset and /reload endpoints which are not safe to expose publicly", ) + + // Short flags + fs.BoolVar( + &c.verbose, + "v", + defaultCfg.verbose, + "enable verbose output for development", + ) } func (c *devCfg) validateConfigFlags() error { @@ -276,21 +287,37 @@ func (c *devCfg) validateConfigFlags() error { return nil } -func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { - ctx, cancel := context.WithCancelCause(context.Background()) - defer cancel(nil) +type App struct { + ctx context.Context + cfg *devCfg + io commands.IO + logger *slog.Logger + + devNode *gnodev.Node + server *http.Server + emitterServer *emitter.Server + watcher *watcher.PackageWatcher + book *address.Book + exportPath string + + // XXX: move this + exported uint +} - if cfg.chdir != "" { - if err := os.Chdir(cfg.chdir); err != nil { - return fmt.Errorf("unable to change directory: %w", err) - } +func NewApp(ctx context.Context, logger *slog.Logger, cfg *devCfg, io commands.IO) *App { + return &App{ + ctx: ctx, + logger: logger, + cfg: cfg, + io: io, } +} - if err := cfg.validateConfigFlags(); err != nil { - return fmt.Errorf("validate error: %w", err) - } +func execDev(cfg *devCfg, args []string, io commands.IO) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - // Setup Raw Terminal + var err error rt, restore, err := setupRawTerm(cfg, io) if err != nil { return fmt.Errorf("unable to init raw term: %w", err) @@ -299,128 +326,157 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { // Setup trap signal osm.TrapSignal(func() { - cancel(nil) + cancel() restore() }) logger := setuplogger(cfg, rt) - loggerEvents := logger.WithGroup(EventServerLogName) - emitterServer := emitter.NewServer(loggerEvents) + devServer := NewApp(ctx, logger, cfg, io) + if err := devServer.Setup(); err != nil { + return err + } + + return devServer.Run(rt) +} + +func (ds *App) Setup() error { + if err := ds.cfg.validateConfigFlags(); err != nil { + return fmt.Errorf("validate error: %w", err) + } + + if ds.cfg.chdir != "" { + if err := os.Chdir(ds.cfg.chdir); err != nil { + return fmt.Errorf("unable to change directory: %w", err) + } + } + + loggerEvents := ds.logger.WithGroup(EventServerLogName) + ds.emitterServer = emitter.NewServer(loggerEvents) dir, err := os.Getwd() if err != nil { return fmt.Errorf("unable to guess current dir: %w", err) } - path, ok := guessPath(cfg, dir) + path, ok := guessPath(ds.cfg, dir) if !ok { return fmt.Errorf("unable to guess path from %q", dir) } - loader := setupPackagesLoader(logger.WithGroup(LoaderLogName), cfg, path, dir) - // load keybase - book, err := setupAddressBook(logger.WithGroup(AccountsLogName), cfg) + resolver := setupPackagesResolver(ds.logger.WithGroup(LoaderLogName), ds.cfg, path, dir) + loader := packages.NewResolverLoader(resolver) + + ds.book, err = setupAddressBook(ds.logger.WithGroup(AccountsLogName), ds.cfg) if err != nil { return fmt.Errorf("unable to load keybase: %w", err) } - // generate balances - balances, err := generateBalances(book, cfg) + balances, err := generateBalances(ds.book, ds.cfg) if err != nil { return fmt.Errorf("unable to generate balances: %w", err) } - logger.Debug("balances loaded", "list", balances.List()) + ds.logger.Debug("balances loaded", "list", balances.List()) - // Setup Dev Node - // XXX: find a good way to export or display node logs - nodeLogger := logger.WithGroup(NodeLogName) - nodeCfg := setupDevNodeConfig(cfg, logger, emitterServer, balances, loader) - devNode, err := setupDevNode(ctx, cfg, nodeCfg, path) + nodeLogger := ds.logger.WithGroup(NodeLogName) + nodeCfg := setupDevNodeConfig(ds.cfg, nodeLogger, ds.emitterServer, balances, loader) + ds.devNode, err = setupDevNode(ds.ctx, ds.cfg, nodeCfg, path) if err != nil { return err } - defer devNode.Close() - - nodeLogger.Info("node started", "lisn", devNode.GetRemoteAddress(), "chainID", cfg.chainId) - // Create server - mux := http.NewServeMux() - server := http.Server{ - Handler: mux, - Addr: cfg.webListenerAddr, - ReadHeaderTimeout: time.Second * 60, + ds.watcher, err = watcher.NewPackageWatcher(loggerEvents, ds.emitterServer) + if err != nil { + return fmt.Errorf("unable to setup packages watcher: %w", err) } - defer server.Close() - // Setup gnoweb - webhandler := setupGnoWebServer(logger.WithGroup(WebLogName), cfg, devNode) + ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) - // Setup unsafe APIs if enabled - if cfg.unsafeAPI { + return nil +} + +func (ds *App) setupHandlers() http.Handler { + mux := http.NewServeMux() + webhandler := setupGnoWebServer(ds.logger.WithGroup(WebLogName), ds.cfg, ds.devNode) + + // Setup unsage api + if ds.cfg.unsafeAPI { mux.HandleFunc("/reset", func(res http.ResponseWriter, req *http.Request) { - if err := devNode.Reset(req.Context()); err != nil { - logger.Error("failed to reset", slog.Any("err", err)) + if err := ds.devNode.Reset(req.Context()); err != nil { + ds.logger.Error("failed to reset", slog.Any("err", err)) res.WriteHeader(http.StatusInternalServerError) } }) mux.HandleFunc("/reload", func(res http.ResponseWriter, req *http.Request) { - if err := devNode.Reload(req.Context()); err != nil { - logger.Error("failed to reload", slog.Any("err", err)) + if err := ds.devNode.Reload(req.Context()); err != nil { + ds.logger.Error("failed to reload", slog.Any("err", err)) res.WriteHeader(http.StatusInternalServerError) } }) } - // XXX: TODO: - // - create a map of known path that are allowed - // - move this - // u, err := url.Parse("http://" + path) - // if err != nil { - // return fmt.Errorf("malformed path %q: %w", path, err) - // } - // handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // if !strings.HasPrefix(r.URL.Path, "/static") && !strings.HasPrefix(r.URL.Path, u.Path) { - // http.Redirect(w, r, u.Path, http.StatusFound) - // return - // } - - // webhandler.ServeHTTP(w, r) - // }) - - // Setup HotReload if needed - if !cfg.noWatch { - evtstarget := fmt.Sprintf("%s/_events", server.Addr) - mux.Handle("/_events", emitterServer) + if !ds.cfg.noWatch { + evtstarget := fmt.Sprintf("%s/_events", ds.cfg.webListenerAddr) + mux.Handle("/_events", ds.emitterServer) mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler)) } else { mux.Handle("/", webhandler) } + return mux +} + +func (ds *App) Run(term *rawterm.RawTerm) error { + ctx, cancelWith := context.WithCancelCause(ds.ctx) + defer cancelWith(nil) + + addr := ds.cfg.webListenerAddr + ds.logger.WithGroup(WebLogName).Info("gnoweb started", "lisn", fmt.Sprintf("http://%s", addr)) + + server := &http.Server{ + Handler: ds.setupHandlers(), + Addr: ds.cfg.webListenerAddr, + ReadHeaderTimeout: time.Second * 60, + } + go func() { err := server.ListenAndServe() - cancel(err) + cancelWith(err) }() - logger.WithGroup(WebLogName). - Info("gnoweb started", - "lisn", fmt.Sprintf("http://%s", server.Addr)) - - watcher, err := watcher.NewPackageWatcher(loggerEvents, emitterServer) - if err != nil { - return fmt.Errorf("unable to setup packages watcher: %w", err) + if ds.cfg.interactive { + ds.logger.WithGroup("--- READY").Info("for commands and help, press `h`") } - defer watcher.Stop() - // Add node pkgs to watcher - watcher.UpdatePackagesWatch(devNode.ListPkgs()...) + keyPressCh := listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) + for { + select { + case <-ctx.Done(): + return context.Cause(ctx) + case _, ok := <-ds.watcher.PackagesUpdate: + if !ok { + return nil + } + ds.logger.WithGroup(NodeLogName).Info("reloading...") + if err := ds.devNode.Reload(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } + ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) - if !cfg.serverMode { - logger.WithGroup("--- READY").Info("for commands and help, press `h`") - } + case key, ok := <-keyPressCh: + if !ok { + return nil + } + + if key == rawterm.KeyCtrlC { + cancelWith(nil) + continue + } - // Run the main event loop - return runEventLoop(ctx, logger, book, rt, devNode, watcher) + ds.handleKeyPress(key) + keyPressCh = listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) + } + } } var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: @@ -437,131 +493,72 @@ Ctrl+R Reset - Reset application to it's initial/save state. Ctrl+C Exit - Exit the application ` -func runEventLoop( - ctx context.Context, - logger *slog.Logger, - bk *address.Book, - rt *rawterm.RawTerm, - dnode *gnodev.Node, - watch *watcher.PackageWatcher, -) error { - // XXX: move this in above, but we need to have a proper struct first - // XXX: make this configurable - var exported uint - path, err := os.MkdirTemp("", "gnodev-export") - if err != nil { - return fmt.Errorf("unable to create `export` directory: %w", err) - } +func (ds *App) handleKeyPress(key rawterm.KeyPress) { + var err error + ds.logger.WithGroup(KeyPressLogName).Debug(fmt.Sprintf("<%s>", key.String())) - defer func() { - if exported == 0 { - _ = os.RemoveAll(path) - } - }() + switch key.Upper() { + case rawterm.KeyH: // Helper + ds.logger.Info("Gno Dev Helper", "helper", helper) - keyPressCh := listenForKeyPress(logger.WithGroup(KeyPressLogName), rt) - for { - var err error + case rawterm.KeyA: // Accounts + logAccounts(ds.logger.WithGroup(AccountsLogName), ds.book, ds.devNode) - select { - case <-ctx.Done(): - return context.Cause(ctx) - case _, ok := <-watch.PackagesUpdate: - if !ok { - return nil - } + case rawterm.KeyR: // Reload + ds.logger.WithGroup(NodeLogName).Info("reloading...") + if err = ds.devNode.ReloadAll(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } - // if err = dnode.UpdatePackages(pkgs.PackagesPath()...); err != nil { - // return fmt.Errorf("unable to update packages: %w", err) - // } + case rawterm.KeyCtrlR: // Reset + ds.logger.WithGroup(NodeLogName).Info("reseting node state...") + if err = ds.devNode.Reset(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reset node state", "err", err) + } - logger.WithGroup(NodeLogName).Info("reloading...") - if err = dnode.Reload(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reload node", "err", err) + case rawterm.KeyCtrlS: // Save + ds.logger.WithGroup(NodeLogName).Info("saving state...") + if err := ds.devNode.SaveCurrentState(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to save node state", "err", err) + } + + case rawterm.KeyE: // Export + // Create a temporary export dir + if ds.exported == 0 { + ds.exportPath, err = os.MkdirTemp("", "gnodev-export") + if err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to create `export` directory", "err", err) + return } + } + ds.exported++ - listpkgs := dnode.ListPkgs() - watch.UpdatePackagesWatch(listpkgs...) + ds.logger.WithGroup(NodeLogName).Info("exporting state...") + doc, err := ds.devNode.ExportStateAsGenesis(ds.ctx) + if err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to export node state", "err", err) + return + } - case key, ok := <-keyPressCh: - if !ok { - return nil - } + docfile := filepath.Join(ds.exportPath, fmt.Sprintf("export_%d.jsonl", ds.exported)) + if err := doc.SaveAs(docfile); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to save genesis", "err", err) + } - logger.WithGroup(KeyPressLogName).Debug( - fmt.Sprintf("<%s>", key.String()), - ) - - switch key.Upper() { - case rawterm.KeyH: // Helper - logger.Info("Gno Dev Helper", "helper", helper) - - case rawterm.KeyA: // Accounts - logAccounts(logger.WithGroup(AccountsLogName), bk, dnode) - - case rawterm.KeyR: // Reload - logger.WithGroup(NodeLogName).Info("reloading...") - if err = dnode.ReloadAll(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reload node", "err", err) - } - - case rawterm.KeyCtrlR: // Reset - logger.WithGroup(NodeLogName).Info("reseting node state...") - if err = dnode.Reset(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reset node state", "err", err) - } - - case rawterm.KeyCtrlS: // Save - logger.WithGroup(NodeLogName).Info("saving state...") - if err := dnode.SaveCurrentState(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to save node state", "err", err) - } - - case rawterm.KeyE: - logger.WithGroup(NodeLogName).Info("exporting state...") - doc, err := dnode.ExportStateAsGenesis(ctx) - if err != nil { - logger.WithGroup(NodeLogName). - Error("unable to export node state", "err", err) - continue - } - - docfile := filepath.Join(path, fmt.Sprintf("export_%d.jsonl", exported)) - if err := doc.SaveAs(docfile); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to save genesis", "err", err) - } - exported++ - - logger.WithGroup(NodeLogName).Info("node state exported", "file", docfile) - - case rawterm.KeyN: // Next tx - logger.Info("moving forward...") - if err := dnode.MoveToNextTX(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to move forward", "err", err) - } - - case rawterm.KeyP: // Next tx - logger.Info("moving backward...") - if err := dnode.MoveToPreviousTX(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to move backward", "err", err) - } - - case rawterm.KeyCtrlC: // Exit - return nil + ds.logger.WithGroup(NodeLogName).Info("node state exported", "file", docfile) - default: - } + case rawterm.KeyN: // Next tx + ds.logger.Info("moving forward...") + if err := ds.devNode.MoveToNextTX(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to move forward", "err", err) + } - // Reset listen for the next keypress - keyPressCh = listenForKeyPress(logger.WithGroup(KeyPressLogName), rt) + case rawterm.KeyP: // Previous tx + ds.logger.Info("moving backward...") + if err := ds.devNode.MoveToPreviousTX(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to move backward", "err", err) } + default: } } diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go index 3350df42089..6f8bd22001f 100644 --- a/contribs/gnodev/cmd/gnodev/setup_loader.go +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "go/scanner" "log/slog" "path/filepath" "strings" @@ -30,14 +29,19 @@ func (va *varResolver) Set(value string) error { case "remote": rpc, err := client.NewHTTPClient(location) if err != nil { - return fmt.Errorf("invalid resolver remote location: %q", location, name) + return fmt.Errorf("invalid resolver remote: %q", location) } - res = packages.Cache(packages.NewRemoteResolver(rpc)) + res = packages.NewRemoteResolver(rpc) case "root": - res = packages.NewRootResolver(location) - // case "pkgdir": - // res = packages.NewLo(location) + res = packages.NewFSResolver(location) + case "pkgdir": + path, ok := guessPathGnoMod(location) + if !ok { + return fmt.Errorf("unable to read module path from gno.mod in %q", location) + } + + res = packages.NewLocalResolver(path, location) default: return fmt.Errorf("invalid resolver name: %q", name) } @@ -46,6 +50,27 @@ func (va *varResolver) Set(value string) error { return nil } +func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, path, dir string) packages.Resolver { + // Add root resolvers + exampleRoot := filepath.Join(cfg.root, "examples") + + resolver := packages.ChainResolvers( + packages.NewLocalResolver(path, dir), // Resolve local directory + packages.ChainResolvers(cfg.resolvers...), // Use user's custom resolvers + packages.NewFSResolver(exampleRoot), // Ultimately use fs resolver + ) + + // Enrich resolver with middleware + return packages.MiddlewareResolver(resolver, + packages.CacheMiddleware(func(pkg *packages.Package) bool { + return pkg.Kind == packages.PackageKindRemote // Cache only remote package + }), + packages.FilterMiddleware("stdlib", isStdPath), // Filter stdlib package from resolving + packages.SyntaxCheckerMiddleware(logger), // Pre-check syntax to avoid bothering the node reloading on invalid files + packages.LogMiddleware(logger), // Log any request + ) +} + func guessPathFromRoots(dir string, roots ...string) (path string, ok bool) { for _, root := range roots { if !strings.HasPrefix(dir, root) { @@ -83,39 +108,10 @@ func guessPath(cfg *devCfg, dir string) (path string, ok bool) { func isStdPath(path string) bool { if i := strings.IndexRune(path, '/'); i > 0 { - if j := strings.IndexRune(path[:i], '.'); i >= 0 { + if j := strings.IndexRune(path[:i], '.'); j >= 0 { return false } } return true } - -func setupPackagesLoader(logger *slog.Logger, cfg *devCfg, path, dir string) (loader *packages.Loader) { - gnoroot := cfg.root - - localresolver := packages.NewLocalResolver(path, dir) - - // Add root resolvers - exampleRoot := filepath.Join(gnoroot, "examples") - fsResolver := packages.NewRootResolver(exampleRoot) - - resolver := packages.ChainWithLogger(logger, - localresolver, - packages.ChainResolvers(cfg.resolvers...), - fsResolver, - ) - - syntaxResolver := packages.SyntaxChecker(resolver, resolverErrorHandler(logger)) - return packages.NewLoaderWithFilter(isStdPath, syntaxResolver) -} - -func resolverErrorHandler(logger *slog.Logger) packages.SyntaxErrorHandler { - return func(path string, filename string, serr *scanner.Error) { - logger.Error("syntax error", - "path", path, - "filename", filename, - "err", serr.Error(), - ) - } -} diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index 1459e1d95f7..b53638c04c6 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -52,7 +52,7 @@ func setupDevNodeConfig( logger *slog.Logger, emitter emitter.Emitter, balances gnoland.Balances, - loader *packages.Loader, + loader packages.Loader, ) *gnodev.NodeConfig { config := gnodev.DefaultNodeConfig(cfg.root, cfg.chainDomain) config.Loader = loader diff --git a/contribs/gnodev/cmd/gnodev/setup_term.go b/contribs/gnodev/cmd/gnodev/setup_term.go index 1f8f3046969..2220a7c5de9 100644 --- a/contribs/gnodev/cmd/gnodev/setup_term.go +++ b/contribs/gnodev/cmd/gnodev/setup_term.go @@ -10,7 +10,7 @@ var noopRestore = func() error { return nil } func setupRawTerm(cfg *devCfg, io commands.IO) (*rawterm.RawTerm, func() error, error) { rt := rawterm.NewRawTerm() restore := noopRestore - if !cfg.serverMode { + if cfg.interactive { var err error restore, err = rt.Init() if err != nil { diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 7521e16d399..969744c763a 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -33,7 +33,7 @@ import ( type NodeConfig struct { Logger *slog.Logger - Loader *packages.Loader + Loader packages.Loader DefaultCreator crypto.Address DefaultDeposit std.Coins BalancesList []gnoland.Balance @@ -106,7 +106,7 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig, pkgpaths ...string) (*Node startTime := time.Now() devnode := &Node{ - loader: *cfg.Loader, + loader: cfg.Loader, config: cfg, client: client.NewLocal(), emitter: cfg.Emitter, diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index bc36ab42015..4c474da75c7 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -62,7 +62,8 @@ func Render(_ string) string { return "foo" } logger := log.NewTestingLogger(t) cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") - cfg.Loader = packages.NewLoader(packages.NewMockResolver(&pkg)) + cfg.Loader = packages.NewResolverLoader(packages.NewMockResolver(&pkg)) + cfg.Logger = logger node, err := NewDevNode(ctx, cfg, path) require.NoError(t, err) @@ -474,7 +475,7 @@ func generateMemPackage(t *testing.T, path string, pairNameFile ...string) gnovm } func newTestingNodeConfig(pkgs ...*gnovm.MemPackage) *NodeConfig { - var loader packages.Loader + var loader packages.ResolverLoader loader.Resolver = packages.NewMockResolver(pkgs...) cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") cfg.Loader = &loader diff --git a/contribs/gnodev/pkg/packages/loader.go b/contribs/gnodev/pkg/packages/loader.go index 6d110d0fd80..02b2b97bbae 100644 --- a/contribs/gnodev/pkg/packages/loader.go +++ b/contribs/gnodev/pkg/packages/loader.go @@ -9,18 +9,16 @@ import ( var ErrNoResolvers = errors.New("no resolvers setup") -type FilterFunc func(path string) bool - -func NoopFilterFunc(path string) bool { return false } -func FilterAllFunc(path string) bool { return true } +type Loader interface { + Load(paths ...string) ([]Package, error) +} -type Loader struct { +type ResolverLoader struct { Resolver - FilterFunc } -func NewLoader(res ...Resolver) *Loader { - loader := Loader{FilterFunc: NoopFilterFunc} +func NewResolverLoader(res ...Resolver) *ResolverLoader { + var loader ResolverLoader switch len(res) { case 0: // Skip case 1: @@ -32,22 +30,12 @@ func NewLoader(res ...Resolver) *Loader { return &loader } -func NewLoaderWithFilter(filter FilterFunc, res ...Resolver) *Loader { - loader := NewLoader(res...) - loader.FilterFunc = filter - return loader -} - -func (l Loader) Load(paths ...string) ([]Package, error) { - if l.Resolver == nil { - return nil, ErrNoResolvers - } - +func (l ResolverLoader) Load(paths ...string) ([]Package, error) { fset := token.NewFileSet() visited, stack := map[string]bool{}, map[string]bool{} pkgs := make([]Package, 0) for _, root := range paths { - deps, err := l.loadPackage(root, fset, l.Resolver, visited, stack) + deps, err := loadPackage(root, fset, l.Resolver, visited, stack) if err != nil { return nil, err } @@ -57,7 +45,7 @@ func (l Loader) Load(paths ...string) ([]Package, error) { return pkgs, nil } -func (l Loader) loadPackage(path string, fset *token.FileSet, resolver Resolver, visited, stack map[string]bool) ([]Package, error) { +func loadPackage(path string, fset *token.FileSet, resolver Resolver, visited, stack map[string]bool) ([]Package, error) { if stack[path] { return nil, fmt.Errorf("cycle detected: %s", path) } @@ -67,20 +55,24 @@ func (l Loader) loadPackage(path string, fset *token.FileSet, resolver Resolver, visited[path] = true - // Apply filter func if any - if l.FilterFunc != nil && l.FilterFunc(path) { - return nil, nil - } - mempkg, err := resolver.Resolve(fset, path) if err != nil { + if errors.Is(err, ErrResolverPackageSkip) { + return nil, nil + } + return nil, fmt.Errorf("unable to resolve package: %w", err) } var name string imports := map[string]struct{}{} for _, file := range mempkg.Files { - f, err := parser.ParseFile(fset, file.Name, file.Body, parser.ImportsOnly) + fname := file.Name + if !isGnoFile(fname) || isTestFile(fname) { + continue + } + + f, err := parser.ParseFile(fset, fname, file.Body, parser.ImportsOnly) if err != nil { return nil, fmt.Errorf("unable to parse file %q: %w", file.Name, err) } @@ -103,7 +95,7 @@ func (l Loader) loadPackage(path string, fset *token.FileSet, resolver Resolver, pkgs := []Package{} for imp := range imports { - subDeps, err := l.loadPackage(imp, fset, resolver, visited, stack) + subDeps, err := loadPackage(imp, fset, resolver, visited, stack) if err != nil { return nil, fmt.Errorf("importing %q: %w", imp, err) } diff --git a/contribs/gnodev/pkg/packages/loader_glob.go b/contribs/gnodev/pkg/packages/loader_glob.go new file mode 100644 index 00000000000..a2cd8384555 --- /dev/null +++ b/contribs/gnodev/pkg/packages/loader_glob.go @@ -0,0 +1,55 @@ +package packages + +import ( + "fmt" + "go/token" + "path/filepath" + "strings" +) + +type GlobLoader struct { + Resolver Resolver + Root string +} + +func NewGlobResolverLoader(rootpath string, res ...Resolver) Loader { + loader := GlobLoader{Root: rootpath} + switch len(res) { + case 0: // Skip + case 1: + loader.Resolver = res[0] + default: + loader.Resolver = ChainResolvers(res...) + } + + return &loader +} + +func (l GlobLoader) Load(paths ...string) ([]Package, error) { + fset := token.NewFileSet() + visited, stack := map[string]bool{}, map[string]bool{} + pkgs := make([]Package, 0) + for _, path := range paths { + // format path to match directory from loader `Root` + path = filepath.Clean(filepath.Join(l.Root, path) + "/") + + matches, err := filepath.Glob(path) + if err != nil { + return nil, fmt.Errorf("invalid glob path: %w", err) + } + + for _, match := range matches { + // extract path + mpath, _ := strings.CutPrefix(match, l.Root) + mpath = strings.Trim(mpath, "/") + + deps, err := loadPackage(mpath, fset, l.Resolver, visited, stack) + if err != nil { + return nil, err + } + pkgs = append(pkgs, deps...) + } + } + + return pkgs, nil +} diff --git a/contribs/gnodev/pkg/packages/package.go b/contribs/gnodev/pkg/packages/package.go index 96fd07cf11b..3d733d54af1 100644 --- a/contribs/gnodev/pkg/packages/package.go +++ b/contribs/gnodev/pkg/packages/package.go @@ -33,28 +33,47 @@ func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) var name string memFiles := []*gnovm.MemFile{} for _, file := range files { - fname := file.Name() - if !isGnoFile(fname) || isTestFile(fname) { + if file.IsDir() { continue } + fname := file.Name() filepath := filepath.Join(dir, fname) body, err := os.ReadFile(filepath) if err != nil { return nil, fmt.Errorf("unable to read file %q: %w", filepath, err) } - memfile, pkgname, err := parseFile(fset, fname, body) - if err != nil { - return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) + if isGnoFile(fname) { + memfile, pkgname, err := parseFile(fset, fname, body) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) + } + + if !isTestFile(fname) { + if name != "" && name != pkgname { + return nil, fmt.Errorf("conflict package name between %q and %q", name, memfile.Name) + } + + name = pkgname + } + + memFiles = append(memFiles, memfile) + continue // continue } - if name != "" && name != pkgname { - return nil, fmt.Errorf("conflict package name between %q and %q", name, memfile.Name) + if isValidPackageFile(fname) { + memFiles = append(memFiles, &gnovm.MemFile{ + Name: fname, Body: string(body), + }) } - name = pkgname - memFiles = append(memFiles, memfile) + // ignore the file + } + + // Empty package, skipping + if name == "" { + return nil, nil } return &Package{ diff --git a/contribs/gnodev/pkg/packages/resolver.go b/contribs/gnodev/pkg/packages/resolver.go index 95d64e3523d..6bfde26547d 100644 --- a/contribs/gnodev/pkg/packages/resolver.go +++ b/contribs/gnodev/pkg/packages/resolver.go @@ -3,56 +3,21 @@ package packages import ( "errors" "fmt" - "go/parser" - "go/scanner" "go/token" - "log/slog" "strings" - "time" ) -var ErrResolverPackageNotFound = errors.New("package not found") +var ( + ErrResolverPackageNotFound = errors.New("package not found") + ErrResolverPackageSkip = errors.New("package has been skip") +) type Resolver interface { Name() string Resolve(fset *token.FileSet, path string) (*Package, error) } -type logResolver struct { - logger *slog.Logger - Resolver -} - -func LogResolver(l *slog.Logger, r Resolver) Resolver { - return &logResolver{l, r} -} - -func (l logResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { - start := time.Now() - pkg, err := l.Resolver.Resolve(fset, path) - if err == nil { - l.logger.Info("path resolved", - "resolver", l.Resolver.Name(), - "took", time.Since(start).String(), - "path", path, - "name", pkg.Name, - "location", pkg.Location) - } else if errors.Is(err, ErrResolverPackageNotFound) { - l.logger.Debug("path not found", - "resolver", l.Resolver.Name(), - "took", time.Since(start).String(), - "path", path) - - } else { - l.logger.Error("unable to resolve path", - "resolver", l.Resolver.Name(), - "took", time.Since(start).String(), - "path", path, - "err", err) - } - - return pkg, err -} +// Chain Resolver type ChainedResolver []Resolver @@ -60,22 +25,19 @@ func ChainResolvers(rs ...Resolver) Resolver { return ChainedResolver(rs) } -func ChainWithLogger(logger *slog.Logger, rs ...Resolver) Resolver { - loggedResolvers := make([]Resolver, len(rs)) - for i, r := range rs { - loggedResolvers[i] = LogResolver(logger, r) - } - return ChainedResolver(loggedResolvers) -} - func (cr ChainedResolver) Name() string { var name strings.Builder for i, r := range cr { + rname := r.Name() + if rname == "" { + continue + } + if i > 0 { name.WriteRune('/') } - name.WriteString(r.Name()) + name.WriteString(rname) } return name.String() @@ -96,75 +58,101 @@ func (cr ChainedResolver) Resolve(fset *token.FileSet, path string) (*Package, e return nil, ErrResolverPackageNotFound } -type inMemoryCacheResolver struct { - subr Resolver - cacheMap map[string] /* path */ *Package -} - -func Cache(r Resolver) Resolver { - return &inMemoryCacheResolver{ - subr: r, - cacheMap: map[string]*Package{}, - } -} - -func (r *inMemoryCacheResolver) Name() string { - return "cache_" + r.subr.Name() -} - -func (r *inMemoryCacheResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { - if p, ok := r.cacheMap[path]; ok { - return p, nil - } - - p, err := r.subr.Resolve(fset, path) - if err != nil { - return nil, err - } - - r.cacheMap[path] = p - return p, nil -} - -type SyntaxErrorHandler func(path string, filename string, serr *scanner.Error) - -type SyntaxCheckerResolver struct { - SyntaxErrorHandler - Resolver -} - -func SyntaxChecker(r Resolver, handler SyntaxErrorHandler) Resolver { - return &SyntaxCheckerResolver{ - SyntaxErrorHandler: handler, - Resolver: r, - } -} - -func (SyntaxCheckerResolver) Name() string { - return "syntax_checker" -} - -func (r *SyntaxCheckerResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { - p, err := r.Resolver.Resolve(fset, path) - if err != nil { - return nil, err - } - - for _, file := range p.Files { - _, err = parser.ParseFile(fset, file.Name, file.Body, parser.AllErrors) - if err == nil { - continue - } - - if el, ok := err.(scanner.ErrorList); ok { - for _, e := range el { - r.SyntaxErrorHandler(path, file.Name, e) - } - } - - return nil, fmt.Errorf("unable to parse %q: %w", - file.Name, err) - } - - return p, err -} +// // Cache Resolver + +// type inMemoryCacheResolver struct { +// subr Resolver +// cacheMap map[string] /* path */ *Package +// } + +// func Cache(r Resolver) Resolver { +// return &inMemoryCacheResolver{ +// subr: r, +// cacheMap: map[string]*Package{}, +// } +// } + +// func (r *inMemoryCacheResolver) Name() string { +// return "cache_" + r.subr.Name() +// } + +// func (r *inMemoryCacheResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { +// if p, ok := r.cacheMap[path]; ok { +// return p, nil +// } + +// p, err := r.subr.Resolve(fset, path) +// if err != nil { +// return nil, err +// } + +// r.cacheMap[path] = p +// return p, nil +// } + +// // Filter Resolver + +// func NoopFilterFunc(path string) bool { return false } +// func FilterAllFunc(path string) bool { return true } + +// type FilterHandler func(path string) bool + +// type filterResolver struct { +// Resolver +// FilterHandler +// } + +// func FilterResolver(handler FilterHandler, r Resolver) Resolver { +// return &filterResolver{Resolver: r, FilterHandler: handler} +// } + +// func (filterResolver) Name() string { +// return "filter" +// } + +// func (r *filterResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { +// if r.FilterHandler(path) { +// return nil, ErrResolverPackageSkip +// } + +// return r.Resolver.Resolve(fset, path) +// } + +// Utility Resolver + +// // Log Resolver + +// type logResolver struct { +// logger *slog.Logger +// Resolver +// } + +// func LogResolver(l *slog.Logger, r Resolver) Resolver { +// return &logResolver{l, r} +// } + +// func (l logResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { +// start := time.Now() +// pkg, err := l.Resolver.Resolve(fset, path) +// if err == nil { +// l.logger.Debug("path resolved", +// "resolver", l.Resolver.Name(), +// "path", path, +// "name", pkg.Name, +// "took", time.Since(start).String(), +// "location", pkg.Location) +// } else if errors.Is(err, ErrResolverPackageNotFound) { +// l.logger.Warn("path not found", +// "resolver", l.Resolver.Name(), +// "path", path, +// "took", time.Since(start).String()) +// } else { +// l.logger.Error("unable to resolve path", +// "resolver", l.Resolver.Name(), +// "path", path, +// "took", time.Since(start).String(), +// "err", err) +// } + +// return pkg, err +// } diff --git a/contribs/gnodev/pkg/packages/resolver_root.go b/contribs/gnodev/pkg/packages/resolver_fs.go similarity index 53% rename from contribs/gnodev/pkg/packages/resolver_root.go rename to contribs/gnodev/pkg/packages/resolver_fs.go index b168094d01a..2a235dd63d6 100644 --- a/contribs/gnodev/pkg/packages/resolver_root.go +++ b/contribs/gnodev/pkg/packages/resolver_fs.go @@ -7,19 +7,19 @@ import ( "path/filepath" ) -type rootResolver struct { +type fsResolver struct { root string // Root folder } -func (l *rootResolver) Name() string { - return fmt.Sprintf("root<%s>", filepath.Base(l.root)) +func (l *fsResolver) Name() string { + return fmt.Sprintf("fs<%s>", filepath.Base(l.root)) } -func NewRootResolver(rootpath string) Resolver { - return &rootResolver{root: rootpath} +func NewFSResolver(rootpath string) Resolver { + return &fsResolver{root: rootpath} } -func (r *rootResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { +func (r *fsResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { dir := filepath.Join(r.root, path) _, err := os.Stat(dir) if err != nil { diff --git a/contribs/gnodev/pkg/packages/resolver_middleware.go b/contribs/gnodev/pkg/packages/resolver_middleware.go new file mode 100644 index 00000000000..baec36cca38 --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_middleware.go @@ -0,0 +1,168 @@ +package packages + +import ( + "errors" + "fmt" + "go/parser" + "go/scanner" + "go/token" + "log/slog" + "time" +) + +type MiddlewareHandler func(fset *token.FileSet, path string, next Resolver) (*Package, error) + +type middlewareResolver struct { + Handler MiddlewareHandler + Next Resolver +} + +func MiddlewareResolver(r Resolver, handlers ...MiddlewareHandler) Resolver { + // Start with the final resolver + start := r + + // Wrap each handler around the previous one + for _, handler := range handlers { + start = &middlewareResolver{ + Next: start, + Handler: handler, + } + } + + return start +} + +func (r middlewareResolver) Name() string { + return r.Next.Name() +} + +func (r *middlewareResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + if r.Handler != nil { + return r.Handler(fset, path, r.Next) + } + + return r.Next.Resolve(fset, path) +} + +// LogMiddleware creates a logging middleware handler. +func LogMiddleware(logger *slog.Logger) MiddlewareHandler { + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + start := time.Now() + pkg, err := next.Resolve(fset, path) + switch { + case err == nil: + logger.Debug("path resolved", + "path", path, + "name", pkg.Name, + "took", time.Since(start).String(), + "location", pkg.Location, + "resolver", next.Name(), + ) + case errors.Is(err, ErrResolverPackageSkip): + logger.Debug(err.Error(), + "path", path, + "took", time.Since(start).String(), + "resolver", next.Name(), + ) + + case errors.Is(err, ErrResolverPackageNotFound): + logger.Warn(err.Error(), + "path", path, + "took", time.Since(start).String(), + "resolver", next.Name()) + + default: + logger.Error(err.Error(), + "path", path, + "took", time.Since(start).String(), + "resolver", next.Name(), + "err", err) + + } + + return pkg, err + } +} + +type ShouldCacheFunc func(pkg *Package) bool + +func CacheAll(_ *Package) bool { return true } + +// CacheMiddleware creates a caching middleware handler. +func CacheMiddleware(shouldCache ShouldCacheFunc) MiddlewareHandler { + cacheMap := make(map[string]*Package) + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + if pkg, ok := cacheMap[path]; ok { + return pkg, nil + } + + pkg, err := next.Resolve(fset, path) + if err == nil && shouldCache(pkg) { + cacheMap[path] = pkg + } + + return pkg, err + } +} + +// FilterHandler defines the function signature for filter handlers. +type FilterHandler func(path string) bool + +// NoopFilterFunc is a filter that allows all paths. +func NoopFilterFunc(path string) bool { return false } + +// FilterAllFunc is a filter that blocks all paths. +func FilterAllFunc(path string) bool { return true } + +func FilterMiddleware(name string, filter FilterHandler) MiddlewareHandler { + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + if filter(path) { + return nil, fmt.Errorf("filter %q: %w", name, ErrResolverPackageSkip) + } + + return next.Resolve(fset, path) + } +} + +// SyntaxCheckerMiddleware creates a middleware handler for post-processing syntax. +func SyntaxCheckerMiddleware(logger *slog.Logger) MiddlewareHandler { + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + // First, resolve the package using the next resolver in the chain. + pkg, err := next.Resolve(fset, path) + if err != nil { + return nil, err + } + + if err := pkg.Validate(); err != nil { + return nil, fmt.Errorf("invalid package %q: %w", path, err) + } + + // Post-process each file in the package. + for _, file := range pkg.Files { + fname := file.Name + if !isGnoFile(fname) { + continue + } + + logger.Debug("checking syntax", "path", path, "filename", fname) + _, err := parser.ParseFile(fset, file.Name, file.Body, parser.AllErrors) + if err == nil { + continue + } + + if el, ok := err.(scanner.ErrorList); ok { + for _, e := range el { + logger.Error("syntax error", + "path", path, + "filename", fname, + "err", e.Error(), + ) + } + } + + return nil, fmt.Errorf("unable to parse %q: %w", file.Name, err) + } + + return pkg, nil + } +} diff --git a/contribs/gnodev/pkg/packages/resolver_remote.go b/contribs/gnodev/pkg/packages/resolver_remote.go index 123eb514f60..0b34e384dec 100644 --- a/contribs/gnodev/pkg/packages/resolver_remote.go +++ b/contribs/gnodev/pkg/packages/resolver_remote.go @@ -55,11 +55,6 @@ func (res *remoteResolver) Resolve(fset *token.FileSet, path string) (*Package, files := bytes.Split(qres.Response.Data, []byte{'\n'}) for _, filename := range files { fname := string(filename) - - if !isGnoFile(fname) || isTestFile(fname) { - continue - } - fpath := filepath.Join(path, fname) qres, err := res.RPCClient.ABCIQuery(qpath, []byte(fpath)) if err != nil { diff --git a/contribs/gnodev/pkg/packages/utils.go b/contribs/gnodev/pkg/packages/utils.go index 93160a3a1a5..369b1fe230e 100644 --- a/contribs/gnodev/pkg/packages/utils.go +++ b/contribs/gnodev/pkg/packages/utils.go @@ -2,6 +2,7 @@ package packages import ( "path/filepath" + "regexp" "strings" ) @@ -12,3 +13,9 @@ func isGnoFile(name string) bool { func isTestFile(name string) bool { return strings.HasSuffix(name, "_filetest.gno") || strings.HasSuffix(name, "_test.gno") } + +var reFileName = regexp.MustCompile(`^([a-zA-Z0-9_]*\.[a-z0-9_\.]*|LICENSE|README)$`) + +func isValidPackageFile(filename string) bool { + return reFileName.MatchString(filename) +} diff --git a/tm2/pkg/commands/command.go b/tm2/pkg/commands/command.go index aa717b62ad9..ee592c5785d 100644 --- a/tm2/pkg/commands/command.go +++ b/tm2/pkg/commands/command.go @@ -31,26 +31,28 @@ func HelpExec(_ context.Context, _ []string) error { // Metadata contains basic help // information about a command type Metadata struct { - Name string - ShortUsage string - ShortHelp string - LongHelp string - Options []ff.Option + Name string + ShortUsage string + ShortHelp string + LongHelp string + Options []ff.Option + NoParentFlags bool } // Command is a simple wrapper for gnoland commands. type Command struct { - name string - shortUsage string - shortHelp string - longHelp string - options []ff.Option - cfg Config - flagSet *flag.FlagSet - subcommands []*Command - exec ExecMethod - selected *Command - args []string + name string + shortUsage string + shortHelp string + longHelp string + options []ff.Option + cfg Config + flagSet *flag.FlagSet + subcommands []*Command + exec ExecMethod + selected *Command + args []string + noParentFlags bool } func NewCommand( @@ -59,14 +61,15 @@ func NewCommand( exec ExecMethod, ) *Command { command := &Command{ - name: meta.Name, - shortUsage: meta.ShortUsage, - shortHelp: meta.ShortHelp, - longHelp: meta.LongHelp, - options: meta.Options, - flagSet: flag.NewFlagSet(meta.Name, flag.ContinueOnError), - exec: exec, - cfg: config, + name: meta.Name, + shortUsage: meta.ShortUsage, + shortHelp: meta.ShortHelp, + longHelp: meta.LongHelp, + options: meta.Options, + noParentFlags: meta.NoParentFlags, + flagSet: flag.NewFlagSet(meta.Name, flag.ContinueOnError), + exec: exec, + cfg: config, } if config != nil { @@ -81,7 +84,7 @@ func NewCommand( // and registers common flags using the flagset func (c *Command) AddSubCommands(cmds ...*Command) { for _, cmd := range cmds { - if c.cfg != nil { + if c.cfg != nil && !cmd.noParentFlags { // Register the parent flagset with the child. // The syntax is not intuitive, but the flagset being // modified is the subcommand's, using the flags defined From 54c819306b6412a82be8d553982e0898a15584d8 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sun, 15 Dec 2024 20:24:10 +0100 Subject: [PATCH 08/24] feat: staging Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/command_staging.go | 67 ++++++++-- contribs/gnodev/cmd/gnodev/main.go | 18 ++- contribs/gnodev/cmd/gnodev/setup_loader.go | 6 +- contribs/gnodev/cmd/gnodev/setup_node.go | 20 +-- contribs/gnodev/pkg/dev/node.go | 85 +++++++++---- contribs/gnodev/pkg/dev/node_test.go | 4 +- contribs/gnodev/pkg/packages/loader.go | 23 ++-- contribs/gnodev/pkg/packages/loader_glob.go | 85 ++++++++----- contribs/gnodev/pkg/packages/package.go | 52 +++++++- contribs/gnodev/pkg/packages/resolver.go | 115 +++--------------- .../pkg/packages/resolver_middleware.go | 18 +-- contribs/gnodev/pkg/packages/utils.go | 4 + tm2/pkg/commands/command.go | 4 +- 13 files changed, 292 insertions(+), 209 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/command_staging.go b/contribs/gnodev/cmd/gnodev/command_staging.go index d0d0f839875..da47f79f059 100644 --- a/contribs/gnodev/cmd/gnodev/command_staging.go +++ b/contribs/gnodev/cmd/gnodev/command_staging.go @@ -4,11 +4,15 @@ import ( "context" "flag" "fmt" + "net/http" "path/filepath" - "strings" + "time" + "github.com/gnolang/gno/gno.land/pkg/log" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/commands" + osm "github.com/gnolang/gno/tm2/pkg/os" + "go.uber.org/zap/zapcore" ) type stagingCfg struct { @@ -17,6 +21,7 @@ type stagingCfg struct { var defaultStagingOptions = devCfg{ chainId: "staging", + chainDomain: DefaultDomain, maxGas: 10_000_000_000, webListenerAddr: "127.0.0.1:8888", nodeRPCListenerAddr: "127.0.0.1:26657", @@ -25,6 +30,7 @@ var defaultStagingOptions = devCfg{ root: gnoenv.RootDir(), interactive: false, unsafeAPI: false, + paths: varStrings{filepath.Join(DefaultDomain, "/**")}, // Load every package under the main domain}, // As we have no reason to configure this yet, set this to random port // to avoid potential conflict with other app @@ -54,15 +60,60 @@ func (c *stagingCfg) RegisterFlags(fs *flag.FlagSet) { } func execStagingCmd(cfg *stagingCfg, args []string, io commands.IO) error { - if len(args) == 0 { - return fmt.Errorf("no argument given") + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Setup trap signal + osm.TrapSignal(cancel) + + level := zapcore.InfoLevel + if cfg.dev.verbose { + level = zapcore.DebugLevel } - mathches, err := filepath.Glob(args[0]) - if err != nil { - return fmt.Errorf("invalid glob: %w", err) + // Set up the logger + logger := log.ZapLoggerToSlog(log.NewZapJSONLogger(io.Out(), level)) + + // Setup trap signal + devServer := NewApp(ctx, logger, &cfg.dev, io) + if err := devServer.Setup(); err != nil { + return err } - io.Println(strings.Join(mathches, "\n")) - return nil + return devServer.RunServer(ctx) +} + +func (ds *App) RunServer(ctx context.Context) error { + ctx, cancelWith := context.WithCancelCause(ctx) + defer cancelWith(nil) + + addr := ds.cfg.webListenerAddr + + server := &http.Server{ + Handler: ds.setupHandlers(), + Addr: ds.cfg.webListenerAddr, + ReadHeaderTimeout: time.Second * 60, + } + + ds.logger.WithGroup(WebLogName).Info("gnoweb started", "lisn", fmt.Sprintf("http://%s", addr)) + go func() { + err := server.ListenAndServe() + cancelWith(err) + }() + + for { + select { + case <-ctx.Done(): + return context.Cause(ctx) + case _, ok := <-ds.watcher.PackagesUpdate: + if !ok { + return nil + } + ds.logger.WithGroup(NodeLogName).Info("reloading...") + if err := ds.devNode.Reload(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } + ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) + } + } } diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 105441993d5..1c449f753bb 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -24,6 +24,8 @@ import ( osm "github.com/gnolang/gno/tm2/pkg/os" ) +const DefaultDomain = "gno.land" + const ( NodeLogName = "Node" WebLogName = "GnoWeb" @@ -78,12 +80,12 @@ type devCfg struct { chainDomain string unsafeAPI bool interactive bool - loadPath string + paths varStrings } var defaultDevOptions = devCfg{ chainId: "dev", - chainDomain: "gno.land", + chainDomain: DefaultDomain, maxGas: 10_000_000_000, webListenerAddr: "127.0.0.1:8888", nodeRPCListenerAddr: "127.0.0.1:26657", @@ -125,6 +127,7 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { } func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { + *c = defaultCfg fs.StringVar( &c.home, "home", @@ -270,6 +273,12 @@ func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { "enable /reset and /reload endpoints which are not safe to expose publicly", ) + fs.Var( + &c.paths, + "paths", + "additional path load, can be use multiple time to be chained", + ) + // Short flags fs.BoolVar( &c.verbose, @@ -363,8 +372,11 @@ func (ds *App) Setup() error { return fmt.Errorf("unable to guess path from %q", dir) } + // XXX: it would be nice to having this hardcoded + examplesDir := filepath.Join(gnoenv.RootDir(), "examples") + resolver := setupPackagesResolver(ds.logger.WithGroup(LoaderLogName), ds.cfg, path, dir) - loader := packages.NewResolverLoader(resolver) + loader := packages.NewGlobLoader(examplesDir, resolver) ds.book, err = setupAddressBook(ds.logger.WithGroup(AccountsLogName), ds.cfg) if err != nil { diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go index 6f8bd22001f..49012d70fdf 100644 --- a/contribs/gnodev/cmd/gnodev/setup_loader.go +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -65,9 +65,9 @@ func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, path, dir string) p packages.CacheMiddleware(func(pkg *packages.Package) bool { return pkg.Kind == packages.PackageKindRemote // Cache only remote package }), - packages.FilterMiddleware("stdlib", isStdPath), // Filter stdlib package from resolving - packages.SyntaxCheckerMiddleware(logger), // Pre-check syntax to avoid bothering the node reloading on invalid files - packages.LogMiddleware(logger), // Log any request + packages.FilterPathMiddleware("stdlib", isStdPath), // Filter stdlib package from resolving + packages.PackageCheckerMiddleware(logger), // Pre-check syntax to avoid bothering the node reloading on invalid files + packages.LogMiddleware(logger), // Log any request ) } diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index b53638c04c6..9abebdd34fe 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -15,19 +15,21 @@ import ( ) // setupDevNode initializes and returns a new DevNode. -func setupDevNode(ctx context.Context, devCfg *devCfg, nodeConfig *gnodev.NodeConfig, path string) (*gnodev.Node, error) { +func setupDevNode(ctx context.Context, cfg *devCfg, nodeConfig *gnodev.NodeConfig, path string) (*gnodev.Node, error) { + fmt.Printf("PATH: %+v\r\n", cfg.paths.String()) + logger := nodeConfig.Logger - if devCfg.txsFile != "" { // Load txs files + if cfg.txsFile != "" { // Load txs files var err error - nodeConfig.InitialTxs, err = gnoland.ReadGenesisTxs(ctx, devCfg.txsFile) + nodeConfig.InitialTxs, err = gnoland.ReadGenesisTxs(ctx, cfg.txsFile) if err != nil { return nil, fmt.Errorf("unable to load transactions: %w", err) } - } else if devCfg.genesisFile != "" { // Load genesis file - state, err := extractAppStateFromGenesisFile(devCfg.genesisFile) + } else if cfg.genesisFile != "" { // Load genesis file + state, err := extractAppStateFromGenesisFile(cfg.genesisFile) if err != nil { - return nil, fmt.Errorf("unable to load genesis file %q: %w", devCfg.genesisFile, err) + return nil, fmt.Errorf("unable to load genesis file %q: %w", cfg.genesisFile, err) } // Override balances and txs @@ -40,10 +42,12 @@ func setupDevNode(ctx context.Context, devCfg *devCfg, nodeConfig *gnodev.NodeCo nodeConfig.InitialTxs[index] = nodeTx } - logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(stateTxs)) + logger.Info("genesis file loaded", "path", cfg.genesisFile, "txs", len(stateTxs)) } - return gnodev.NewDevNode(ctx, nodeConfig, path) + paths := append(cfg.paths.Strings(), path) + fmt.Println("PATHS:", paths) + return gnodev.NewDevNode(ctx, nodeConfig, paths...) } // setupDevNodeConfig creates and returns a new dev.NodeConfig. diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 969744c763a..af5b05fa70e 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "os" + "path/filepath" "strings" "sync" "time" @@ -17,6 +18,7 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/integration" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/amino" tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" @@ -32,20 +34,52 @@ import ( ) type NodeConfig struct { - Logger *slog.Logger - Loader packages.Loader - DefaultCreator crypto.Address - DefaultDeposit std.Coins - BalancesList []gnoland.Balance - PackagesModifier []PackageModifier - Emitter emitter.Emitter - InitialTxs []gnoland.TxWithMetadata - TMConfig *tmcfg.Config + // Logger is used for logging node activities. It can be set to a custom logger or a noop logger for + // silent operation. + Logger *slog.Logger + + // Loader is responsible for loading packages. It abstracts the mechanism for retrieving and managing + // package data. + Loader packages.Loader + + // DefaultCreator specifies the default address used for creating packages and transactions. + DefaultCreator crypto.Address + + // DefaultDeposit is the default amount of coins deposited when creating a package. + DefaultDeposit std.Coins + + // BalancesList defines the initial balance of accounts in the genesis state. + BalancesList []gnoland.Balance + + // PackagesModifier allows modifications to be applied to packages during initialization. + PackagesModifier []PackageModifier + + // Emitter is used to emit events for various node operations. It can be set to a noop emitter if no + // event emission is required. + Emitter emitter.Emitter + + // InitialTxs contains the transactions that are included in the genesis state. + InitialTxs []gnoland.TxWithMetadata + + // TMConfig holds the Tendermint configuration settings. + TMConfig *tmcfg.Config + + // SkipFailingGenesisTxs indicates whether to skip failing transactions during the genesis + // initialization. SkipFailingGenesisTxs bool - NoReplay bool - MaxGasPerBlock int64 - ChainID string - ChainDomain string + + // NoReplay, if set to true, prevents replaying of transactions from the block store during node + // initialization. + NoReplay bool + + // MaxGasPerBlock sets the maximum amount of gas that can be used in a single block. + MaxGasPerBlock int64 + + // ChainID is the unique identifier for the blockchain. + ChainID string + + // ChainDomain specifies the domain name associated with the blockchain network. + ChainDomain string } func DefaultNodeConfig(rootdir, domain string) *NodeConfig { @@ -62,9 +96,13 @@ func DefaultNodeConfig(rootdir, domain string) *NodeConfig { }, } + exampleFolder := filepath.Join(gnoenv.RootDir(), "example") // XXX: we should avoid having to hardcoding this here + defaultLoader := packages.NewLoader(packages.NewFSResolver(exampleFolder)) + return &NodeConfig{ Logger: log.NewNoopLogger(), Emitter: &emitter.NoopServer{}, + Loader: defaultLoader, DefaultCreator: defaultDeployer, DefaultDeposit: nil, BalancesList: balances, @@ -254,16 +292,6 @@ func (n *Node) ReloadAll(ctx context.Context) error { n.muNode.Lock() defer n.muNode.Unlock() - // pkgs := n.pkgs.toList() - // paths := make([]string, len(pkgs)) - // for i, pkg := range pkgs { - // paths[i] = pkg.Dir - // } - - // if err := n.updatePackages(paths...); err != nil { - // return fmt.Errorf("unable to reload packages: %w", err) - // } - return n.rebuildNodeFromState(ctx) } @@ -332,7 +360,6 @@ func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata state = append(state, txs...) } - // override current state return state, nil } @@ -521,6 +548,16 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { if !res.IsErr() { + for _, msg := range tx.Msgs { + if addpkg, ok := msg.(vm.MsgAddPackage); ok && addpkg.Package != nil { + n.logger.Info("package added", + "path", addpkg.Package.Path, + "files", len(addpkg.Package.Files), + "creator", addpkg.Creator.String(), + ) + } + } + return } diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 4c474da75c7..7f69a92386a 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -62,7 +62,7 @@ func Render(_ string) string { return "foo" } logger := log.NewTestingLogger(t) cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") - cfg.Loader = packages.NewResolverLoader(packages.NewMockResolver(&pkg)) + cfg.Loader = packages.NewLoader(packages.NewMockResolver(&pkg)) cfg.Logger = logger node, err := NewDevNode(ctx, cfg, path) @@ -475,7 +475,7 @@ func generateMemPackage(t *testing.T, path string, pairNameFile ...string) gnovm } func newTestingNodeConfig(pkgs ...*gnovm.MemPackage) *NodeConfig { - var loader packages.ResolverLoader + var loader packages.BaseLoader loader.Resolver = packages.NewMockResolver(pkgs...) cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") cfg.Loader = &loader diff --git a/contribs/gnodev/pkg/packages/loader.go b/contribs/gnodev/pkg/packages/loader.go index 02b2b97bbae..b281e9058ae 100644 --- a/contribs/gnodev/pkg/packages/loader.go +++ b/contribs/gnodev/pkg/packages/loader.go @@ -13,29 +13,20 @@ type Loader interface { Load(paths ...string) ([]Package, error) } -type ResolverLoader struct { +type BaseLoader struct { Resolver } -func NewResolverLoader(res ...Resolver) *ResolverLoader { - var loader ResolverLoader - switch len(res) { - case 0: // Skip - case 1: - loader.Resolver = res[0] - default: - loader.Resolver = ChainResolvers(res...) - } - - return &loader +func NewLoader(res ...Resolver) *BaseLoader { + return &BaseLoader{ChainResolvers(res...)} } -func (l ResolverLoader) Load(paths ...string) ([]Package, error) { +func (l BaseLoader) Load(paths ...string) ([]Package, error) { fset := token.NewFileSet() visited, stack := map[string]bool{}, map[string]bool{} pkgs := make([]Package, 0) for _, root := range paths { - deps, err := loadPackage(root, fset, l.Resolver, visited, stack) + deps, err := load(root, fset, l.Resolver, visited, stack) if err != nil { return nil, err } @@ -45,7 +36,7 @@ func (l ResolverLoader) Load(paths ...string) ([]Package, error) { return pkgs, nil } -func loadPackage(path string, fset *token.FileSet, resolver Resolver, visited, stack map[string]bool) ([]Package, error) { +func load(path string, fset *token.FileSet, resolver Resolver, visited, stack map[string]bool) ([]Package, error) { if stack[path] { return nil, fmt.Errorf("cycle detected: %s", path) } @@ -95,7 +86,7 @@ func loadPackage(path string, fset *token.FileSet, resolver Resolver, visited, s pkgs := []Package{} for imp := range imports { - subDeps, err := loadPackage(imp, fset, resolver, visited, stack) + subDeps, err := load(imp, fset, resolver, visited, stack) if err != nil { return nil, fmt.Errorf("importing %q: %w", imp, err) } diff --git a/contribs/gnodev/pkg/packages/loader_glob.go b/contribs/gnodev/pkg/packages/loader_glob.go index a2cd8384555..1cc972fa9ba 100644 --- a/contribs/gnodev/pkg/packages/loader_glob.go +++ b/contribs/gnodev/pkg/packages/loader_glob.go @@ -2,54 +2,77 @@ package packages import ( "fmt" - "go/token" + "io/fs" + "os" "path/filepath" "strings" ) type GlobLoader struct { - Resolver Resolver Root string + Resolver Resolver } -func NewGlobResolverLoader(rootpath string, res ...Resolver) Loader { - loader := GlobLoader{Root: rootpath} - switch len(res) { - case 0: // Skip - case 1: - loader.Resolver = res[0] - default: - loader.Resolver = ChainResolvers(res...) - } - - return &loader +func NewGlobLoader(rootpath string, res ...Resolver) *GlobLoader { + return &GlobLoader{rootpath, ChainResolvers(res...)} } -func (l GlobLoader) Load(paths ...string) ([]Package, error) { - fset := token.NewFileSet() - visited, stack := map[string]bool{}, map[string]bool{} - pkgs := make([]Package, 0) - for _, path := range paths { - // format path to match directory from loader `Root` - path = filepath.Clean(filepath.Join(l.Root, path) + "/") +func (l GlobLoader) MatchPaths(globs ...string) ([]string, error) { + if l.Root == "" { + return globs, nil + } + + if _, err := os.Stat(l.Root); err != nil { + return nil, fmt.Errorf("unable to stats: %w", err) + } - matches, err := filepath.Glob(path) + mpaths := []string{} + for _, input := range globs { + cleanInputs := filepath.Clean(input) + gpath, err := Parse(cleanInputs) if err != nil { - return nil, fmt.Errorf("invalid glob path: %w", err) + return nil, fmt.Errorf("invalid glob path %q: %w", input, err) } - for _, match := range matches { - // extract path - mpath, _ := strings.CutPrefix(match, l.Root) - mpath = strings.Trim(mpath, "/") + base := gpath.StarFreeBase() + if base == cleanInputs { + mpaths = append(mpaths, base) + continue + } - deps, err := loadPackage(mpath, fset, l.Resolver, visited, stack) + root := filepath.Join(l.Root, base) + err = filepath.WalkDir(root, func(dirpath string, d fs.DirEntry, err error) error { if err != nil { - return nil, err + return err } - pkgs = append(pkgs, deps...) - } + + if !d.IsDir() { + return nil + } + + if strings.HasPrefix(d.Name(), ".") { + return fs.SkipDir + } + + path := strings.TrimPrefix(dirpath, l.Root+"/") + if gpath.Match(path) { + mpaths = append(mpaths, path) + return nil + } + + return nil + }) + } + + return mpaths, nil +} + +func (l GlobLoader) Load(gpaths ...string) ([]Package, error) { + paths, err := l.MatchPaths(gpaths...) + if err != nil { + return nil, fmt.Errorf("unable to resolve dir paths: %w", err) } - return pkgs, nil + loader := &BaseLoader{Resolver: l.Resolver} + return loader.Load(paths...) } diff --git a/contribs/gnodev/pkg/packages/package.go b/contribs/gnodev/pkg/packages/package.go index 3d733d54af1..d5717e5eba5 100644 --- a/contribs/gnodev/pkg/packages/package.go +++ b/contribs/gnodev/pkg/packages/package.go @@ -4,10 +4,12 @@ import ( "fmt" "go/parser" "go/token" + "io/fs" "os" "path/filepath" "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnomod" ) type PackageKind int @@ -30,6 +32,18 @@ func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) return nil, fmt.Errorf("unable to read dir %q: %w", dir, err) } + draft, err := isDraftPackages(dir, files) + if err != nil { + return nil, err + } + + // Skip draft package + // XXX: We could potentially do that in a middleware, but doing this + // here avoid to potentially parse broken files + if draft { + return nil, ErrResolverPackageSkip + } + var name string memFiles := []*gnovm.MemFile{} for _, file := range files { @@ -44,6 +58,18 @@ func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) return nil, fmt.Errorf("unable to read file %q: %w", filepath, err) } + if isModFile(fname) { + file, err := gnomod.Parse(fname, body) + if err != nil { + return nil, fmt.Errorf("unable to read `gno.mod`: %w", err) + } + + // Skip draft package + if file.Draft { + return nil, ErrResolverPackageSkip + } + } + if isGnoFile(fname) { memfile, pkgname, err := parseFile(fset, fname, body) if err != nil { @@ -73,7 +99,7 @@ func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) // Empty package, skipping if name == "" { - return nil, nil + return nil, fmt.Errorf("empty package: %w", ErrResolverPackageSkip) } return &Package{ @@ -87,6 +113,30 @@ func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) }, nil } +func isDraftPackages(dir string, files []fs.DirEntry) (bool, error) { + for _, file := range files { + fname := file.Name() + if !isModFile(fname) { + continue + } + + filepath := filepath.Join(dir, fname) + body, err := os.ReadFile(filepath) + if err != nil { + return false, fmt.Errorf("unable to read file %q: %w", filepath, err) + } + + mod, err := gnomod.Parse(fname, body) + if err != nil { + return false, fmt.Errorf("unable to parse `gno.mod`: %w", err) + } + + return mod.Draft, nil + } + + return false, nil +} + func parseFile(fset *token.FileSet, fname string, body []byte) (*gnovm.MemFile, string, error) { f, err := parser.ParseFile(fset, fname, body, parser.PackageClauseOnly) if err != nil { diff --git a/contribs/gnodev/pkg/packages/resolver.go b/contribs/gnodev/pkg/packages/resolver.go index 6bfde26547d..ea3bf19a633 100644 --- a/contribs/gnodev/pkg/packages/resolver.go +++ b/contribs/gnodev/pkg/packages/resolver.go @@ -17,12 +17,26 @@ type Resolver interface { Resolve(fset *token.FileSet, path string) (*Package, error) } +type NoopResolver struct{} + +func (NoopResolver) Name() string { return "" } +func (NoopResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + return nil, ErrResolverPackageNotFound +} + // Chain Resolver type ChainedResolver []Resolver func ChainResolvers(rs ...Resolver) Resolver { - return ChainedResolver(rs) + switch len(rs) { + case 0: + return &NoopResolver{} + case 1: + return rs[0] + default: + return ChainedResolver(rs) + } } func (cr ChainedResolver) Name() string { @@ -57,102 +71,3 @@ func (cr ChainedResolver) Resolve(fset *token.FileSet, path string) (*Package, e return nil, ErrResolverPackageNotFound } - -// // Cache Resolver - -// type inMemoryCacheResolver struct { -// subr Resolver -// cacheMap map[string] /* path */ *Package -// } - -// func Cache(r Resolver) Resolver { -// return &inMemoryCacheResolver{ -// subr: r, -// cacheMap: map[string]*Package{}, -// } -// } - -// func (r *inMemoryCacheResolver) Name() string { -// return "cache_" + r.subr.Name() -// } - -// func (r *inMemoryCacheResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { -// if p, ok := r.cacheMap[path]; ok { -// return p, nil -// } - -// p, err := r.subr.Resolve(fset, path) -// if err != nil { -// return nil, err -// } - -// r.cacheMap[path] = p -// return p, nil -// } - -// // Filter Resolver - -// func NoopFilterFunc(path string) bool { return false } -// func FilterAllFunc(path string) bool { return true } - -// type FilterHandler func(path string) bool - -// type filterResolver struct { -// Resolver -// FilterHandler -// } - -// func FilterResolver(handler FilterHandler, r Resolver) Resolver { -// return &filterResolver{Resolver: r, FilterHandler: handler} -// } - -// func (filterResolver) Name() string { -// return "filter" -// } - -// func (r *filterResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { -// if r.FilterHandler(path) { -// return nil, ErrResolverPackageSkip -// } - -// return r.Resolver.Resolve(fset, path) -// } - -// Utility Resolver - -// // Log Resolver - -// type logResolver struct { -// logger *slog.Logger -// Resolver -// } - -// func LogResolver(l *slog.Logger, r Resolver) Resolver { -// return &logResolver{l, r} -// } - -// func (l logResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { -// start := time.Now() -// pkg, err := l.Resolver.Resolve(fset, path) -// if err == nil { -// l.logger.Debug("path resolved", -// "resolver", l.Resolver.Name(), -// "path", path, -// "name", pkg.Name, -// "took", time.Since(start).String(), -// "location", pkg.Location) -// } else if errors.Is(err, ErrResolverPackageNotFound) { -// l.logger.Warn("path not found", -// "resolver", l.Resolver.Name(), -// "path", path, -// "took", time.Since(start).String()) -// } else { -// l.logger.Error("unable to resolve path", -// "resolver", l.Resolver.Name(), -// "path", path, -// "took", time.Since(start).String(), -// "err", err) -// } - -// return pkg, err -// } diff --git a/contribs/gnodev/pkg/packages/resolver_middleware.go b/contribs/gnodev/pkg/packages/resolver_middleware.go index baec36cca38..dce2539bfc2 100644 --- a/contribs/gnodev/pkg/packages/resolver_middleware.go +++ b/contribs/gnodev/pkg/packages/resolver_middleware.go @@ -97,7 +97,7 @@ func CacheMiddleware(shouldCache ShouldCacheFunc) MiddlewareHandler { } pkg, err := next.Resolve(fset, path) - if err == nil && shouldCache(pkg) { + if pkg != nil && shouldCache(pkg) { cacheMap[path] = pkg } @@ -105,16 +105,10 @@ func CacheMiddleware(shouldCache ShouldCacheFunc) MiddlewareHandler { } } -// FilterHandler defines the function signature for filter handlers. -type FilterHandler func(path string) bool +// FilterPathHandler defines the function signature for filter handlers. +type FilterPathHandler func(path string) bool -// NoopFilterFunc is a filter that allows all paths. -func NoopFilterFunc(path string) bool { return false } - -// FilterAllFunc is a filter that blocks all paths. -func FilterAllFunc(path string) bool { return true } - -func FilterMiddleware(name string, filter FilterHandler) MiddlewareHandler { +func FilterPathMiddleware(name string, filter FilterPathHandler) MiddlewareHandler { return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { if filter(path) { return nil, fmt.Errorf("filter %q: %w", name, ErrResolverPackageSkip) @@ -124,8 +118,8 @@ func FilterMiddleware(name string, filter FilterHandler) MiddlewareHandler { } } -// SyntaxCheckerMiddleware creates a middleware handler for post-processing syntax. -func SyntaxCheckerMiddleware(logger *slog.Logger) MiddlewareHandler { +// PackageCheckerMiddleware creates a middleware handler for post-processing syntax. +func PackageCheckerMiddleware(logger *slog.Logger) MiddlewareHandler { return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { // First, resolve the package using the next resolver in the chain. pkg, err := next.Resolve(fset, path) diff --git a/contribs/gnodev/pkg/packages/utils.go b/contribs/gnodev/pkg/packages/utils.go index 369b1fe230e..a4f67feae98 100644 --- a/contribs/gnodev/pkg/packages/utils.go +++ b/contribs/gnodev/pkg/packages/utils.go @@ -6,6 +6,10 @@ import ( "strings" ) +func isModFile(name string) bool { + return strings.ToLower(name) == "gno.mod" && !strings.HasPrefix(name, ".") +} + func isGnoFile(name string) bool { return filepath.Ext(name) == ".gno" && !strings.HasPrefix(name, ".") } diff --git a/tm2/pkg/commands/command.go b/tm2/pkg/commands/command.go index ee592c5785d..fffa526f1b5 100644 --- a/tm2/pkg/commands/command.go +++ b/tm2/pkg/commands/command.go @@ -302,7 +302,9 @@ func usage(c *Command) string { def := f.DefValue if def == "" { - def = "..." + if def := f.Value.String(); def == "" { + def = "..." + } } fmt.Fprintf(tw, " -%s%s%s\t%s\n", f.Name, space, def, f.Usage) From ec51f7876377e67c6bf692b03a28c3f635edb2e1 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sun, 15 Dec 2024 23:10:17 +0100 Subject: [PATCH 09/24] feat: gnodev app Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/app.go | 339 ++++++++++++++++ contribs/gnodev/cmd/gnodev/command_staging.go | 113 +++--- contribs/gnodev/cmd/gnodev/logger.go | 53 ++- contribs/gnodev/cmd/gnodev/main.go | 383 +++--------------- contribs/gnodev/cmd/gnodev/setup_loader.go | 11 +- contribs/gnodev/cmd/gnodev/setup_node.go | 3 - contribs/gnodev/cmd/gnodev/utils.go | 25 ++ contribs/gnodev/pkg/logger/log_column.go | 28 +- contribs/gnodev/pkg/packages/glob.go | 210 ++++++++++ contribs/gnodev/pkg/packages/glob_test.go | 88 ++++ .../gnodev/pkg/packages/resolver_local.go | 8 +- 11 files changed, 829 insertions(+), 432 deletions(-) create mode 100644 contribs/gnodev/cmd/gnodev/app.go create mode 100644 contribs/gnodev/cmd/gnodev/utils.go create mode 100644 contribs/gnodev/pkg/packages/glob.go create mode 100644 contribs/gnodev/pkg/packages/glob_test.go diff --git a/contribs/gnodev/cmd/gnodev/app.go b/contribs/gnodev/cmd/gnodev/app.go new file mode 100644 index 00000000000..1bd5dd3c8a2 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/app.go @@ -0,0 +1,339 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gnolang/gno/contribs/gnodev/pkg/address" + gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" + "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type App struct { + ctx context.Context + cfg *devCfg + io commands.IO + logger *slog.Logger + + devNode *gnodev.Node + server *http.Server + emitterServer *emitter.Server + watcher *watcher.PackageWatcher + book *address.Book + exportPath string + + // XXX: move this + exported uint +} + +func NewApp(ctx context.Context, logger *slog.Logger, cfg *devCfg, io commands.IO) *App { + return &App{ + ctx: ctx, + logger: logger, + cfg: cfg, + io: io, + } +} + +func (ds *App) Setup() error { + if err := ds.cfg.validateConfigFlags(); err != nil { + return fmt.Errorf("validate error: %w", err) + } + + if ds.cfg.chdir != "" { + if err := os.Chdir(ds.cfg.chdir); err != nil { + return fmt.Errorf("unable to change directory: %w", err) + } + } + + loggerEvents := ds.logger.WithGroup(EventServerLogName) + ds.emitterServer = emitter.NewServer(loggerEvents) + + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("unable to guess current dir: %w", err) + } + + path := guessPath(ds.cfg, dir) + ds.logger.WithGroup(LoaderLogName).Info("realm path", "path", path, "dir", dir) + + // XXX: it would be nice to having this hardcoded + examplesDir := filepath.Join(ds.cfg.root, "examples") + + resolver := setupPackagesResolver(ds.logger.WithGroup(LoaderLogName), ds.cfg, path, dir) + loader := packages.NewGlobLoader(examplesDir, resolver) + + ds.book, err = setupAddressBook(ds.logger.WithGroup(AccountsLogName), ds.cfg) + if err != nil { + return fmt.Errorf("unable to load keybase: %w", err) + } + + balances, err := generateBalances(ds.book, ds.cfg) + if err != nil { + return fmt.Errorf("unable to generate balances: %w", err) + } + ds.logger.Debug("balances loaded", "list", balances.List()) + + nodeLogger := ds.logger.WithGroup(NodeLogName) + nodeCfg := setupDevNodeConfig(ds.cfg, nodeLogger, ds.emitterServer, balances, loader) + ds.devNode, err = setupDevNode(ds.ctx, ds.cfg, nodeCfg, path) + if err != nil { + return err + } + + ds.watcher, err = watcher.NewPackageWatcher(loggerEvents, ds.emitterServer) + if err != nil { + return fmt.Errorf("unable to setup packages watcher: %w", err) + } + + ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) + + return nil +} + +func (ds *App) setupHandlers() http.Handler { + mux := http.NewServeMux() + webhandler := setupGnoWebServer(ds.logger.WithGroup(WebLogName), ds.cfg, ds.devNode) + + // Setup unsage api + if ds.cfg.unsafeAPI { + mux.HandleFunc("/reset", func(res http.ResponseWriter, req *http.Request) { + if err := ds.devNode.Reset(req.Context()); err != nil { + ds.logger.Error("failed to reset", slog.Any("err", err)) + res.WriteHeader(http.StatusInternalServerError) + } + }) + + mux.HandleFunc("/reload", func(res http.ResponseWriter, req *http.Request) { + if err := ds.devNode.Reload(req.Context()); err != nil { + ds.logger.Error("failed to reload", slog.Any("err", err)) + res.WriteHeader(http.StatusInternalServerError) + } + }) + } + + if !ds.cfg.noWatch { + evtstarget := fmt.Sprintf("%s/_events", ds.cfg.webListenerAddr) + mux.Handle("/_events", ds.emitterServer) + mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler)) + } else { + mux.Handle("/", webhandler) + } + + return mux +} + +func (ds *App) RunServer(term *rawterm.RawTerm) error { + ctx, cancelWith := context.WithCancelCause(ds.ctx) + defer cancelWith(nil) + + addr := ds.cfg.webListenerAddr + ds.logger.WithGroup(WebLogName).Info("gnoweb started", "lisn", fmt.Sprintf("http://%s", addr)) + + server := &http.Server{ + Handler: ds.setupHandlers(), + Addr: ds.cfg.webListenerAddr, + ReadHeaderTimeout: time.Second * 60, + } + + go func() { + err := server.ListenAndServe() + cancelWith(err) + }() + + if ds.cfg.interactive { + ds.logger.WithGroup("--- READY").Info("for commands and help, press `h`") + } else { + ds.logger.Info("node is ready") + } + + for { + select { + case <-ctx.Done(): + return context.Cause(ctx) + case _, ok := <-ds.watcher.PackagesUpdate: + if !ok { + return nil + } + ds.logger.WithGroup(NodeLogName).Info("reloading...") + if err := ds.devNode.Reload(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } + ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) + } + } +} + +func (ds *App) RunInteractive(term *rawterm.RawTerm) { + var keyPressCh <-chan rawterm.KeyPress + if ds.cfg.interactive { + keyPressCh = listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) + } + + for { + select { + case <-ds.ctx.Done(): + case key, ok := <-keyPressCh: + if !ok { + return + } + + if key == rawterm.KeyCtrlC { + return + } + + ds.handleKeyPress(key) + keyPressCh = listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) + } + } +} + +var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: +https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev + +P Previous TX - Go to the previous tx +N Next TX - Go to the next tx +E Export - Export the current state as genesis doc +A Accounts - Display known accounts and balances +H Help - Display this message +R Reload - Reload all packages to take change into account. +Ctrl+S Save State - Save the current state +Ctrl+R Reset - Reset application to it's initial/save state. +Ctrl+C Exit - Exit the application +` + +func (ds *App) handleKeyPress(key rawterm.KeyPress) { + var err error + ds.logger.WithGroup(KeyPressLogName).Debug(fmt.Sprintf("<%s>", key.String())) + + switch key.Upper() { + case rawterm.KeyH: // Helper + ds.logger.Info("Gno Dev Helper", "helper", helper) + + case rawterm.KeyA: // Accounts + logAccounts(ds.logger.WithGroup(AccountsLogName), ds.book, ds.devNode) + + case rawterm.KeyR: // Reload + ds.logger.WithGroup(NodeLogName).Info("reloading...") + if err = ds.devNode.ReloadAll(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } + + case rawterm.KeyCtrlR: // Reset + ds.logger.WithGroup(NodeLogName).Info("reseting node state...") + if err = ds.devNode.Reset(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reset node state", "err", err) + } + + case rawterm.KeyCtrlS: // Save + ds.logger.WithGroup(NodeLogName).Info("saving state...") + if err := ds.devNode.SaveCurrentState(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to save node state", "err", err) + } + + case rawterm.KeyE: // Export + // Create a temporary export dir + if ds.exported == 0 { + ds.exportPath, err = os.MkdirTemp("", "gnodev-export") + if err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to create `export` directory", "err", err) + return + } + } + ds.exported++ + + ds.logger.WithGroup(NodeLogName).Info("exporting state...") + doc, err := ds.devNode.ExportStateAsGenesis(ds.ctx) + if err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to export node state", "err", err) + return + } + + docfile := filepath.Join(ds.exportPath, fmt.Sprintf("export_%d.jsonl", ds.exported)) + if err := doc.SaveAs(docfile); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to save genesis", "err", err) + } + + ds.logger.WithGroup(NodeLogName).Info("node state exported", "file", docfile) + + case rawterm.KeyN: // Next tx + ds.logger.Info("moving forward...") + if err := ds.devNode.MoveToNextTX(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to move forward", "err", err) + } + + case rawterm.KeyP: // Previous tx + ds.logger.Info("moving backward...") + if err := ds.devNode.MoveToPreviousTX(ds.ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to move backward", "err", err) + } + default: + } +} + +func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { + cc := make(chan rawterm.KeyPress, 1) + go func() { + defer close(cc) + key, err := rt.ReadKeyPress() + if err != nil { + logger.Error("unable to read keypress", "err", err) + return + } + + cc <- key + }() + + return cc +} + +func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ([]gnodev.PackageModifier, error) { + modifiers := make([]gnodev.PackageModifier, 0, len(args)) + + if cfg.deployKey == "" { + return nil, fmt.Errorf("default deploy key cannot be empty") + } + + defaultKey, _, ok := bk.GetFromNameOrAddress(cfg.deployKey) + if !ok { + return nil, fmt.Errorf("unable to get deploy key %q", cfg.deployKey) + } + + if len(args) == 0 { + args = append(args, ".") // add current dir if none are provided + } + + for _, arg := range args { + path, err := gnodev.ResolvePackageModifierQuery(bk, arg) + if err != nil { + return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) + } + + // Assign a default creator if user haven't specified it. + if path.Creator.IsZero() { + path.Creator = defaultKey + } + + modifiers = append(modifiers, path) + } + + // Add examples folder if minimal is set to false + if cfg.minimal { + modifiers = append(modifiers, gnodev.PackageModifier{ + Path: filepath.Join(cfg.root, "examples"), + Creator: defaultKey, + Deposit: nil, + }) + } + + return modifiers, nil +} diff --git a/contribs/gnodev/cmd/gnodev/command_staging.go b/contribs/gnodev/cmd/gnodev/command_staging.go index da47f79f059..92ba6de7e5f 100644 --- a/contribs/gnodev/cmd/gnodev/command_staging.go +++ b/contribs/gnodev/cmd/gnodev/command_staging.go @@ -3,16 +3,10 @@ package main import ( "context" "flag" - "fmt" - "net/http" "path/filepath" - "time" - "github.com/gnolang/gno/gno.land/pkg/log" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/commands" - osm "github.com/gnolang/gno/tm2/pkg/os" - "go.uber.org/zap/zapcore" ) type stagingCfg struct { @@ -22,6 +16,7 @@ type stagingCfg struct { var defaultStagingOptions = devCfg{ chainId: "staging", chainDomain: DefaultDomain, + logFormat: "json", maxGas: 10_000_000_000, webListenerAddr: "127.0.0.1:8888", nodeRPCListenerAddr: "127.0.0.1:26657", @@ -44,8 +39,9 @@ func NewStagingCmd(io commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "staging", - ShortUsage: "gnodev staging [flags] ", - ShortHelp: "start gnodev in staging mode", + ShortUsage: "gnodev staging [flags]", + ShortHelp: "Start gnodev in staging mode", + LongHelp: "STAGING: Staging mode configure the node for server usage", NoParentFlags: true, }, &cfg, @@ -60,60 +56,61 @@ func (c *stagingCfg) RegisterFlags(fs *flag.FlagSet) { } func execStagingCmd(cfg *stagingCfg, args []string, io commands.IO) error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + return execDev(&cfg.dev, args, io) + // ctx, cancel := context.WithCancel(context.Background()) + // defer cancel() - // Setup trap signal - osm.TrapSignal(cancel) + // // Setup trap signal + // osm.TrapSignal(cancel) - level := zapcore.InfoLevel - if cfg.dev.verbose { - level = zapcore.DebugLevel - } + // level := zapcore.InfoLevel + // if cfg.dev.verbose { + // level = zapcore.DebugLevel + // } - // Set up the logger - logger := log.ZapLoggerToSlog(log.NewZapJSONLogger(io.Out(), level)) + // // Set up the logger + // logger := log.ZapLoggerToSlog(log.NewZapJSONLogger(io.Out(), level)) - // Setup trap signal - devServer := NewApp(ctx, logger, &cfg.dev, io) - if err := devServer.Setup(); err != nil { - return err - } + // // Setup trap signal + // devServer := NewApp(ctx, logger, &cfg.dev, io) + // if err := devServer.Setup(); err != nil { + // return err + // } - return devServer.RunServer(ctx) + // return devServer.RunServer(ctx) } -func (ds *App) RunServer(ctx context.Context) error { - ctx, cancelWith := context.WithCancelCause(ctx) - defer cancelWith(nil) - - addr := ds.cfg.webListenerAddr - - server := &http.Server{ - Handler: ds.setupHandlers(), - Addr: ds.cfg.webListenerAddr, - ReadHeaderTimeout: time.Second * 60, - } - - ds.logger.WithGroup(WebLogName).Info("gnoweb started", "lisn", fmt.Sprintf("http://%s", addr)) - go func() { - err := server.ListenAndServe() - cancelWith(err) - }() - - for { - select { - case <-ctx.Done(): - return context.Cause(ctx) - case _, ok := <-ds.watcher.PackagesUpdate: - if !ok { - return nil - } - ds.logger.WithGroup(NodeLogName).Info("reloading...") - if err := ds.devNode.Reload(ds.ctx); err != nil { - ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) - } - ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) - } - } -} +// func (ds *App) RunServer(ctx context.Context) error { +// ctx, cancelWith := context.WithCancelCause(ctx) +// defer cancelWith(nil) + +// addr := ds.cfg.webListenerAddr + +// server := &http.Server{ +// Handler: ds.setupHandlers(), +// Addr: ds.cfg.webListenerAddr, +// ReadHeaderTimeout: time.Second * 60, +// } + +// ds.logger.WithGroup(WebLogName).Info("gnoweb started", "lisn", fmt.Sprintf("http://%s", addr)) +// go func() { +// err := server.ListenAndServe() +// cancelWith(err) +// }() + +// for { +// select { +// case <-ctx.Done(): +// return context.Cause(ctx) +// case _, ok := <-ds.watcher.PackagesUpdate: +// if !ok { +// return nil +// } +// ds.logger.WithGroup(NodeLogName).Info("reloading...") +// if err := ds.devNode.Reload(ds.ctx); err != nil { +// ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) +// } +// ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) +// } +// } +// } diff --git a/contribs/gnodev/cmd/gnodev/logger.go b/contribs/gnodev/cmd/gnodev/logger.go index 8ed942700db..59d73c8abe9 100644 --- a/contribs/gnodev/cmd/gnodev/logger.go +++ b/contribs/gnodev/cmd/gnodev/logger.go @@ -1,34 +1,59 @@ package main import ( + "fmt" "io" "log/slog" "github.com/charmbracelet/lipgloss" "github.com/gnolang/gno/contribs/gnodev/pkg/logger" + "github.com/gnolang/gno/gno.land/pkg/log" "github.com/muesli/termenv" + "go.uber.org/zap/zapcore" ) -func setuplogger(cfg *devCfg, out io.Writer) *slog.Logger { +func setuplogger(cfg *devCfg, out io.Writer) (*slog.Logger, error) { level := slog.LevelInfo if cfg.verbose { level = slog.LevelDebug } - // if cfg.serverMode { - // zaplogger := logger.NewZapLogger(out, level) - // return gnolog.ZapLoggerToSlog(zaplogger) - // } + // Set up the logger + switch cfg.logFormat { + case "json": + return newJSONLogger(out, level), nil + case "console", "": + // Detect term color profile + colorProfile := termenv.DefaultOutput().Profile - // Detect term color profile - colorProfile := termenv.DefaultOutput().Profile - clogger := logger.NewColumnLogger(out, level, colorProfile) + clogger := logger.NewColumnLogger(out, level, colorProfile) - // Register well known group color with system colors - clogger.RegisterGroupColor(NodeLogName, lipgloss.Color("3")) - clogger.RegisterGroupColor(WebLogName, lipgloss.Color("4")) - clogger.RegisterGroupColor(KeyPressLogName, lipgloss.Color("5")) - clogger.RegisterGroupColor(EventServerLogName, lipgloss.Color("6")) + // Register well known group color with system colors + clogger.RegisterGroupColor(NodeLogName, lipgloss.Color("3")) + clogger.RegisterGroupColor(WebLogName, lipgloss.Color("4")) + clogger.RegisterGroupColor(KeyPressLogName, lipgloss.Color("5")) + clogger.RegisterGroupColor(EventServerLogName, lipgloss.Color("6")) - return slog.New(clogger) + return slog.New(clogger), nil + default: + return nil, fmt.Errorf("invalid log format %q", cfg.logFormat) + } +} + +func newJSONLogger(w io.Writer, level slog.Level) *slog.Logger { + var zaplevel zapcore.Level + switch level { + case slog.LevelDebug: + zaplevel = zapcore.DebugLevel + case slog.LevelInfo: + zaplevel = zapcore.InfoLevel + case slog.LevelWarn: + zaplevel = zapcore.WarnLevel + case slog.LevelError: + zaplevel = zapcore.ErrorLevel + default: + panic("unknown slog level: " + level.String()) + } + + return log.ZapLoggerToSlog(log.NewZapJSONLogger(w, zaplevel)) } diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 1c449f753bb..f5417ec3439 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -5,18 +5,10 @@ import ( "errors" "flag" "fmt" - "log/slog" - "net/http" + "io" "os" - "path/filepath" - "time" - "github.com/gnolang/gno/contribs/gnodev/pkg/address" - gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" - "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" - "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" - "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" "github.com/gnolang/gno/gno.land/pkg/integration" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/commands" @@ -26,6 +18,12 @@ import ( const DefaultDomain = "gno.land" +var ( + DefaultDeployerName = integration.DefaultAccount_Name + DefaultDeployerAddress = crypto.MustAddressFromString(integration.DefaultAccount_Address) + DefaultDeployerSeed = integration.DefaultAccount_Seed +) + const ( NodeLogName = "Node" WebLogName = "GnoWeb" @@ -37,14 +35,9 @@ const ( var ErrConflictingFileArgs = errors.New("cannot specify `balances-file` or `txs-file` along with `genesis-file`") -var ( - DefaultDeployerName = integration.DefaultAccount_Name - DefaultDeployerAddress = crypto.MustAddressFromString(integration.DefaultAccount_Address) - DefaultDeployerSeed = integration.DefaultAccount_Seed -) - type devCfg struct { - chdir string + chdir string + rootPath string // Listeners nodeRPCListenerAddr string @@ -71,6 +64,7 @@ type devCfg struct { resolvers varResolver // Node Configuration + logFormat string minimal bool verbose bool noWatch bool @@ -85,6 +79,7 @@ type devCfg struct { var defaultDevOptions = devCfg{ chainId: "dev", + logFormat: "console", chainDomain: DefaultDomain, maxGas: 10_000_000_000, webListenerAddr: "127.0.0.1:8888", @@ -108,8 +103,8 @@ func main() { cmd := commands.NewCommand( commands.Metadata{ Name: "gnodev", - ShortUsage: "gnodev [flags] [path ...]", - ShortHelp: "runs an in-memory node and gno.land web server for development purposes.", + ShortUsage: "gnodev [flags] ", + ShortHelp: "Runs an in-memory node and gno.land web server for development purposes.", LongHelp: `The gnodev command starts an in-memory node and a gno.land web interface primarily for realm package development. It automatically loads the 'examples' directory and any additional specified paths.`, }, cfg, @@ -127,7 +122,8 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { } func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { - *c = defaultCfg + *c = defaultCfg // Copy default config + fs.StringVar( &c.home, "home", @@ -273,6 +269,13 @@ func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { "enable /reset and /reload endpoints which are not safe to expose publicly", ) + fs.StringVar( + &c.logFormat, + "log-format", + defaultCfg.logFormat, + "log output format, can be `json` or `console`", + ) + fs.Var( &c.paths, "paths", @@ -296,338 +299,48 @@ func (c *devCfg) validateConfigFlags() error { return nil } -type App struct { - ctx context.Context - cfg *devCfg - io commands.IO - logger *slog.Logger - - devNode *gnodev.Node - server *http.Server - emitterServer *emitter.Server - watcher *watcher.PackageWatcher - book *address.Book - exportPath string - - // XXX: move this - exported uint -} - -func NewApp(ctx context.Context, logger *slog.Logger, cfg *devCfg, io commands.IO) *App { - return &App{ - ctx: ctx, - logger: logger, - cfg: cfg, - io: io, - } -} - -func execDev(cfg *devCfg, args []string, io commands.IO) error { +func execDev(cfg *devCfg, args []string, cio commands.IO) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - var err error - rt, restore, err := setupRawTerm(cfg, io) - if err != nil { - return fmt.Errorf("unable to init raw term: %w", err) - } - defer restore() - - // Setup trap signal - osm.TrapSignal(func() { - cancel() - restore() - }) - - logger := setuplogger(cfg, rt) - devServer := NewApp(ctx, logger, cfg, io) - if err := devServer.Setup(); err != nil { - return err - } - - return devServer.Run(rt) -} + var rt *rawterm.RawTerm -func (ds *App) Setup() error { - if err := ds.cfg.validateConfigFlags(); err != nil { - return fmt.Errorf("validate error: %w", err) - } + var out io.Writer + if !cfg.interactive { + var err error + var restore func() error - if ds.cfg.chdir != "" { - if err := os.Chdir(ds.cfg.chdir); err != nil { - return fmt.Errorf("unable to change directory: %w", err) + // Setup raw terminal for interaction + rt, restore, err = setupRawTerm(cfg, cio) + if err != nil { + return fmt.Errorf("unable to init raw term: %w", err) } - } - - loggerEvents := ds.logger.WithGroup(EventServerLogName) - ds.emitterServer = emitter.NewServer(loggerEvents) - - dir, err := os.Getwd() - if err != nil { - return fmt.Errorf("unable to guess current dir: %w", err) - } - - path, ok := guessPath(ds.cfg, dir) - if !ok { - return fmt.Errorf("unable to guess path from %q", dir) - } - - // XXX: it would be nice to having this hardcoded - examplesDir := filepath.Join(gnoenv.RootDir(), "examples") - - resolver := setupPackagesResolver(ds.logger.WithGroup(LoaderLogName), ds.cfg, path, dir) - loader := packages.NewGlobLoader(examplesDir, resolver) - - ds.book, err = setupAddressBook(ds.logger.WithGroup(AccountsLogName), ds.cfg) - if err != nil { - return fmt.Errorf("unable to load keybase: %w", err) - } - - balances, err := generateBalances(ds.book, ds.cfg) - if err != nil { - return fmt.Errorf("unable to generate balances: %w", err) - } - ds.logger.Debug("balances loaded", "list", balances.List()) - - nodeLogger := ds.logger.WithGroup(NodeLogName) - nodeCfg := setupDevNodeConfig(ds.cfg, nodeLogger, ds.emitterServer, balances, loader) - ds.devNode, err = setupDevNode(ds.ctx, ds.cfg, nodeCfg, path) - if err != nil { - return err - } - - ds.watcher, err = watcher.NewPackageWatcher(loggerEvents, ds.emitterServer) - if err != nil { - return fmt.Errorf("unable to setup packages watcher: %w", err) - } - - ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) - - return nil -} - -func (ds *App) setupHandlers() http.Handler { - mux := http.NewServeMux() - webhandler := setupGnoWebServer(ds.logger.WithGroup(WebLogName), ds.cfg, ds.devNode) - - // Setup unsage api - if ds.cfg.unsafeAPI { - mux.HandleFunc("/reset", func(res http.ResponseWriter, req *http.Request) { - if err := ds.devNode.Reset(req.Context()); err != nil { - ds.logger.Error("failed to reset", slog.Any("err", err)) - res.WriteHeader(http.StatusInternalServerError) - } - }) + defer restore() - mux.HandleFunc("/reload", func(res http.ResponseWriter, req *http.Request) { - if err := ds.devNode.Reload(req.Context()); err != nil { - ds.logger.Error("failed to reload", slog.Any("err", err)) - res.WriteHeader(http.StatusInternalServerError) - } + osm.TrapSignal(func() { + cancel() + restore() }) - } - if !ds.cfg.noWatch { - evtstarget := fmt.Sprintf("%s/_events", ds.cfg.webListenerAddr) - mux.Handle("/_events", ds.emitterServer) - mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler)) + out = rt } else { - mux.Handle("/", webhandler) - } - - return mux -} - -func (ds *App) Run(term *rawterm.RawTerm) error { - ctx, cancelWith := context.WithCancelCause(ds.ctx) - defer cancelWith(nil) - - addr := ds.cfg.webListenerAddr - ds.logger.WithGroup(WebLogName).Info("gnoweb started", "lisn", fmt.Sprintf("http://%s", addr)) - - server := &http.Server{ - Handler: ds.setupHandlers(), - Addr: ds.cfg.webListenerAddr, - ReadHeaderTimeout: time.Second * 60, - } - - go func() { - err := server.ListenAndServe() - cancelWith(err) - }() - - if ds.cfg.interactive { - ds.logger.WithGroup("--- READY").Info("for commands and help, press `h`") - } - - keyPressCh := listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) - for { - select { - case <-ctx.Done(): - return context.Cause(ctx) - case _, ok := <-ds.watcher.PackagesUpdate: - if !ok { - return nil - } - ds.logger.WithGroup(NodeLogName).Info("reloading...") - if err := ds.devNode.Reload(ds.ctx); err != nil { - ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) - } - ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) - - case key, ok := <-keyPressCh: - if !ok { - return nil - } - - if key == rawterm.KeyCtrlC { - cancelWith(nil) - continue - } - - ds.handleKeyPress(key) - keyPressCh = listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) - } - } -} - -var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: -https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev - -P Previous TX - Go to the previous tx -N Next TX - Go to the next tx -E Export - Export the current state as genesis doc -A Accounts - Display known accounts and balances -H Help - Display this message -R Reload - Reload all packages to take change into account. -Ctrl+S Save State - Save the current state -Ctrl+R Reset - Reset application to it's initial/save state. -Ctrl+C Exit - Exit the application -` - -func (ds *App) handleKeyPress(key rawterm.KeyPress) { - var err error - ds.logger.WithGroup(KeyPressLogName).Debug(fmt.Sprintf("<%s>", key.String())) - - switch key.Upper() { - case rawterm.KeyH: // Helper - ds.logger.Info("Gno Dev Helper", "helper", helper) - - case rawterm.KeyA: // Accounts - logAccounts(ds.logger.WithGroup(AccountsLogName), ds.book, ds.devNode) - - case rawterm.KeyR: // Reload - ds.logger.WithGroup(NodeLogName).Info("reloading...") - if err = ds.devNode.ReloadAll(ds.ctx); err != nil { - ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) - } - - case rawterm.KeyCtrlR: // Reset - ds.logger.WithGroup(NodeLogName).Info("reseting node state...") - if err = ds.devNode.Reset(ds.ctx); err != nil { - ds.logger.WithGroup(NodeLogName).Error("unable to reset node state", "err", err) - } - - case rawterm.KeyCtrlS: // Save - ds.logger.WithGroup(NodeLogName).Info("saving state...") - if err := ds.devNode.SaveCurrentState(ds.ctx); err != nil { - ds.logger.WithGroup(NodeLogName).Error("unable to save node state", "err", err) - } - - case rawterm.KeyE: // Export - // Create a temporary export dir - if ds.exported == 0 { - ds.exportPath, err = os.MkdirTemp("", "gnodev-export") - if err != nil { - ds.logger.WithGroup(NodeLogName).Error("unable to create `export` directory", "err", err) - return - } - } - ds.exported++ - - ds.logger.WithGroup(NodeLogName).Info("exporting state...") - doc, err := ds.devNode.ExportStateAsGenesis(ds.ctx) - if err != nil { - ds.logger.WithGroup(NodeLogName).Error("unable to export node state", "err", err) - return - } - - docfile := filepath.Join(ds.exportPath, fmt.Sprintf("export_%d.jsonl", ds.exported)) - if err := doc.SaveAs(docfile); err != nil { - ds.logger.WithGroup(NodeLogName).Error("unable to save genesis", "err", err) - } - - ds.logger.WithGroup(NodeLogName).Info("node state exported", "file", docfile) - - case rawterm.KeyN: // Next tx - ds.logger.Info("moving forward...") - if err := ds.devNode.MoveToNextTX(ds.ctx); err != nil { - ds.logger.WithGroup(NodeLogName).Error("unable to move forward", "err", err) - } - - case rawterm.KeyP: // Previous tx - ds.logger.Info("moving backward...") - if err := ds.devNode.MoveToPreviousTX(ds.ctx); err != nil { - ds.logger.WithGroup(NodeLogName).Error("unable to move backward", "err", err) - } - default: - } -} - -func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { - cc := make(chan rawterm.KeyPress, 1) - go func() { - defer close(cc) - key, err := rt.ReadKeyPress() - if err != nil { - logger.Error("unable to read keypress", "err", err) - return - } - - cc <- key - }() - - return cc -} - -func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ([]gnodev.PackageModifier, error) { - modifiers := make([]gnodev.PackageModifier, 0, len(args)) - - if cfg.deployKey == "" { - return nil, fmt.Errorf("default deploy key cannot be empty") + osm.TrapSignal(cancel) + out = cio.Out() } - defaultKey, _, ok := bk.GetFromNameOrAddress(cfg.deployKey) - if !ok { - return nil, fmt.Errorf("unable to get deploy key %q", cfg.deployKey) - } - - if len(args) == 0 { - args = append(args, ".") // add current dir if none are provided + logger, err := setuplogger(cfg, out) + if err != nil { + return fmt.Errorf("unable to setup logger: %w", err) } - for _, arg := range args { - path, err := gnodev.ResolvePackageModifierQuery(bk, arg) - if err != nil { - return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) - } - - // Assign a default creator if user haven't specified it. - if path.Creator.IsZero() { - path.Creator = defaultKey - } - - modifiers = append(modifiers, path) + app := NewApp(ctx, logger, cfg, cio) + if err := app.Setup(); err != nil { + return err } - // Add examples folder if minimal is set to false - if cfg.minimal { - modifiers = append(modifiers, gnodev.PackageModifier{ - Path: filepath.Join(cfg.root, "examples"), - Creator: defaultKey, - Deposit: nil, - }) + if rt != nil { + go app.RunInteractive(rt) } - return modifiers, nil + return app.RunServer(rt) } diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go index 49012d70fdf..792bb54fcd5 100644 --- a/contribs/gnodev/cmd/gnodev/setup_loader.go +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -93,17 +93,12 @@ func guessPathGnoMod(dir string) (path string, ok bool) { return "", false } -func guessPath(cfg *devCfg, dir string) (path string, ok bool) { - gnoroot := cfg.root +func guessPath(cfg *devCfg, dir string) (path string) { if path, ok := guessPathGnoMod(dir); ok { - return path, true + return path } - if path, ok = guessPathFromRoots(dir, gnoroot); ok { - return path, ok - } - - return "", false + return filepath.Join(cfg.chainDomain, "/r/dev/myrealm") } func isStdPath(path string) bool { diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index 9abebdd34fe..871ac1752e1 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -16,8 +16,6 @@ import ( // setupDevNode initializes and returns a new DevNode. func setupDevNode(ctx context.Context, cfg *devCfg, nodeConfig *gnodev.NodeConfig, path string) (*gnodev.Node, error) { - fmt.Printf("PATH: %+v\r\n", cfg.paths.String()) - logger := nodeConfig.Logger if cfg.txsFile != "" { // Load txs files @@ -46,7 +44,6 @@ func setupDevNode(ctx context.Context, cfg *devCfg, nodeConfig *gnodev.NodeConfi } paths := append(cfg.paths.Strings(), path) - fmt.Println("PATHS:", paths) return gnodev.NewDevNode(ctx, nodeConfig, paths...) } diff --git a/contribs/gnodev/cmd/gnodev/utils.go b/contribs/gnodev/cmd/gnodev/utils.go new file mode 100644 index 00000000000..ea315add499 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/utils.go @@ -0,0 +1,25 @@ +package main + +import "strings" + +type varStrings []string + +func (va *varStrings) Set(val string) error { + for _, subval := range strings.Split(val, ",") { + *va = append(*va, subval) + } + + return nil +} + +func (va *varStrings) String() string { + return strings.Join(*va, ",") +} + +func (va *varStrings) Strings() []string { + if va == nil { + return []string{} + } + + return []string(*va) +} diff --git a/contribs/gnodev/pkg/logger/log_column.go b/contribs/gnodev/pkg/logger/log_column.go index 2a720525903..b034d8b878d 100644 --- a/contribs/gnodev/pkg/logger/log_column.go +++ b/contribs/gnodev/pkg/logger/log_column.go @@ -21,7 +21,8 @@ func NewColumnLogger(w io.Writer, level slog.Level, profile termenv.Profile) *Co }) // Default column output - defaultOutput := newColumeWriter(lipgloss.NewStyle(), "", w) + renderer := lipgloss.NewRenderer(nil, termenv.WithProfile(profile)) + defaultOutput := newColumeWriter(w, lipgloss.NewStyle(), "") charmLogger.SetOutput(defaultOutput) charmLogger.SetStyles(defaultStyles()) charmLogger.SetColorProfile(profile) @@ -40,19 +41,20 @@ func NewColumnLogger(w io.Writer, level slog.Level, profile termenv.Profile) *Co } return &ColumnLogger{ - Logger: charmLogger, - writer: w, - prefix: charmLogger.GetPrefix(), - colors: map[string]lipgloss.Color{}, + Logger: charmLogger, + writer: w, + prefix: charmLogger.GetPrefix(), + colors: map[string]lipgloss.Color{}, + renderer: renderer, } } type ColumnLogger struct { *log.Logger - prefix string - writer io.Writer - colorProfile termenv.Profile + prefix string + writer io.Writer + renderer *lipgloss.Renderer colors map[string]lipgloss.Color muColors sync.RWMutex @@ -72,11 +74,11 @@ func (cl *ColumnLogger) WithGroup(group string) slog.Handler { // generate bright color based on the group name fg = colorFromString(group, 0.5, 0.6) } - baseStyle := lipgloss.NewStyle().Foreground(fg) + + baseStyle := lipgloss.NewStyle().Foreground(fg).Renderer(cl.renderer) nlog := cl.Logger.With() // clone logger - nlog.SetOutput(newColumeWriter(baseStyle, group, cl.writer)) - nlog.SetColorProfile(cl.colorProfile) + nlog.SetOutput(newColumeWriter(cl.writer, baseStyle, group)) return &ColumnLogger{ Logger: nlog, prefix: group, @@ -99,7 +101,7 @@ type columnWriter struct { writer io.Writer } -func newColumeWriter(baseStyle lipgloss.Style, prefix string, writer io.Writer) *columnWriter { +func newColumeWriter(w io.Writer, baseStyle lipgloss.Style, prefix string) *columnWriter { const width = 12 style := baseStyle. @@ -112,7 +114,7 @@ func newColumeWriter(baseStyle lipgloss.Style, prefix string, writer io.Writer) prefix = prefix[:width-3] + "..." } - return &columnWriter{style: style, prefix: prefix, writer: writer} + return &columnWriter{style: style, prefix: prefix, writer: w} } func (cl *columnWriter) Write(buf []byte) (n int, err error) { diff --git a/contribs/gnodev/pkg/packages/glob.go b/contribs/gnodev/pkg/packages/glob.go new file mode 100644 index 00000000000..36e1acccdf8 --- /dev/null +++ b/contribs/gnodev/pkg/packages/glob.go @@ -0,0 +1,210 @@ +// Inspired by: https://cs.opensource.google/go/x/tools/+/master:gopls/internal/test/integration/fake/glob/glob.go + +package packages + +import ( + "errors" + "fmt" + "strings" +) + +var ErrAdjacentSlash = errors.New("** may only be adjacent to '/'") + +// Glob patterns can have the following syntax: +// - `*` to match one or more characters in a path segment +// - `**` to match any number of path segments, including none +// +// Expanding on this: +// - '/' matches one or more literal slashes. +// - any other character matches itself literally. +type Glob struct { + elems []element // pattern elements +} + +// Parse builds a Glob for the given pattern, returning an error if the pattern +// is invalid. +func Parse(pattern string) (*Glob, error) { + g, _, err := parse(pattern) + return g, err +} + +func parse(pattern string) (*Glob, string, error) { + g := new(Glob) + for len(pattern) > 0 { + switch pattern[0] { + case '/': + pattern = pattern[1:] + g.elems = append(g.elems, slash{}) + + case '*': + if len(pattern) > 1 && pattern[1] == '*' { + if (len(g.elems) > 0 && g.elems[len(g.elems)-1] != slash{}) || (len(pattern) > 2 && pattern[2] != '/') { + return nil, "", ErrAdjacentSlash + } + pattern = pattern[2:] + g.elems = append(g.elems, starStar{}) + break + } + pattern = pattern[1:] + g.elems = append(g.elems, star{}) + + default: + pattern = g.parseLiteral(pattern) + } + } + return g, "", nil +} + +func (g *Glob) parseLiteral(pattern string) string { + end := strings.IndexAny(pattern, "*/") + if end == -1 { + end = len(pattern) + } + g.elems = append(g.elems, literal(pattern[:end])) + return pattern[end:] +} + +func (g *Glob) String() string { + var b strings.Builder + for _, e := range g.elems { + fmt.Fprint(&b, e) + } + return b.String() +} + +func (g *Glob) StarFreeBase() string { + var b strings.Builder + for _, e := range g.elems { + if e == (star{}) || e == (starStar{}) { + break + } + fmt.Fprint(&b, e) + } + return b.String() +} + +// element holds a glob pattern element, as defined below. +type element fmt.Stringer + +// element types. +type ( + slash struct{} // One or more '/' separators + literal string // string literal, not containing / or * + star struct{} // * + starStar struct{} // ** +) + +func (s slash) String() string { return "/" } +func (l literal) String() string { return string(l) } +func (s star) String() string { return "*" } +func (s starStar) String() string { return "**" } + +// Match reports whether the input string matches the glob pattern. +func (g *Glob) Match(input string) bool { + return match(g.elems, input) +} + +func match(elems []element, input string) (ok bool) { + var elem interface{} + for len(elems) > 0 { + elem, elems = elems[0], elems[1:] + switch elem := elem.(type) { + case slash: + if len(input) == 0 || input[0] != '/' { + return false + } + for input[0] == '/' { + input = input[1:] + } + + case starStar: + // Special cases: + // - **/a matches "a" + // - **/ matches everything + // + // Note that if ** is followed by anything, it must be '/' (this is + // enforced by Parse). + if len(elems) > 0 { + elems = elems[1:] + } + + // A trailing ** matches anything. + if len(elems) == 0 { + return true + } + + // Backtracking: advance pattern segments until the remaining pattern + // elements match. + for len(input) != 0 { + if match(elems, input) { + return true + } + _, input = split(input) + } + return false + + case literal: + if !strings.HasPrefix(input, string(elem)) { + return false + } + input = input[len(elem):] + + case star: + var segInput string + segInput, input = split(input) + + elemEnd := len(elems) + for i, e := range elems { + if e == (slash{}) { + elemEnd = i + break + } + } + segElems := elems[:elemEnd] + elems = elems[elemEnd:] + + // A trailing * matches the entire segment. + if len(segElems) == 0 { + if len(elems) > 0 && elems[0] == (slash{}) { + elems = elems[1:] // shift elems + } + break + } + + // Backtracking: advance characters until remaining subpattern elements + // match. + matched := false + for i := range segInput { + if match(segElems, segInput[i:]) { + matched = true + break + } + } + if !matched { + return false + } + + default: + panic(fmt.Sprintf("segment type %T not implemented", elem)) + } + } + + return len(input) == 0 +} + +// split returns the portion before and after the first slash +// (or sequence of consecutive slashes). If there is no slash +// it returns (input, nil). +func split(input string) (first, rest string) { + i := strings.IndexByte(input, '/') + if i < 0 { + return input, "" + } + first = input[:i] + for j := i; j < len(input); j++ { + if input[j] != '/' { + return first, input[j:] + } + } + return first, "" +} diff --git a/contribs/gnodev/pkg/packages/glob_test.go b/contribs/gnodev/pkg/packages/glob_test.go new file mode 100644 index 00000000000..e8d1f57ecb6 --- /dev/null +++ b/contribs/gnodev/pkg/packages/glob_test.go @@ -0,0 +1,88 @@ +// Inspired by: https://cs.opensource.google/go/x/tools/+/master:gopls/internal/test/integration/fake/glob/glob_test.go + +package packages + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMatch(t *testing.T) { + tests := []struct { + pattern, input string + want bool + }{ + // Basic cases. + {"", "", true}, + {"", "a", false}, + {"", "/", false}, + {"abc", "abc", true}, + + // ** behavior + {"**", "abc", true}, + {"**/abc", "abc", true}, + {"**", "abc/def", true}, + + // * behavior + {"/*", "/a", true}, + {"*", "foo", true}, + {"*o", "foo", true}, + {"*o", "foox", false}, + {"f*o", "foo", true}, + {"f*o", "fo", true}, + + // Dirs cases + {"**/", "path/to/foo/", true}, + {"**/", "path/to/foo", true}, + + {"path/to/foo", "path/to/foo", true}, + {"path/to/foo", "path/to/bar", false}, + {"path/*/foo", "path/to/foo", true}, + {"path/*/1/*/3/*/5*/foo", "path/to/1/2/3/4/522/foo", true}, + {"path/*/1/*/3/*/5*/foo", "path/to/1/2/3/4/722/foo", false}, + {"path/*/1/*/3/*/5*/foo", "path/to/1/2/3/4/522/bar", false}, + {"path/*/foo", "path/to/to/foo", false}, + {"path/**/foo", "path/to/to/foo", true}, + {"path/**/foo", "path/foo", true}, + {"**/abc/**", "foo/r/x/abc/bar", true}, + + // Realistic examples. + {"**/*.ts", "path/to/foo.ts", true}, + {"**/*.js", "path/to/foo.js", true}, + {"**/*.go", "path/to/foo.go", true}, + } + + for _, test := range tests { + g, err := Parse(test.pattern) + require.NoErrorf(t, err, "Parse(%q) failed unexpectedly: %v", test.pattern, err) + assert.Equalf(t, test.want, g.Match(test.input), + "Parse(%q).Match(%q) = %t, want %t", test.pattern, test.input, !test.want, test.want) + } +} + +func TestBaseFreeStar(t *testing.T) { + tests := []struct { + pattern, baseFree string + }{ + // Basic cases. + {"", ""}, + {"foo", "foo"}, + {"foo/bar", "foo/bar"}, + {"foo///bar", "foo/bar"}, + {"foo/bar/", "foo/bar/"}, + {"foo/bar/*/*/z", "foo/bar/"}, + {"foo/bar/**", "foo/bar/"}, + {"**", ""}, + {"/**", "/"}, + } + + for _, test := range tests { + g, err := Parse(test.pattern) + require.NoErrorf(t, err, "Parse(%q) failed unexpectedly: %v", test.pattern, err) + got := g.StarFreeBase() + assert.Equalf(t, test.baseFree, got, + "Parse(%q).Match(%q) = %q, want %q", test.pattern, test.baseFree, got, test.baseFree) + } +} diff --git a/contribs/gnodev/pkg/packages/resolver_local.go b/contribs/gnodev/pkg/packages/resolver_local.go index 867ff6154ba..16480e304d5 100644 --- a/contribs/gnodev/pkg/packages/resolver_local.go +++ b/contribs/gnodev/pkg/packages/resolver_local.go @@ -1,6 +1,7 @@ package packages import ( + "errors" "fmt" "go/token" "path/filepath" @@ -30,5 +31,10 @@ func (lr LocalResolver) Resolve(fset *token.FileSet, path string) (*Package, err } dir := filepath.Join(lr.Dir, after) - return ReadPackageFromDir(fset, path, dir) + pkg, err := ReadPackageFromDir(fset, path, dir) + if err != nil && after == "" && errors.Is(err, ErrResolverPackageSkip) { + return nil, fmt.Errorf("empty local package %q", err) + } + + return pkg, nil } From d48ffd982bfa1d78382de78997392595ef39493b Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sun, 15 Dec 2024 23:19:46 +0100 Subject: [PATCH 10/24] chore: lint Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/app.go | 83 +++++++++---------- contribs/gnodev/cmd/gnodev/main.go | 6 +- contribs/gnodev/cmd/gnodev/setup_loader.go | 25 +++--- contribs/gnodev/pkg/dev/node.go | 1 - contribs/gnodev/pkg/dev/packages_test.go | 1 - contribs/gnodev/pkg/packages/resolver_fs.go | 8 +- .../gnodev/pkg/packages/resolver_local.go | 17 ++-- .../pkg/packages/resolver_middleware.go | 1 - .../gnodev/pkg/packages/resolver_remote.go | 9 +- 9 files changed, 70 insertions(+), 81 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/app.go b/contribs/gnodev/cmd/gnodev/app.go index 1bd5dd3c8a2..393ec792b4c 100644 --- a/contribs/gnodev/cmd/gnodev/app.go +++ b/contribs/gnodev/cmd/gnodev/app.go @@ -25,7 +25,6 @@ type App struct { logger *slog.Logger devNode *gnodev.Node - server *http.Server emitterServer *emitter.Server watcher *watcher.PackageWatcher book *address.Book @@ -296,44 +295,44 @@ func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm. return cc } -func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ([]gnodev.PackageModifier, error) { - modifiers := make([]gnodev.PackageModifier, 0, len(args)) - - if cfg.deployKey == "" { - return nil, fmt.Errorf("default deploy key cannot be empty") - } - - defaultKey, _, ok := bk.GetFromNameOrAddress(cfg.deployKey) - if !ok { - return nil, fmt.Errorf("unable to get deploy key %q", cfg.deployKey) - } - - if len(args) == 0 { - args = append(args, ".") // add current dir if none are provided - } - - for _, arg := range args { - path, err := gnodev.ResolvePackageModifierQuery(bk, arg) - if err != nil { - return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) - } - - // Assign a default creator if user haven't specified it. - if path.Creator.IsZero() { - path.Creator = defaultKey - } - - modifiers = append(modifiers, path) - } - - // Add examples folder if minimal is set to false - if cfg.minimal { - modifiers = append(modifiers, gnodev.PackageModifier{ - Path: filepath.Join(cfg.root, "examples"), - Creator: defaultKey, - Deposit: nil, - }) - } - - return modifiers, nil -} +// func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ([]gnodev.PackageModifier, error) { +// modifiers := make([]gnodev.PackageModifier, 0, len(args)) + +// if cfg.deployKey == "" { +// return nil, fmt.Errorf("default deploy key cannot be empty") +// } + +// defaultKey, _, ok := bk.GetFromNameOrAddress(cfg.deployKey) +// if !ok { +// return nil, fmt.Errorf("unable to get deploy key %q", cfg.deployKey) +// } + +// if len(args) == 0 { +// args = append(args, ".") // add current dir if none are provided +// } + +// for _, arg := range args { +// path, err := gnodev.ResolvePackageModifierQuery(bk, arg) +// if err != nil { +// return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) +// } + +// // Assign a default creator if user haven't specified it. +// if path.Creator.IsZero() { +// path.Creator = defaultKey +// } + +// modifiers = append(modifiers, path) +// } + +// // Add examples folder if minimal is set to false +// if cfg.minimal { +// modifiers = append(modifiers, gnodev.PackageModifier{ +// Path: filepath.Join(cfg.root, "examples"), +// Creator: defaultKey, +// Deposit: nil, +// }) +// } + +// return modifiers, nil +// } diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index f5417ec3439..56e836a5a2d 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -36,8 +36,7 @@ const ( var ErrConflictingFileArgs = errors.New("cannot specify `balances-file` or `txs-file` along with `genesis-file`") type devCfg struct { - chdir string - rootPath string + chdir string // Listeners nodeRPCListenerAddr string @@ -65,7 +64,6 @@ type devCfg struct { // Node Configuration logFormat string - minimal bool verbose bool noWatch bool noReplay bool @@ -176,7 +174,7 @@ func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { fs.Var( &c.resolvers, "resolver", - "list of addtional resolvers, will be exectued in the given order", + "list of additional resolvers, will be executed in the given order", ) fs.StringVar( diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go index 792bb54fcd5..bfb7d1bc4dd 100644 --- a/contribs/gnodev/cmd/gnodev/setup_loader.go +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -71,23 +71,10 @@ func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, path, dir string) p ) } -func guessPathFromRoots(dir string, roots ...string) (path string, ok bool) { - for _, root := range roots { - if !strings.HasPrefix(dir, root) { - continue - } - - return strings.TrimPrefix(dir, root), true - } - - return "", false -} - func guessPathGnoMod(dir string) (path string, ok bool) { modfile, err := gnomod.ParseAt(dir) if err == nil { return modfile.Module.Mod.Path, true - } return "", false @@ -110,3 +97,15 @@ func isStdPath(path string) bool { return true } + +// func guessPathFromRoots(dir string, roots ...string) (path string, ok bool) { +// for _, root := range roots { +// if !strings.HasPrefix(dir, root) { +// continue +// } + +// return strings.TrimPrefix(dir, root), true +// } + +// return "", false +// } diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index af5b05fa70e..dcb17afabcd 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -526,7 +526,6 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) node, nodeErr := gnoland.NewInMemoryNode(noopLogger, nodeConfig) if nodeErr != nil { return fmt.Errorf("unable to create a new node: %w", err) - } node.EventSwitch().AddListener("dev-emitter", n.handleEventTX) diff --git a/contribs/gnodev/pkg/dev/packages_test.go b/contribs/gnodev/pkg/dev/packages_test.go index f6ccfda877f..a35442ee0e1 100644 --- a/contribs/gnodev/pkg/dev/packages_test.go +++ b/contribs/gnodev/pkg/dev/packages_test.go @@ -1,6 +1,5 @@ package dev - // import ( // "testing" diff --git a/contribs/gnodev/pkg/packages/resolver_fs.go b/contribs/gnodev/pkg/packages/resolver_fs.go index 2a235dd63d6..c5b6ba42955 100644 --- a/contribs/gnodev/pkg/packages/resolver_fs.go +++ b/contribs/gnodev/pkg/packages/resolver_fs.go @@ -11,14 +11,14 @@ type fsResolver struct { root string // Root folder } -func (l *fsResolver) Name() string { - return fmt.Sprintf("fs<%s>", filepath.Base(l.root)) -} - func NewFSResolver(rootpath string) Resolver { return &fsResolver{root: rootpath} } +func (r *fsResolver) Name() string { + return fmt.Sprintf("fs<%s>", filepath.Base(r.root)) +} + func (r *fsResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { dir := filepath.Join(r.root, path) _, err := os.Stat(dir) diff --git a/contribs/gnodev/pkg/packages/resolver_local.go b/contribs/gnodev/pkg/packages/resolver_local.go index 16480e304d5..d87f146786a 100644 --- a/contribs/gnodev/pkg/packages/resolver_local.go +++ b/contribs/gnodev/pkg/packages/resolver_local.go @@ -13,10 +13,6 @@ type LocalResolver struct { Dir string } -func (l *LocalResolver) Name() string { - return fmt.Sprintf("local<%s>", filepath.Base(l.Dir)) -} - func NewLocalResolver(path, dir string) *LocalResolver { return &LocalResolver{ Path: path, @@ -24,16 +20,21 @@ func NewLocalResolver(path, dir string) *LocalResolver { } } -func (lr LocalResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { - after, found := strings.CutPrefix(path, lr.Path) +func (r *LocalResolver) Name() string { + return fmt.Sprintf("local<%s>", filepath.Base(r.Dir)) +} + +func (r LocalResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + after, found := strings.CutPrefix(path, r.Path) if !found { return nil, ErrResolverPackageNotFound } - dir := filepath.Join(lr.Dir, after) + dir := filepath.Join(r.Dir, after) pkg, err := ReadPackageFromDir(fset, path, dir) + if err != nil && after == "" && errors.Is(err, ErrResolverPackageSkip) { - return nil, fmt.Errorf("empty local package %q", err) + return nil, fmt.Errorf("empty local package %w", err) // local package cannot be empty } return pkg, nil diff --git a/contribs/gnodev/pkg/packages/resolver_middleware.go b/contribs/gnodev/pkg/packages/resolver_middleware.go index dce2539bfc2..9e9e5346e43 100644 --- a/contribs/gnodev/pkg/packages/resolver_middleware.go +++ b/contribs/gnodev/pkg/packages/resolver_middleware.go @@ -77,7 +77,6 @@ func LogMiddleware(logger *slog.Logger) MiddlewareHandler { "took", time.Since(start).String(), "resolver", next.Name(), "err", err) - } return pkg, err diff --git a/contribs/gnodev/pkg/packages/resolver_remote.go b/contribs/gnodev/pkg/packages/resolver_remote.go index 0b34e384dec..2fb2f204c22 100644 --- a/contribs/gnodev/pkg/packages/resolver_remote.go +++ b/contribs/gnodev/pkg/packages/resolver_remote.go @@ -12,10 +12,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" ) -type remoteCaching interface { - Get(path string) -} - type remoteResolver struct { *client.RPCClient // Root folder fset *token.FileSet @@ -62,12 +58,11 @@ func (res *remoteResolver) Resolve(fset *token.FileSet, path string) (*Package, } if err := qres.Response.Error; err != nil { - return nil, fmt.Errorf("unable to query file %q on path %q: %w", - string(fname), path, err) + return nil, fmt.Errorf("unable to query file %q on path %q: %w", fname, path, err) } body := qres.Response.Data - memfile, pkgname, err := parseFile(fset, string(fname), body) + memfile, pkgname, err := parseFile(fset, fname, body) if err != nil { return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) } From b1930817dd42b22713c57e6ce1ecfcc6188afaa6 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 16 Dec 2024 00:51:31 +0100 Subject: [PATCH 11/24] fix: web redirect Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/app.go | 135 +++++++++++++----- contribs/gnodev/cmd/gnodev/command_staging.go | 63 +------- contribs/gnodev/cmd/gnodev/logger.go | 5 +- contribs/gnodev/cmd/gnodev/main.go | 54 ++----- contribs/gnodev/cmd/gnodev/setup_loader.go | 26 +++- contribs/gnodev/cmd/gnodev/setup_node.go | 8 +- contribs/gnodev/pkg/dev/node.go | 2 +- contribs/gnodev/pkg/logger/log_column.go | 8 +- 8 files changed, 148 insertions(+), 153 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/app.go b/contribs/gnodev/cmd/gnodev/app.go index 393ec792b4c..612fe895c88 100644 --- a/contribs/gnodev/cmd/gnodev/app.go +++ b/contribs/gnodev/cmd/gnodev/app.go @@ -3,10 +3,12 @@ package main import ( "context" "fmt" + "io" "log/slog" "net/http" "os" "path/filepath" + "strings" "time" "github.com/gnolang/gno/contribs/gnodev/pkg/address" @@ -16,14 +18,16 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" "github.com/gnolang/gno/tm2/pkg/commands" + osm "github.com/gnolang/gno/tm2/pkg/os" ) type App struct { - ctx context.Context cfg *devCfg io commands.IO logger *slog.Logger + webHome string + paths []string devNode *gnodev.Node emitterServer *emitter.Server watcher *watcher.PackageWatcher @@ -34,43 +38,94 @@ type App struct { exported uint } -func NewApp(ctx context.Context, logger *slog.Logger, cfg *devCfg, io commands.IO) *App { +func runApp(cfg *devCfg, cio commands.IO, dirs ...string) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var rt *rawterm.RawTerm + + var out io.Writer + if cfg.interactive { + var err error + var restore func() error + + // Setup raw terminal for interaction + rt, restore, err = setupRawTerm(cfg, cio) + if err != nil { + return fmt.Errorf("unable to init raw term: %w", err) + } + defer restore() + + osm.TrapSignal(func() { + cancel() + restore() + }) + + out = rt + } else { + osm.TrapSignal(cancel) + out = cio.Out() + } + + logger, err := setuplogger(cfg, out) + if err != nil { + return fmt.Errorf("unable to setup logger: %w", err) + } + + app := NewApp(logger, cfg, cio) + if err := app.Setup(ctx, dirs...); err != nil { + return err + } + + if rt != nil { + go func() { + app.RunInteractive(ctx, rt) + cancel() + }() + } + + return app.RunServer(ctx, rt) +} + +func NewApp(logger *slog.Logger, cfg *devCfg, io commands.IO) *App { return &App{ - ctx: ctx, logger: logger, cfg: cfg, io: io, } } -func (ds *App) Setup() error { - if err := ds.cfg.validateConfigFlags(); err != nil { - return fmt.Errorf("validate error: %w", err) - } +func (ds *App) Setup(ctx context.Context, dirs ...string) error { + var err error - if ds.cfg.chdir != "" { - if err := os.Chdir(ds.cfg.chdir); err != nil { - return fmt.Errorf("unable to change directory: %w", err) - } + if err = ds.cfg.validateConfigFlags(); err != nil { + return fmt.Errorf("validate error: %w", err) } loggerEvents := ds.logger.WithGroup(EventServerLogName) ds.emitterServer = emitter.NewServer(loggerEvents) - dir, err := os.Getwd() - if err != nil { - return fmt.Errorf("unable to guess current dir: %w", err) - } - - path := guessPath(ds.cfg, dir) - ds.logger.WithGroup(LoaderLogName).Info("realm path", "path", path, "dir", dir) - // XXX: it would be nice to having this hardcoded examplesDir := filepath.Join(ds.cfg.root, "examples") - resolver := setupPackagesResolver(ds.logger.WithGroup(LoaderLogName), ds.cfg, path, dir) + resolver, localPaths := setupPackagesResolver(ds.logger.WithGroup(LoaderLogName), ds.cfg, dirs...) loader := packages.NewGlobLoader(examplesDir, resolver) + // Setup default web home realm, fallback on first local path + switch webHome := ds.cfg.webHome; webHome { + case "": + if len(localPaths) > 0 { + ds.webHome = strings.TrimPrefix(localPaths[0], ds.cfg.chainDomain) + } + case "/": // skip + default: + ds.webHome = webHome + } + + // generate paths + paths := strings.Split(ds.cfg.paths, ",") + ds.paths = append(localPaths, paths...) + ds.book, err = setupAddressBook(ds.logger.WithGroup(AccountsLogName), ds.cfg) if err != nil { return fmt.Errorf("unable to load keybase: %w", err) @@ -84,7 +139,7 @@ func (ds *App) Setup() error { nodeLogger := ds.logger.WithGroup(NodeLogName) nodeCfg := setupDevNodeConfig(ds.cfg, nodeLogger, ds.emitterServer, balances, loader) - ds.devNode, err = setupDevNode(ds.ctx, ds.cfg, nodeCfg, path) + ds.devNode, err = setupDevNode(ctx, ds.cfg, nodeCfg, ds.paths...) if err != nil { return err } @@ -103,6 +158,17 @@ func (ds *App) setupHandlers() http.Handler { mux := http.NewServeMux() webhandler := setupGnoWebServer(ds.logger.WithGroup(WebLogName), ds.cfg, ds.devNode) + if ds.webHome != "" { + serveWeb := webhandler.ServeHTTP + webhandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "" || r.URL.Path == "/" { + http.Redirect(w, r, ds.webHome, http.StatusFound) + } else { + serveWeb(w, r) + } + }) + } + // Setup unsage api if ds.cfg.unsafeAPI { mux.HandleFunc("/reset", func(res http.ResponseWriter, req *http.Request) { @@ -131,8 +197,8 @@ func (ds *App) setupHandlers() http.Handler { return mux } -func (ds *App) RunServer(term *rawterm.RawTerm) error { - ctx, cancelWith := context.WithCancelCause(ds.ctx) +func (ds *App) RunServer(ctx context.Context, term *rawterm.RawTerm) error { + ctx, cancelWith := context.WithCancelCause(ctx) defer cancelWith(nil) addr := ds.cfg.webListenerAddr @@ -164,7 +230,7 @@ func (ds *App) RunServer(term *rawterm.RawTerm) error { return nil } ds.logger.WithGroup(NodeLogName).Info("reloading...") - if err := ds.devNode.Reload(ds.ctx); err != nil { + if err := ds.devNode.Reload(ctx); err != nil { ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) } ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) @@ -172,7 +238,7 @@ func (ds *App) RunServer(term *rawterm.RawTerm) error { } } -func (ds *App) RunInteractive(term *rawterm.RawTerm) { +func (ds *App) RunInteractive(ctx context.Context, term *rawterm.RawTerm) { var keyPressCh <-chan rawterm.KeyPress if ds.cfg.interactive { keyPressCh = listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) @@ -180,7 +246,8 @@ func (ds *App) RunInteractive(term *rawterm.RawTerm) { for { select { - case <-ds.ctx.Done(): + case <-ctx.Done(): + return case key, ok := <-keyPressCh: if !ok { return @@ -190,7 +257,7 @@ func (ds *App) RunInteractive(term *rawterm.RawTerm) { return } - ds.handleKeyPress(key) + ds.handleKeyPress(ctx, key) keyPressCh = listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) } } @@ -210,7 +277,7 @@ Ctrl+R Reset - Reset application to it's initial/save state. Ctrl+C Exit - Exit the application ` -func (ds *App) handleKeyPress(key rawterm.KeyPress) { +func (ds *App) handleKeyPress(ctx context.Context, key rawterm.KeyPress) { var err error ds.logger.WithGroup(KeyPressLogName).Debug(fmt.Sprintf("<%s>", key.String())) @@ -223,19 +290,19 @@ func (ds *App) handleKeyPress(key rawterm.KeyPress) { case rawterm.KeyR: // Reload ds.logger.WithGroup(NodeLogName).Info("reloading...") - if err = ds.devNode.ReloadAll(ds.ctx); err != nil { + if err = ds.devNode.ReloadAll(ctx); err != nil { ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) } case rawterm.KeyCtrlR: // Reset ds.logger.WithGroup(NodeLogName).Info("reseting node state...") - if err = ds.devNode.Reset(ds.ctx); err != nil { + if err = ds.devNode.Reset(ctx); err != nil { ds.logger.WithGroup(NodeLogName).Error("unable to reset node state", "err", err) } case rawterm.KeyCtrlS: // Save ds.logger.WithGroup(NodeLogName).Info("saving state...") - if err := ds.devNode.SaveCurrentState(ds.ctx); err != nil { + if err := ds.devNode.SaveCurrentState(ctx); err != nil { ds.logger.WithGroup(NodeLogName).Error("unable to save node state", "err", err) } @@ -251,7 +318,7 @@ func (ds *App) handleKeyPress(key rawterm.KeyPress) { ds.exported++ ds.logger.WithGroup(NodeLogName).Info("exporting state...") - doc, err := ds.devNode.ExportStateAsGenesis(ds.ctx) + doc, err := ds.devNode.ExportStateAsGenesis(ctx) if err != nil { ds.logger.WithGroup(NodeLogName).Error("unable to export node state", "err", err) return @@ -266,13 +333,13 @@ func (ds *App) handleKeyPress(key rawterm.KeyPress) { case rawterm.KeyN: // Next tx ds.logger.Info("moving forward...") - if err := ds.devNode.MoveToNextTX(ds.ctx); err != nil { + if err := ds.devNode.MoveToNextTX(ctx); err != nil { ds.logger.WithGroup(NodeLogName).Error("unable to move forward", "err", err) } case rawterm.KeyP: // Previous tx ds.logger.Info("moving backward...") - if err := ds.devNode.MoveToPreviousTX(ds.ctx); err != nil { + if err := ds.devNode.MoveToPreviousTX(ctx); err != nil { ds.logger.WithGroup(NodeLogName).Error("unable to move backward", "err", err) } default: diff --git a/contribs/gnodev/cmd/gnodev/command_staging.go b/contribs/gnodev/cmd/gnodev/command_staging.go index 92ba6de7e5f..fc6c420da31 100644 --- a/contribs/gnodev/cmd/gnodev/command_staging.go +++ b/contribs/gnodev/cmd/gnodev/command_staging.go @@ -18,6 +18,7 @@ var defaultStagingOptions = devCfg{ chainDomain: DefaultDomain, logFormat: "json", maxGas: 10_000_000_000, + webHome: "/", webListenerAddr: "127.0.0.1:8888", nodeRPCListenerAddr: "127.0.0.1:26657", deployKey: DefaultDeployerAddress.String(), @@ -25,7 +26,7 @@ var defaultStagingOptions = devCfg{ root: gnoenv.RootDir(), interactive: false, unsafeAPI: false, - paths: varStrings{filepath.Join(DefaultDomain, "/**")}, // Load every package under the main domain}, + paths: filepath.Join(DefaultDomain, "/**"), // Load every package under the main domain}, // As we have no reason to configure this yet, set this to random port // to avoid potential conflict with other app @@ -39,7 +40,7 @@ func NewStagingCmd(io commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "staging", - ShortUsage: "gnodev staging [flags]", + ShortUsage: "gnodev staging [flags] [package_dir...]", ShortHelp: "Start gnodev in staging mode", LongHelp: "STAGING: Staging mode configure the node for server usage", NoParentFlags: true, @@ -56,61 +57,5 @@ func (c *stagingCfg) RegisterFlags(fs *flag.FlagSet) { } func execStagingCmd(cfg *stagingCfg, args []string, io commands.IO) error { - return execDev(&cfg.dev, args, io) - // ctx, cancel := context.WithCancel(context.Background()) - // defer cancel() - - // // Setup trap signal - // osm.TrapSignal(cancel) - - // level := zapcore.InfoLevel - // if cfg.dev.verbose { - // level = zapcore.DebugLevel - // } - - // // Set up the logger - // logger := log.ZapLoggerToSlog(log.NewZapJSONLogger(io.Out(), level)) - - // // Setup trap signal - // devServer := NewApp(ctx, logger, &cfg.dev, io) - // if err := devServer.Setup(); err != nil { - // return err - // } - - // return devServer.RunServer(ctx) + return runApp(&cfg.dev, io, args...) } - -// func (ds *App) RunServer(ctx context.Context) error { -// ctx, cancelWith := context.WithCancelCause(ctx) -// defer cancelWith(nil) - -// addr := ds.cfg.webListenerAddr - -// server := &http.Server{ -// Handler: ds.setupHandlers(), -// Addr: ds.cfg.webListenerAddr, -// ReadHeaderTimeout: time.Second * 60, -// } - -// ds.logger.WithGroup(WebLogName).Info("gnoweb started", "lisn", fmt.Sprintf("http://%s", addr)) -// go func() { -// err := server.ListenAndServe() -// cancelWith(err) -// }() - -// for { -// select { -// case <-ctx.Done(): -// return context.Cause(ctx) -// case _, ok := <-ds.watcher.PackagesUpdate: -// if !ok { -// return nil -// } -// ds.logger.WithGroup(NodeLogName).Info("reloading...") -// if err := ds.devNode.Reload(ds.ctx); err != nil { -// ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) -// } -// ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) -// } -// } -// } diff --git a/contribs/gnodev/cmd/gnodev/logger.go b/contribs/gnodev/cmd/gnodev/logger.go index 59d73c8abe9..6712e6a3cf4 100644 --- a/contribs/gnodev/cmd/gnodev/logger.go +++ b/contribs/gnodev/cmd/gnodev/logger.go @@ -8,7 +8,6 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/gnolang/gno/contribs/gnodev/pkg/logger" "github.com/gnolang/gno/gno.land/pkg/log" - "github.com/muesli/termenv" "go.uber.org/zap/zapcore" ) @@ -24,9 +23,7 @@ func setuplogger(cfg *devCfg, out io.Writer) (*slog.Logger, error) { return newJSONLogger(out, level), nil case "console", "": // Detect term color profile - colorProfile := termenv.DefaultOutput().Profile - - clogger := logger.NewColumnLogger(out, level, colorProfile) + clogger := logger.NewColumnLogger(out, level) // Register well known group color with system colors clogger.RegisterGroupColor(NodeLogName, lipgloss.Color("3")) diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 56e836a5a2d..2cf8e2af11b 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -5,15 +5,12 @@ import ( "errors" "flag" "fmt" - "io" "os" - "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" "github.com/gnolang/gno/gno.land/pkg/integration" "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto" - osm "github.com/gnolang/gno/tm2/pkg/os" ) const DefaultDomain = "gno.land" @@ -58,6 +55,7 @@ type devCfg struct { webListenerAddr string webRemoteHelperAddr string webWithHTML bool + webHome string // Resolver resolvers varResolver @@ -72,7 +70,7 @@ type devCfg struct { chainDomain string unsafeAPI bool interactive bool - paths varStrings + paths string } var defaultDevOptions = devCfg{ @@ -274,10 +272,11 @@ func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { "log output format, can be `json` or `console`", ) - fs.Var( + fs.StringVar( &c.paths, "paths", - "additional path load, can be use multiple time to be chained", + defaultCfg.paths, + "additional path(s) to load, separated by comma", ) // Short flags @@ -298,47 +297,16 @@ func (c *devCfg) validateConfigFlags() error { } func execDev(cfg *devCfg, args []string, cio commands.IO) error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - var rt *rawterm.RawTerm - - var out io.Writer - if !cfg.interactive { - var err error - var restore func() error - - // Setup raw terminal for interaction - rt, restore, err = setupRawTerm(cfg, cio) - if err != nil { - return fmt.Errorf("unable to init raw term: %w", err) + if cfg.chdir != "" { + if err := os.Chdir(cfg.chdir); err != nil { + return fmt.Errorf("unable to change directory: %w", err) } - defer restore() - - osm.TrapSignal(func() { - cancel() - restore() - }) - - out = rt - } else { - osm.TrapSignal(cancel) - out = cio.Out() } - logger, err := setuplogger(cfg, out) + dir, err := os.Getwd() if err != nil { - return fmt.Errorf("unable to setup logger: %w", err) - } - - app := NewApp(ctx, logger, cfg, cio) - if err := app.Setup(); err != nil { - return err - } - - if rt != nil { - go app.RunInteractive(rt) + return fmt.Errorf("unable to guess current dir: %w", err) } - return app.RunServer(rt) + return runApp(cfg, cio, dir) } diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go index bfb7d1bc4dd..d0a63f89474 100644 --- a/contribs/gnodev/cmd/gnodev/setup_loader.go +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "path/filepath" + "regexp" "strings" "github.com/gnolang/gno/contribs/gnodev/pkg/packages" @@ -50,14 +51,24 @@ func (va *varResolver) Set(value string) error { return nil } -func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, path, dir string) packages.Resolver { +func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, dirs ...string) (packages.Resolver, []string) { // Add root resolvers exampleRoot := filepath.Join(cfg.root, "examples") + var localResolvers []packages.Resolver + var paths []string + for _, dir := range dirs { + path := guessPath(cfg, dir) + localResolvers = append(localResolvers, packages.NewLocalResolver(path, dir)) + paths = append(paths, path) + + logger.Info("guessing directory path", "path", path, "dir", dir) + } + resolver := packages.ChainResolvers( - packages.NewLocalResolver(path, dir), // Resolve local directory - packages.ChainResolvers(cfg.resolvers...), // Use user's custom resolvers - packages.NewFSResolver(exampleRoot), // Ultimately use fs resolver + packages.ChainResolvers(localResolvers...), // Resolve local directories + packages.ChainResolvers(cfg.resolvers...), // Use user's custom resolvers + packages.NewFSResolver(exampleRoot), // Ultimately use fs resolver ) // Enrich resolver with middleware @@ -68,7 +79,7 @@ func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, path, dir string) p packages.FilterPathMiddleware("stdlib", isStdPath), // Filter stdlib package from resolving packages.PackageCheckerMiddleware(logger), // Pre-check syntax to avoid bothering the node reloading on invalid files packages.LogMiddleware(logger), // Log any request - ) + ), paths } func guessPathGnoMod(dir string) (path string, ok bool) { @@ -80,12 +91,15 @@ func guessPathGnoMod(dir string) (path string, ok bool) { return "", false } +var reInvalidChar = regexp.MustCompile(`[^\w_-]`) + func guessPath(cfg *devCfg, dir string) (path string) { if path, ok := guessPathGnoMod(dir); ok { return path } - return filepath.Join(cfg.chainDomain, "/r/dev/myrealm") + rname := reInvalidChar.ReplaceAllString(filepath.Base(dir), "-") + return filepath.Join(cfg.chainDomain, "/r/dev/", rname) } func isStdPath(path string) bool { diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index 871ac1752e1..722130a1dae 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -15,7 +15,7 @@ import ( ) // setupDevNode initializes and returns a new DevNode. -func setupDevNode(ctx context.Context, cfg *devCfg, nodeConfig *gnodev.NodeConfig, path string) (*gnodev.Node, error) { +func setupDevNode(ctx context.Context, cfg *devCfg, nodeConfig *gnodev.NodeConfig, paths ...string) (*gnodev.Node, error) { logger := nodeConfig.Logger if cfg.txsFile != "" { // Load txs files @@ -43,7 +43,11 @@ func setupDevNode(ctx context.Context, cfg *devCfg, nodeConfig *gnodev.NodeConfi logger.Info("genesis file loaded", "path", cfg.genesisFile, "txs", len(stateTxs)) } - paths := append(cfg.paths.Strings(), path) + logger.Info("packages", "paths", paths) + if len(paths) == 0 { + logger.Warn("no path to load") + } + return gnodev.NewDevNode(ctx, nodeConfig, paths...) } diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index dcb17afabcd..d272339547f 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -549,7 +549,7 @@ func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result if !res.IsErr() { for _, msg := range tx.Msgs { if addpkg, ok := msg.(vm.MsgAddPackage); ok && addpkg.Package != nil { - n.logger.Info("package added", + n.logger.Info("add package", "path", addpkg.Package.Path, "files", len(addpkg.Package.Files), "creator", addpkg.Creator.String(), diff --git a/contribs/gnodev/pkg/logger/log_column.go b/contribs/gnodev/pkg/logger/log_column.go index b034d8b878d..5217c2dacd7 100644 --- a/contribs/gnodev/pkg/logger/log_column.go +++ b/contribs/gnodev/pkg/logger/log_column.go @@ -10,10 +10,9 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" - "github.com/muesli/termenv" ) -func NewColumnLogger(w io.Writer, level slog.Level, profile termenv.Profile) *ColumnLogger { +func NewColumnLogger(w io.Writer, level slog.Level) *ColumnLogger { charmLogger := log.NewWithOptions(w, log.Options{ ReportTimestamp: false, ReportCaller: false, @@ -21,11 +20,12 @@ func NewColumnLogger(w io.Writer, level slog.Level, profile termenv.Profile) *Co }) // Default column output - renderer := lipgloss.NewRenderer(nil, termenv.WithProfile(profile)) + renderer := lipgloss.DefaultRenderer() + defaultOutput := newColumeWriter(w, lipgloss.NewStyle(), "") charmLogger.SetOutput(defaultOutput) charmLogger.SetStyles(defaultStyles()) - charmLogger.SetColorProfile(profile) + // charmLogger.SetColorProfile(profile) charmLogger.SetReportCaller(false) switch level { case slog.LevelDebug: From 6ef3685fa16ab3eb1783f0ace5cc96e294dc6821 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 16 Dec 2024 01:10:43 +0100 Subject: [PATCH 12/24] fix: minor fix Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/app.go | 12 ++++++------ contribs/gnodev/cmd/gnodev/command_staging.go | 2 +- contribs/gnodev/cmd/gnodev/main.go | 15 +++++++++++---- contribs/gnodev/cmd/gnodev/setup_loader.go | 5 +++-- contribs/gnodev/pkg/packages/package.go | 12 ------------ contribs/gnodev/pkg/packages/resolver_local.go | 2 +- 6 files changed, 22 insertions(+), 26 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/app.go b/contribs/gnodev/cmd/gnodev/app.go index 612fe895c88..9a44448b0c5 100644 --- a/contribs/gnodev/cmd/gnodev/app.go +++ b/contribs/gnodev/cmd/gnodev/app.go @@ -26,7 +26,7 @@ type App struct { io commands.IO logger *slog.Logger - webHome string + webHomePath string paths []string devNode *gnodev.Node emitterServer *emitter.Server @@ -115,11 +115,11 @@ func (ds *App) Setup(ctx context.Context, dirs ...string) error { switch webHome := ds.cfg.webHome; webHome { case "": if len(localPaths) > 0 { - ds.webHome = strings.TrimPrefix(localPaths[0], ds.cfg.chainDomain) + ds.webHomePath = strings.TrimPrefix(localPaths[0], ds.cfg.chainDomain) } - case "/": // skip + case "/", ":none:": // skip default: - ds.webHome = webHome + ds.webHomePath = webHome } // generate paths @@ -158,11 +158,11 @@ func (ds *App) setupHandlers() http.Handler { mux := http.NewServeMux() webhandler := setupGnoWebServer(ds.logger.WithGroup(WebLogName), ds.cfg, ds.devNode) - if ds.webHome != "" { + if ds.webHomePath != "" { serveWeb := webhandler.ServeHTTP webhandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "" || r.URL.Path == "/" { - http.Redirect(w, r, ds.webHome, http.StatusFound) + http.Redirect(w, r, ds.webHomePath, http.StatusFound) } else { serveWeb(w, r) } diff --git a/contribs/gnodev/cmd/gnodev/command_staging.go b/contribs/gnodev/cmd/gnodev/command_staging.go index fc6c420da31..21a9f8c2b6a 100644 --- a/contribs/gnodev/cmd/gnodev/command_staging.go +++ b/contribs/gnodev/cmd/gnodev/command_staging.go @@ -18,7 +18,7 @@ var defaultStagingOptions = devCfg{ chainDomain: DefaultDomain, logFormat: "json", maxGas: 10_000_000_000, - webHome: "/", + webHome: ":none:", webListenerAddr: "127.0.0.1:8888", nodeRPCListenerAddr: "127.0.0.1:26657", deployKey: DefaultDeployerAddress.String(), diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 2cf8e2af11b..611d028873f 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -151,24 +151,31 @@ func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { fs.StringVar( &c.webListenerAddr, "web-listener", - defaultDevOptions.webListenerAddr, + defaultCfg.webListenerAddr, "gnoweb: web server listener address", ) fs.StringVar( &c.webRemoteHelperAddr, "web-help-remote", - defaultDevOptions.webRemoteHelperAddr, + defaultCfg.webRemoteHelperAddr, "gnoweb: web server help page's remote addr (default to )", ) fs.BoolVar( &c.webWithHTML, "web-with-html", - defaultDevOptions.webWithHTML, + defaultCfg.webWithHTML, "gnoweb: enable HTML parsing in markdown rendering", ) + fs.StringVar( + &c.webHome, + "web-home", + defaultCfg.webHome, + "gnoweb: set default home page, use `/` or `:none:` to use default web home redirect", + ) + fs.Var( &c.resolvers, "resolver", @@ -233,7 +240,7 @@ func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { fs.StringVar( &c.chainDomain, "chain-domain", - defaultDevOptions.chainDomain, + defaultCfg.chainDomain, "set node ChainDomain", ) diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go index d0a63f89474..d4f3a047466 100644 --- a/contribs/gnodev/cmd/gnodev/setup_loader.go +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -59,10 +59,11 @@ func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, dirs ...string) (pa var paths []string for _, dir := range dirs { path := guessPath(cfg, dir) - localResolvers = append(localResolvers, packages.NewLocalResolver(path, dir)) - paths = append(paths, path) + resolver := packages.NewLocalResolver(path, dir) logger.Info("guessing directory path", "path", path, "dir", dir) + paths = append(paths, path) + localResolvers = append(localResolvers, resolver) } resolver := packages.ChainResolvers( diff --git a/contribs/gnodev/pkg/packages/package.go b/contribs/gnodev/pkg/packages/package.go index d5717e5eba5..62db1aa7c33 100644 --- a/contribs/gnodev/pkg/packages/package.go +++ b/contribs/gnodev/pkg/packages/package.go @@ -58,18 +58,6 @@ func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) return nil, fmt.Errorf("unable to read file %q: %w", filepath, err) } - if isModFile(fname) { - file, err := gnomod.Parse(fname, body) - if err != nil { - return nil, fmt.Errorf("unable to read `gno.mod`: %w", err) - } - - // Skip draft package - if file.Draft { - return nil, ErrResolverPackageSkip - } - } - if isGnoFile(fname) { memfile, pkgname, err := parseFile(fset, fname, body) if err != nil { diff --git a/contribs/gnodev/pkg/packages/resolver_local.go b/contribs/gnodev/pkg/packages/resolver_local.go index d87f146786a..a381bdf9506 100644 --- a/contribs/gnodev/pkg/packages/resolver_local.go +++ b/contribs/gnodev/pkg/packages/resolver_local.go @@ -34,7 +34,7 @@ func (r LocalResolver) Resolve(fset *token.FileSet, path string) (*Package, erro pkg, err := ReadPackageFromDir(fset, path, dir) if err != nil && after == "" && errors.Is(err, ErrResolverPackageSkip) { - return nil, fmt.Errorf("empty local package %w", err) // local package cannot be empty + return nil, fmt.Errorf("empty local package %q", r.Dir) // local package cannot be empty } return pkg, nil From 0a1695c6d64ef5abc84556497340d87bcc86f713 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sun, 5 Jan 2025 17:53:36 +0100 Subject: [PATCH 13/24] feat: add lazy proxy Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/app.go | 196 +++++++++------ contribs/gnodev/cmd/gnodev/command_staging.go | 1 + contribs/gnodev/cmd/gnodev/logger.go | 5 +- contribs/gnodev/cmd/gnodev/main.go | 24 +- contribs/gnodev/cmd/gnodev/setup_loader.go | 15 +- contribs/gnodev/cmd/gnodev/setup_node.go | 11 +- contribs/gnodev/cmd/gnodev/setup_web.go | 5 +- contribs/gnodev/pkg/dev/node.go | 9 +- contribs/gnodev/pkg/emitter/server.go | 4 + .../gnodev/pkg/emitter/static/hotreload.js | 37 ++- contribs/gnodev/pkg/logger/log_column.go | 26 +- contribs/gnodev/pkg/packages/loader.go | 11 +- contribs/gnodev/pkg/packages/loader_glob.go | 5 + contribs/gnodev/pkg/packages/package.go | 4 + .../gnodev/pkg/packages/resolver_local.go | 14 +- contribs/gnodev/pkg/proxy/path_interceptor.go | 230 ++++++++++++++++++ contribs/gnodev/pkg/rawterm/keypress.go | 14 ++ contribs/gnodev/pkg/rawterm/rawterm.go | 25 +- 18 files changed, 508 insertions(+), 128 deletions(-) create mode 100644 contribs/gnodev/pkg/proxy/path_interceptor.go diff --git a/contribs/gnodev/cmd/gnodev/app.go b/contribs/gnodev/cmd/gnodev/app.go index 9a44448b0c5..763fac6022e 100644 --- a/contribs/gnodev/cmd/gnodev/app.go +++ b/contribs/gnodev/cmd/gnodev/app.go @@ -15,6 +15,7 @@ import ( gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/contribs/gnodev/pkg/proxy" "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" "github.com/gnolang/gno/tm2/pkg/commands" @@ -22,34 +23,36 @@ import ( ) type App struct { - cfg *devCfg - io commands.IO - logger *slog.Logger - + cfg *devCfg + io commands.IO + logger *slog.Logger webHomePath string paths []string devNode *gnodev.Node emitterServer *emitter.Server watcher *watcher.PackageWatcher + loader packages.Loader book *address.Book exportPath string + proxy *proxy.PathInterceptor + pathManager *pathManager + + // Contains all the deferred functions of the app. + // Will be triggered on close for cleanup. + deferred func() // XXX: move this exported uint } -func runApp(cfg *devCfg, cio commands.IO, dirs ...string) error { +func runApp(cfg *devCfg, cio commands.IO, dirs ...string) (err error) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() var rt *rawterm.RawTerm - var out io.Writer if cfg.interactive { - var err error var restore func() error - - // Setup raw terminal for interaction rt, restore, err = setupRawTerm(cfg, cio) if err != nil { return fmt.Errorf("unable to init raw term: %w", err) @@ -76,6 +79,7 @@ func runApp(cfg *devCfg, cio commands.IO, dirs ...string) error { if err := app.Setup(ctx, dirs...); err != nil { return err } + defer app.Close() if rt != nil { go func() { @@ -89,27 +93,49 @@ func runApp(cfg *devCfg, cio commands.IO, dirs ...string) error { func NewApp(logger *slog.Logger, cfg *devCfg, io commands.IO) *App { return &App{ - logger: logger, - cfg: cfg, - io: io, + deferred: func() {}, + logger: logger, + cfg: cfg, + io: io, + pathManager: newPathManager(), } } -func (ds *App) Setup(ctx context.Context, dirs ...string) error { - var err error +func (ds *App) Defer(fn func()) { + old := ds.deferred + ds.deferred = func() { + defer old() + fn() + } +} - if err = ds.cfg.validateConfigFlags(); err != nil { +func (ds *App) DeferClose(fn func() error) { + ds.Defer(func() { + if err := fn(); err != nil { + ds.logger.Error("close", "error", err.Error()) + } + }) +} + +func (ds *App) Close() { + ds.deferred() +} + +func (ds *App) Setup(ctx context.Context, dirs ...string) (err error) { + if err := ds.cfg.validateConfigFlags(); err != nil { return fmt.Errorf("validate error: %w", err) } loggerEvents := ds.logger.WithGroup(EventServerLogName) ds.emitterServer = emitter.NewServer(loggerEvents) - // XXX: it would be nice to having this hardcoded + // XXX: it would be nice to not have this hardcoded examplesDir := filepath.Join(ds.cfg.root, "examples") - resolver, localPaths := setupPackagesResolver(ds.logger.WithGroup(LoaderLogName), ds.cfg, dirs...) - loader := packages.NewGlobLoader(examplesDir, resolver) + // Setup loader and resolver + loaderLogger := ds.logger.WithGroup(LoaderLogName) + resolver, localPaths := setupPackagesResolver(loaderLogger, ds.cfg, dirs...) + ds.loader = packages.NewGlobLoader(examplesDir, resolver) // Setup default web home realm, fallback on first local path switch webHome := ds.cfg.webHome; webHome { @@ -122,11 +148,12 @@ func (ds *App) Setup(ctx context.Context, dirs ...string) error { ds.webHomePath = webHome } - // generate paths + // Generate paths paths := strings.Split(ds.cfg.paths, ",") ds.paths = append(localPaths, paths...) - ds.book, err = setupAddressBook(ds.logger.WithGroup(AccountsLogName), ds.cfg) + accountLogger := ds.logger.WithGroup(AccountsLogName) + ds.book, err = setupAddressBook(accountLogger, ds.cfg) if err != nil { return fmt.Errorf("unable to load keybase: %w", err) } @@ -138,11 +165,30 @@ func (ds *App) Setup(ctx context.Context, dirs ...string) error { ds.logger.Debug("balances loaded", "list", balances.List()) nodeLogger := ds.logger.WithGroup(NodeLogName) - nodeCfg := setupDevNodeConfig(ds.cfg, nodeLogger, ds.emitterServer, balances, loader) + nodeCfg := setupDevNodeConfig(ds.cfg, nodeLogger, ds.emitterServer, balances, ds.loader) + + address := resolveUnixOrTCPAddr(nodeCfg.TMConfig.RPC.ListenAddress) + + // Setup lazy proxy + if ds.cfg.lazyLoader { + proxyLogger := ds.logger.WithGroup(ProxyLogName) + ds.proxy, err = proxy.NewPathInterceptor(proxyLogger, address) + if err != nil { + return fmt.Errorf("unable to setup proxy: %w", err) + } + ds.DeferClose(ds.proxy.Close) + + // Override current rpc listener + nodeCfg.TMConfig.RPC.ListenAddress = ds.proxy.ProxyAddress() + } else { + nodeCfg.TMConfig.RPC.ListenAddress = fmt.Sprintf("%s://%s", address.Network(), address.String()) + } + ds.devNode, err = setupDevNode(ctx, ds.cfg, nodeCfg, ds.paths...) if err != nil { return err } + ds.DeferClose(ds.devNode.Close) ds.watcher, err = watcher.NewPackageWatcher(loggerEvents, ds.emitterServer) if err != nil { @@ -154,9 +200,53 @@ func (ds *App) Setup(ctx context.Context, dirs ...string) error { return nil } -func (ds *App) setupHandlers() http.Handler { +func (ds *App) setupHandlers(ctx context.Context) http.Handler { mux := http.NewServeMux() - webhandler := setupGnoWebServer(ds.logger.WithGroup(WebLogName), ds.cfg, ds.devNode) + remote := ds.devNode.GetRemoteAddress() + + if ds.proxy != nil { + proxyLogger := ds.logger.WithGroup(ProxyLogName) + remote = ds.proxy.TargetAddress() // update remote address with proxy target address + ds.proxy.HandlePath(func(paths ...string) { + new := false + for _, path := range paths { + // Try to resolve the path first. + // If we are unable to resolve it, ignore and continue + if _, err := ds.loader.Resolve(path); err != nil { + proxyLogger.Debug("unable to resolve path", + "error", err, + "path", path) + continue + } + + // If we already know this path, continue. + if exist := ds.pathManager.Save(path); exist { + continue + } + + proxyLogger.Info("new monitored path", + "path", path) + + new = true + } + + if !new { + return + } + + ds.emitterServer.LockEmit() + defer ds.emitterServer.UnlockEmit() + + ds.devNode.SetPackagePaths(ds.paths...) + ds.devNode.AddPackagePaths(ds.pathManager.List()...) + + // Reload node on new path + if err := ds.devNode.Reload(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } + }) + } + webhandler := setupGnoWebServer(ds.logger.WithGroup(WebLogName), ds.cfg, remote) if ds.webHomePath != "" { serveWeb := webhandler.ServeHTTP @@ -169,7 +259,7 @@ func (ds *App) setupHandlers() http.Handler { }) } - // Setup unsage api + // Setup unsafe API if ds.cfg.unsafeAPI { mux.HandleFunc("/reset", func(res http.ResponseWriter, req *http.Request) { if err := ds.devNode.Reset(req.Context()); err != nil { @@ -205,9 +295,9 @@ func (ds *App) RunServer(ctx context.Context, term *rawterm.RawTerm) error { ds.logger.WithGroup(WebLogName).Info("gnoweb started", "lisn", fmt.Sprintf("http://%s", addr)) server := &http.Server{ - Handler: ds.setupHandlers(), - Addr: ds.cfg.webListenerAddr, - ReadHeaderTimeout: time.Second * 60, + Handler: ds.setupHandlers(ctx), + Addr: addr, + ReadHeaderTimeout: 60 * time.Second, } go func() { @@ -229,6 +319,7 @@ func (ds *App) RunServer(ctx context.Context, term *rawterm.RawTerm) error { if !ok { return nil } + ds.logger.WithGroup(NodeLogName).Info("reloading...") if err := ds.devNode.Reload(ctx); err != nil { ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) @@ -239,6 +330,7 @@ func (ds *App) RunServer(ctx context.Context, term *rawterm.RawTerm) error { } func (ds *App) RunInteractive(ctx context.Context, term *rawterm.RawTerm) { + ds.logger.WithGroup(KeyPressLogName).Debug("starting interactive mode") var keyPressCh <-chan rawterm.KeyPress if ds.cfg.interactive { keyPressCh = listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) @@ -249,6 +341,7 @@ func (ds *App) RunInteractive(ctx context.Context, term *rawterm.RawTerm) { case <-ctx.Done(): return case key, ok := <-keyPressCh: + ds.logger.WithGroup(KeyPressLogName).Debug("pressed", "key", key.String()) if !ok { return } @@ -279,7 +372,6 @@ Ctrl+C Exit - Exit the application func (ds *App) handleKeyPress(ctx context.Context, key rawterm.KeyPress) { var err error - ds.logger.WithGroup(KeyPressLogName).Debug(fmt.Sprintf("<%s>", key.String())) switch key.Upper() { case rawterm.KeyH: // Helper @@ -295,7 +387,11 @@ func (ds *App) handleKeyPress(ctx context.Context, key rawterm.KeyPress) { } case rawterm.KeyCtrlR: // Reset - ds.logger.WithGroup(NodeLogName).Info("reseting node state...") + ds.logger.WithGroup(NodeLogName).Info("resetting node state...") + // Reset paths + ds.pathManager.Reset() + ds.devNode.SetPackagePaths(ds.paths...) + // Reset the node if err = ds.devNode.Reset(ctx); err != nil { ds.logger.WithGroup(NodeLogName).Error("unable to reset node state", "err", err) } @@ -361,45 +457,3 @@ func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm. return cc } - -// func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ([]gnodev.PackageModifier, error) { -// modifiers := make([]gnodev.PackageModifier, 0, len(args)) - -// if cfg.deployKey == "" { -// return nil, fmt.Errorf("default deploy key cannot be empty") -// } - -// defaultKey, _, ok := bk.GetFromNameOrAddress(cfg.deployKey) -// if !ok { -// return nil, fmt.Errorf("unable to get deploy key %q", cfg.deployKey) -// } - -// if len(args) == 0 { -// args = append(args, ".") // add current dir if none are provided -// } - -// for _, arg := range args { -// path, err := gnodev.ResolvePackageModifierQuery(bk, arg) -// if err != nil { -// return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) -// } - -// // Assign a default creator if user haven't specified it. -// if path.Creator.IsZero() { -// path.Creator = defaultKey -// } - -// modifiers = append(modifiers, path) -// } - -// // Add examples folder if minimal is set to false -// if cfg.minimal { -// modifiers = append(modifiers, gnodev.PackageModifier{ -// Path: filepath.Join(cfg.root, "examples"), -// Creator: defaultKey, -// Deposit: nil, -// }) -// } - -// return modifiers, nil -// } diff --git a/contribs/gnodev/cmd/gnodev/command_staging.go b/contribs/gnodev/cmd/gnodev/command_staging.go index 21a9f8c2b6a..bcae4cab857 100644 --- a/contribs/gnodev/cmd/gnodev/command_staging.go +++ b/contribs/gnodev/cmd/gnodev/command_staging.go @@ -26,6 +26,7 @@ var defaultStagingOptions = devCfg{ root: gnoenv.RootDir(), interactive: false, unsafeAPI: false, + lazyLoader: false, paths: filepath.Join(DefaultDomain, "/**"), // Load every package under the main domain}, // As we have no reason to configure this yet, set this to random port diff --git a/contribs/gnodev/cmd/gnodev/logger.go b/contribs/gnodev/cmd/gnodev/logger.go index 6712e6a3cf4..59d73c8abe9 100644 --- a/contribs/gnodev/cmd/gnodev/logger.go +++ b/contribs/gnodev/cmd/gnodev/logger.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/gnolang/gno/contribs/gnodev/pkg/logger" "github.com/gnolang/gno/gno.land/pkg/log" + "github.com/muesli/termenv" "go.uber.org/zap/zapcore" ) @@ -23,7 +24,9 @@ func setuplogger(cfg *devCfg, out io.Writer) (*slog.Logger, error) { return newJSONLogger(out, level), nil case "console", "": // Detect term color profile - clogger := logger.NewColumnLogger(out, level) + colorProfile := termenv.DefaultOutput().Profile + + clogger := logger.NewColumnLogger(out, level, colorProfile) // Register well known group color with system colors clogger.RegisterGroupColor(NodeLogName, lipgloss.Color("3")) diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 611d028873f..72d425eed6c 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -28,6 +28,7 @@ const ( EventServerLogName = "Event" AccountsLogName = "Accounts" LoaderLogName = "Loader" + ProxyLogName = "Proxy" ) var ErrConflictingFileArgs = errors.New("cannot specify `balances-file` or `txs-file` along with `genesis-file`") @@ -62,6 +63,7 @@ type devCfg struct { // Node Configuration logFormat string + lazyLoader bool verbose bool noWatch bool noReplay bool @@ -85,6 +87,7 @@ var defaultDevOptions = devCfg{ root: gnoenv.RootDir(), interactive: true, unsafeAPI: true, + lazyLoader: true, // As we have no reason to configure this yet, set this to random port // to avoid potential conflict with other app @@ -134,13 +137,6 @@ func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { "enable gnodev interactive mode", ) - fs.StringVar( - &c.chdir, - "chdir", - defaultCfg.chdir, - "change directory context", - ) - fs.StringVar( &c.root, "root", @@ -258,6 +254,13 @@ func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { "do not replay previous transactions upon reload", ) + fs.BoolVar( + &c.lazyLoader, + "lazy-loader", + defaultCfg.lazyLoader, + "enable lazy loader", + ) + fs.Int64Var( &c.maxGas, "max-gas", @@ -287,6 +290,13 @@ func (c *devCfg) registerFlagsWithDefault(defaultCfg devCfg, fs *flag.FlagSet) { ) // Short flags + fs.StringVar( + &c.chdir, + "C", + defaultCfg.chdir, + "change directory context before running gnodev", + ) + fs.BoolVar( &c.verbose, "v", diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go index d4f3a047466..5d1e792b7cd 100644 --- a/contribs/gnodev/cmd/gnodev/setup_loader.go +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -61,25 +61,30 @@ func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, dirs ...string) (pa path := guessPath(cfg, dir) resolver := packages.NewLocalResolver(path, dir) - logger.Info("guessing directory path", "path", path, "dir", dir) - paths = append(paths, path) + if resolver.IsValid() { + logger.Info("guessing directory path", "path", path, "dir", dir) + paths = append(paths, path) // append local path + } else { + logger.Warn("invalid local path", "dir", dir) + } + localResolvers = append(localResolvers, resolver) } resolver := packages.ChainResolvers( packages.ChainResolvers(localResolvers...), // Resolve local directories packages.ChainResolvers(cfg.resolvers...), // Use user's custom resolvers - packages.NewFSResolver(exampleRoot), // Ultimately use fs resolver + packages.NewFSResolver(exampleRoot), // Ultimately use fs resolver from example folder ) // Enrich resolver with middleware return packages.MiddlewareResolver(resolver, packages.CacheMiddleware(func(pkg *packages.Package) bool { - return pkg.Kind == packages.PackageKindRemote // Cache only remote package + return pkg.Kind == packages.PackageKindRemote // Only cache remote package }), packages.FilterPathMiddleware("stdlib", isStdPath), // Filter stdlib package from resolving packages.PackageCheckerMiddleware(logger), // Pre-check syntax to avoid bothering the node reloading on invalid files - packages.LogMiddleware(logger), // Log any request + packages.LogMiddleware(logger), // Log request ), paths } diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index 722130a1dae..9f9b0394f54 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -66,7 +66,7 @@ func setupDevNodeConfig( config.Emitter = emitter config.BalancesList = balances.List() // config.PackagesModifier = pkgspath - config.TMConfig.RPC.ListenAddress = resolveUnixOrTCPAddr(cfg.nodeRPCListenerAddr) + config.TMConfig.RPC.ListenAddress = cfg.nodeRPCListenerAddr config.NoReplay = cfg.noReplay config.MaxGasPerBlock = cfg.maxGas config.ChainID = cfg.chainId @@ -92,21 +92,20 @@ func extractAppStateFromGenesisFile(path string) (*gnoland.GnoGenesisState, erro return &state, nil } -func resolveUnixOrTCPAddr(in string) (out string) { +func resolveUnixOrTCPAddr(in string) (addr net.Addr) { var err error - var addr net.Addr if strings.HasPrefix(in, "unix://") { in = strings.TrimPrefix(in, "unix://") - if addr, err := net.ResolveUnixAddr("unix", in); err == nil { - return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) + if addr, err = net.ResolveUnixAddr("unix", in); err == nil { + return addr } err = fmt.Errorf("unable to resolve unix address `unix://%s`: %w", in, err) } else { // don't bother to checking prefix in = strings.TrimPrefix(in, "tcp://") if addr, err = net.ResolveTCPAddr("tcp", in); err == nil { - return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) + return addr } err = fmt.Errorf("unable to resolve tcp address `tcp://%s`: %w", in, err) diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go index d55814142a6..47462d170ad 100644 --- a/contribs/gnodev/cmd/gnodev/setup_web.go +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -4,16 +4,15 @@ import ( "log/slog" "net/http" - gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/gno.land/pkg/gnoweb" ) // setupGnowebServer initializes and starts the Gnoweb server. -func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) http.Handler { +func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, remoteAddr string) http.Handler { webConfig := gnoweb.NewDefaultConfig() webConfig.HelpChainID = cfg.chainId - webConfig.RemoteAddr = dnode.GetRemoteAddress() + webConfig.RemoteAddr = remoteAddr webConfig.HelpRemote = cfg.webRemoteHelperAddr webConfig.WithHTML = cfg.webWithHTML diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index d272339547f..cf9ee7324ac 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -198,6 +198,13 @@ func (n *Node) AddPackagePaths(paths ...string) { n.paths = append(n.paths, paths...) } +func (n *Node) SetPackagePaths(paths ...string) { + n.muNode.Lock() + defer n.muNode.Unlock() + + n.paths = paths +} + // GetBlockTransactions returns the transactions contained // within the specified block, if any func (n *Node) GetBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) { @@ -549,7 +556,7 @@ func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result if !res.IsErr() { for _, msg := range tx.Msgs { if addpkg, ok := msg.(vm.MsgAddPackage); ok && addpkg.Package != nil { - n.logger.Info("add package", + n.logger.Debug("add package", "path", addpkg.Package.Path, "files", len(addpkg.Package.Files), "creator", addpkg.Creator.String(), diff --git a/contribs/gnodev/pkg/emitter/server.go b/contribs/gnodev/pkg/emitter/server.go index 3e32984268d..9f046d87eb8 100644 --- a/contribs/gnodev/pkg/emitter/server.go +++ b/contribs/gnodev/pkg/emitter/server.go @@ -32,6 +32,10 @@ func NewServer(logger *slog.Logger) *Server { } } +func (s *Server) LockEmit() { s.muClients.Lock() } + +func (s *Server) UnlockEmit() { s.muClients.Unlock() } + // ws handler func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { conn, err := s.upgrader.Upgrade(w, r, nil) diff --git a/contribs/gnodev/pkg/emitter/static/hotreload.js b/contribs/gnodev/pkg/emitter/static/hotreload.js index aabad4f341c..015d8dfeb8e 100644 --- a/contribs/gnodev/pkg/emitter/static/hotreload.js +++ b/contribs/gnodev/pkg/emitter/static/hotreload.js @@ -1,17 +1,32 @@ -(function() { +document.addEventListener('DOMContentLoaded', function() { // Define the events that will trigger a page reload const eventsReload = {{ .ReloadEvents | json }}; - + // Establish the WebSocket connection to the event server const ws = new WebSocket('ws://{{- .Remote -}}'); - + // `gracePeriod` mitigates reload loops due to excessive events. This period // occurs post-loading and lasts for the `graceTimeout` duration. const graceTimeout = 1000; // ms let gracePeriod = true; let debounceTimeout = setTimeout(function() { gracePeriod = false; - }, graceTimeout); + }, graceTimeout); + + // Flag to track if a link click is in progress + let clickInProgress = false; + + // Capture clicks on tags to prevent reloading appening when clicking on link + document.addEventListener('click', function(event) { + const target = event.target; + if (target.tagName === 'A' && target.href) { + clickInProgress = true; + // Wait a bit before allowing reload again + setTimeout(function() { + clickInProgress = false; + }, 5000); + } + }); // Handle incoming WebSocket messages ws.onmessage = function(event) { @@ -21,19 +36,21 @@ // Ignore events not in the reload-triggering list if (!eventsReload.includes(message.type)) { - return; + return; } - // Reload the page immediately if we're not in the grace period - if (!gracePeriod) { + // Reload the page immediately if we're not in the grace period and no clicks are in progress + if (!gracePeriod && !clickInProgress) { window.location.reload(); return; } - // If still in the grace period, debounce the reload + // If still in the grace period or a click is in progress, debounce the reload clearTimeout(debounceTimeout); debounceTimeout = setTimeout(function() { - window.location.reload(); + if (!clickInProgress) { + window.location.reload(); + } }, graceTimeout); } catch (e) { @@ -48,4 +65,4 @@ ws.onclose = function() { console.log('WebSocket connection closed'); }; -})(); +}); diff --git a/contribs/gnodev/pkg/logger/log_column.go b/contribs/gnodev/pkg/logger/log_column.go index 5217c2dacd7..0e6c181ad6d 100644 --- a/contribs/gnodev/pkg/logger/log_column.go +++ b/contribs/gnodev/pkg/logger/log_column.go @@ -10,9 +10,10 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" + "github.com/muesli/termenv" ) -func NewColumnLogger(w io.Writer, level slog.Level) *ColumnLogger { +func NewColumnLogger(w io.Writer, level slog.Level, profile termenv.Profile) *ColumnLogger { charmLogger := log.NewWithOptions(w, log.Options{ ReportTimestamp: false, ReportCaller: false, @@ -20,12 +21,12 @@ func NewColumnLogger(w io.Writer, level slog.Level) *ColumnLogger { }) // Default column output - renderer := lipgloss.DefaultRenderer() + renderer := lipgloss.NewRenderer(nil, termenv.WithProfile(profile)) defaultOutput := newColumeWriter(w, lipgloss.NewStyle(), "") charmLogger.SetOutput(defaultOutput) charmLogger.SetStyles(defaultStyles()) - // charmLogger.SetColorProfile(profile) + charmLogger.SetColorProfile(profile) charmLogger.SetReportCaller(false) switch level { case slog.LevelDebug: @@ -41,20 +42,22 @@ func NewColumnLogger(w io.Writer, level slog.Level) *ColumnLogger { } return &ColumnLogger{ - Logger: charmLogger, - writer: w, - prefix: charmLogger.GetPrefix(), - colors: map[string]lipgloss.Color{}, - renderer: renderer, + Logger: charmLogger, + writer: w, + prefix: charmLogger.GetPrefix(), + colors: map[string]lipgloss.Color{}, + colorProfile: profile, + renderer: renderer, } } type ColumnLogger struct { *log.Logger - prefix string - writer io.Writer - renderer *lipgloss.Renderer + prefix string + writer io.Writer + renderer *lipgloss.Renderer + colorProfile termenv.Profile colors map[string]lipgloss.Color muColors sync.RWMutex @@ -79,6 +82,7 @@ func (cl *ColumnLogger) WithGroup(group string) slog.Handler { nlog := cl.Logger.With() // clone logger nlog.SetOutput(newColumeWriter(cl.writer, baseStyle, group)) + nlog.SetColorProfile(cl.colorProfile) return &ColumnLogger{ Logger: nlog, prefix: group, diff --git a/contribs/gnodev/pkg/packages/loader.go b/contribs/gnodev/pkg/packages/loader.go index b281e9058ae..0447f6e309f 100644 --- a/contribs/gnodev/pkg/packages/loader.go +++ b/contribs/gnodev/pkg/packages/loader.go @@ -7,10 +7,12 @@ import ( "go/token" ) -var ErrNoResolvers = errors.New("no resolvers setup") - type Loader interface { + // Load resolves package package paths and all their dependencies in the correct order. Load(paths ...string) ([]Package, error) + + // Resolve processes a single package path and returns the corresponding Package. + Resolve(path string) (*Package, error) } type BaseLoader struct { @@ -36,6 +38,11 @@ func (l BaseLoader) Load(paths ...string) ([]Package, error) { return pkgs, nil } +func (l BaseLoader) Resolve(path string) (*Package, error) { + fset := token.NewFileSet() + return l.Resolver.Resolve(fset, path) +} + func load(path string, fset *token.FileSet, resolver Resolver, visited, stack map[string]bool) ([]Package, error) { if stack[path] { return nil, fmt.Errorf("cycle detected: %s", path) diff --git a/contribs/gnodev/pkg/packages/loader_glob.go b/contribs/gnodev/pkg/packages/loader_glob.go index 1cc972fa9ba..685c8a18d4e 100644 --- a/contribs/gnodev/pkg/packages/loader_glob.go +++ b/contribs/gnodev/pkg/packages/loader_glob.go @@ -2,6 +2,7 @@ package packages import ( "fmt" + "go/token" "io/fs" "os" "path/filepath" @@ -76,3 +77,7 @@ func (l GlobLoader) Load(gpaths ...string) ([]Package, error) { loader := &BaseLoader{Resolver: l.Resolver} return loader.Load(paths...) } + +func (l GlobLoader) Resolve(path string) (*Package, error) { + return l.Resolver.Resolve(token.NewFileSet(), path) +} diff --git a/contribs/gnodev/pkg/packages/package.go b/contribs/gnodev/pkg/packages/package.go index 62db1aa7c33..6798aefbaf3 100644 --- a/contribs/gnodev/pkg/packages/package.go +++ b/contribs/gnodev/pkg/packages/package.go @@ -29,6 +29,10 @@ type Package struct { func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) { files, err := os.ReadDir(dir) if err != nil { + if os.IsNotExist(err) { + return nil, ErrResolverPackageNotFound + } + return nil, fmt.Errorf("unable to read dir %q: %w", dir, err) } diff --git a/contribs/gnodev/pkg/packages/resolver_local.go b/contribs/gnodev/pkg/packages/resolver_local.go index a381bdf9506..13448aca52d 100644 --- a/contribs/gnodev/pkg/packages/resolver_local.go +++ b/contribs/gnodev/pkg/packages/resolver_local.go @@ -1,7 +1,6 @@ package packages import ( - "errors" "fmt" "go/token" "path/filepath" @@ -24,6 +23,11 @@ func (r *LocalResolver) Name() string { return fmt.Sprintf("local<%s>", filepath.Base(r.Dir)) } +func (r LocalResolver) IsValid() bool { + pkg, err := r.Resolve(token.NewFileSet(), r.Path) + return err == nil && pkg != nil +} + func (r LocalResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { after, found := strings.CutPrefix(path, r.Path) if !found { @@ -31,11 +35,5 @@ func (r LocalResolver) Resolve(fset *token.FileSet, path string) (*Package, erro } dir := filepath.Join(r.Dir, after) - pkg, err := ReadPackageFromDir(fset, path, dir) - - if err != nil && after == "" && errors.Is(err, ErrResolverPackageSkip) { - return nil, fmt.Errorf("empty local package %q", r.Dir) // local package cannot be empty - } - - return pkg, nil + return ReadPackageFromDir(fset, path, dir) } diff --git a/contribs/gnodev/pkg/proxy/path_interceptor.go b/contribs/gnodev/pkg/proxy/path_interceptor.go new file mode 100644 index 00000000000..15aebf7419f --- /dev/null +++ b/contribs/gnodev/pkg/proxy/path_interceptor.go @@ -0,0 +1,230 @@ +package proxy + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "net/url" + "strings" + "sync" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/amino" + rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type PathHandler func(path ...string) + +type PathInterceptor struct { + proxyAddr, targetAddr net.Addr + + logger *slog.Logger + listener net.Listener + handlers []PathHandler + muHandlers sync.RWMutex +} + +// NewPathInterceptor creates a proxy loader with a logger and target address. +func NewPathInterceptor(logger *slog.Logger, target net.Addr) (*PathInterceptor, error) { + // Create a lisnener with target addr + porxyListener, err := net.Listen(target.Network(), target.String()) + if err != nil { + return nil, fmt.Errorf("failed to listen on %s://%s", target.Network(), target.String()) + } + + // Find a new random port for the target + targertListener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, fmt.Errorf("failed to listen on %s://%s", target.Network(), target.String()) + } + proxyAddr := targertListener.Addr() + // Immedialty close this listener after proxy init + defer targertListener.Close() + + proxy := &PathInterceptor{ + listener: porxyListener, + logger: logger, + targetAddr: target, + proxyAddr: proxyAddr, + } + + go proxy.handleConnections() + + return proxy, nil +} + +// HandlePath adds a new path handler to the interceptor. +func (proxy *PathInterceptor) HandlePath(fn PathHandler) { + proxy.muHandlers.Lock() + defer proxy.muHandlers.Unlock() + proxy.handlers = append(proxy.handlers, fn) +} + +// ProxyAddress returns the network address of the proxy. +func (proxy *PathInterceptor) ProxyAddress() string { + return fmt.Sprintf("%s://%s", proxy.proxyAddr.Network(), proxy.proxyAddr.String()) +} + +// ProxyAddress returns the network address of the proxy. +func (proxy *PathInterceptor) TargetAddress() string { + return fmt.Sprintf("%s://%s", proxy.targetAddr.Network(), proxy.targetAddr.String()) +} + +// handleConnections manages incoming connections to the proxy. +func (proxy *PathInterceptor) handleConnections() { + defer proxy.listener.Close() + + for { + conn, err := proxy.listener.Accept() + if err != nil { + proxy.logger.Debug("failed to accept connection", "error", err) + continue + } + go proxy.handleConnection(conn) + } +} + +// handleConnection processes a single connection. +func (proxy *PathInterceptor) handleConnection(inConn net.Conn) { + defer inConn.Close() + + var buffer bytes.Buffer + teeReader := io.TeeReader(inConn, &buffer) + + defer func() { proxy.forwardRequest(&buffer, inConn) }() + + request, err := http.ReadRequest(bufio.NewReader(teeReader)) + if err != nil { + proxy.logger.Debug("failed to read HTTP request", "error", err) + return + } + + if request.Header.Get("Upgrade") == "websocket" { + proxy.logger.Debug("WebSocket connection detected, forwarding directly") + return + } + + body, err := io.ReadAll(request.Body) + if err != nil { + proxy.logger.Warn("failed to read request body", "error", err) + return + } + defer request.Body.Close() + + if err := proxy.handleRequest(body); err != nil { + proxy.logger.Debug("error handling request", "error", err) + } +} + +// forwardRequest forwards the buffered request to the target address. +func (proxy *PathInterceptor) forwardRequest(buffer *bytes.Buffer, inConn net.Conn) { + outConn, err := net.Dial(proxy.proxyAddr.Network(), proxy.proxyAddr.String()) + if err != nil { + proxy.logger.Error("failed to connect to remote socket", "address", proxy.proxyAddr, "error", err) + return + } + defer outConn.Close() + + if buffer.Len() > 0 { + if _, err := outConn.Write(buffer.Bytes()); err != nil { + proxy.logger.Error("unable to write to socket", "error", err) + return + } + } + + go io.Copy(outConn, inConn) + io.Copy(inConn, outConn) +} + +// handleRequest parses and processes the RPC request body. +func (proxy *PathInterceptor) handleRequest(body []byte) error { + paths, err := parseRPCRequest(body) + if err != nil { + return fmt.Errorf("unable to parse rpc request: %w", err) + } + + if len(paths) == 0 { + return nil + } + proxy.logger.Debug("parsed request paths", "paths", paths) + + proxy.muHandlers.RLock() + defer proxy.muHandlers.RUnlock() + + for _, handle := range proxy.handlers { + handle(paths...) + } + + return nil +} + +// Close closes the proxy listener. +func (proxy *PathInterceptor) Close() error { + return proxy.listener.Close() +} + +type sDataQuery struct { + Path string `json:"path"` + Data []byte `json:"data,omitempty"` +} + +// parseRPCRequest unmarshals and processes RPC requests, returning paths. +func parseRPCRequest(body []byte) ([]string, error) { + var req rpctypes.RPCRequest + if err := json.Unmarshal(body, &req); err != nil { + return nil, fmt.Errorf("unable to unmarshal rpc request: %w", err) + } + + if req.Method != "abci_query" { + return nil, fmt.Errorf("not an abci query") + } + + var query sDataQuery + if err := json.Unmarshal(req.Params, &query); err != nil { + return nil, fmt.Errorf("unable to unmarshal params: %w", err) + } + + return handleQuery(query) +} + +// handleQuery processes the query and returns relevant paths. +func handleQuery(query sDataQuery) ([]string, error) { + var paths []string + + switch query.Path { + case ".app/simulate": + var tx std.Tx + if err := amino.Unmarshal(query.Data, &tx); err != nil { + return nil, fmt.Errorf("unable to unmarshal tx: %w", err) + } + + for _, msg := range tx.Msgs { + switch msg := msg.(type) { + case vm.MsgCall: + paths = append(paths, msg.PkgPath) + case vm.MsgRun: + paths = append(paths, msg.Package.Path) + } + } + return paths, nil + + case "vm/qrender", "vm/qfile": + path, _, _ := strings.Cut(string(query.Data), ":") + u, err := url.Parse(path) + if err != nil { + return nil, fmt.Errorf("unable to parse path: %w", err) + } + return []string{strings.TrimSpace(u.Path)}, nil + + default: + return nil, fmt.Errorf("unhandled: %q", query.Path) + } + + // XXX: handle more cases +} diff --git a/contribs/gnodev/pkg/rawterm/keypress.go b/contribs/gnodev/pkg/rawterm/keypress.go index 45c64c999dd..e9c1728bd4b 100644 --- a/contribs/gnodev/pkg/rawterm/keypress.go +++ b/contribs/gnodev/pkg/rawterm/keypress.go @@ -26,6 +26,12 @@ const ( KeyN KeyPress = 'N' KeyP KeyPress = 'P' KeyR KeyPress = 'R' + + // Special keys + KeyUp KeyPress = 0x80 // Arbitrary value outside ASCII range + KeyDown KeyPress = 0x81 + KeyLeft KeyPress = 0x82 + KeyRight KeyPress = 0x83 ) func (k KeyPress) Upper() KeyPress { @@ -52,6 +58,14 @@ func (k KeyPress) String() string { return "Ctrl+S" case KeyCtrlT: return "Ctrl+T" + case KeyUp: + return "Up Arrow" + case KeyDown: + return "Down Arrow" + case KeyLeft: + return "Left Arrow" + case KeyRight: + return "Right Arrow" default: // For printable ASCII characters if k > 0x20 && k < 0x7e { diff --git a/contribs/gnodev/pkg/rawterm/rawterm.go b/contribs/gnodev/pkg/rawterm/rawterm.go index 58b8dde1530..7ff4cadaf94 100644 --- a/contribs/gnodev/pkg/rawterm/rawterm.go +++ b/contribs/gnodev/pkg/rawterm/rawterm.go @@ -54,12 +54,31 @@ func (rt *RawTerm) read(buf []byte) (n int, err error) { } func (rt *RawTerm) ReadKeyPress() (KeyPress, error) { - buf := make([]byte, 1) - if _, err := rt.read(buf); err != nil { + buf := make([]byte, 3) + n, err := rt.read(buf) + if err != nil { return KeyNone, err } - return KeyPress(buf[0]), nil + if n == 1 && buf[0] != '\x1b' { + // Single character, not an escape sequence + return KeyPress(buf[0]), nil + } + + if n >= 3 && buf[0] == '\x1b' && buf[1] == '[' { + switch buf[2] { + case 'A': + return KeyUp, nil + case 'B': + return KeyDown, nil + case 'C': + return KeyRight, nil + case 'D': + return KeyLeft, nil + } + } + + return KeyNone, fmt.Errorf("unknown key sequence: %v", buf[:n]) } // writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n. From 215802642d919729067ca535a42966c8f139c90a Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Sun, 5 Jan 2025 22:29:22 +0100 Subject: [PATCH 14/24] fix: add missing files Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/path_manager.go | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 contribs/gnodev/cmd/gnodev/path_manager.go diff --git a/contribs/gnodev/cmd/gnodev/path_manager.go b/contribs/gnodev/cmd/gnodev/path_manager.go new file mode 100644 index 00000000000..705e90fe2c4 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/path_manager.go @@ -0,0 +1,45 @@ +package main + +import ( + "sync" +) + +// pathManager manages a set of unique paths. +type pathManager struct { + paths map[string]struct{} + mu sync.RWMutex +} + +func newPathManager() *pathManager { + return &pathManager{ + paths: make(map[string]struct{}), + } +} + +// Save add one path to the PathManager. If a path already exists, it is not added again. +func (p *pathManager) Save(path string) (exist bool) { + p.mu.Lock() + defer p.mu.Unlock() + if _, exist = p.paths[path]; !exist { + p.paths[path] = struct{}{} + } + return exist +} + +func (p *pathManager) List() []string { + p.mu.RLock() + defer p.mu.RUnlock() + + paths := make([]string, 0, len(p.paths)) + for path := range p.paths { + paths = append(paths, path) + } + + return paths +} + +func (p *pathManager) Reset() { + p.mu.Lock() + defer p.mu.Unlock() + p.paths = make(map[string]struct{}) +} From d6315a1efd5a4f84a3cf8dbcb75660288a58d33a Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:57:58 +0100 Subject: [PATCH 15/24] fix: typo Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/app.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contribs/gnodev/cmd/gnodev/app.go b/contribs/gnodev/cmd/gnodev/app.go index 588fa3ba108..09fab28358f 100644 --- a/contribs/gnodev/cmd/gnodev/app.go +++ b/contribs/gnodev/cmd/gnodev/app.go @@ -211,11 +211,13 @@ func (ds *App) setupHandlers(ctx context.Context) (http.Handler, error) { if ds.proxy != nil { proxyLogger := ds.logger.WithGroup(ProxyLogName) remote = ds.proxy.TargetAddress() // update remote address with proxy target address - // Generate initlial paths + + // Generate initial paths initPaths := map[string]struct{}{} for _, path := range ds.paths { initPaths[path] = struct{}{} } + ds.proxy.HandlePath(func(paths ...string) { new := false for _, path := range paths { From 8e931e2dd6d00bf2af10bd83e586aa7c59b999f7 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:51:12 +0100 Subject: [PATCH 16/24] fix: remote package load Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/pkg/dev/node.go | 8 ++++- contribs/gnodev/pkg/packages/package.go | 32 ++++++++++++++++--- .../gnodev/pkg/packages/resolver_remote.go | 31 +++++++++++++----- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 74eff9c54e5..1c91c34b027 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -418,6 +418,8 @@ func (n *Node) stopIfRunning() error { } func (n *Node) rebuildNodeFromState(ctx context.Context) error { + start := time.Now() + if n.config.NoReplay { // If NoReplay is true, simply reset the node to its initial state n.logger.Warn("replay disabled") @@ -454,7 +456,11 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { // Reset the node with the new genesis state. err = n.rebuildNode(ctx, genesis) - n.logger.Info("reload done", "pkgs", len(pkgsTxs), "state applied", len(state)) + n.logger.Info("reload done", + "pkgs", len(pkgsTxs), + "state applied", len(state), + "took", time.Since(start), + ) // Update node infos n.pkgs = pkgs diff --git a/contribs/gnodev/pkg/packages/package.go b/contribs/gnodev/pkg/packages/package.go index 6798aefbaf3..86b34da1105 100644 --- a/contribs/gnodev/pkg/packages/package.go +++ b/contribs/gnodev/pkg/packages/package.go @@ -7,6 +7,7 @@ import ( "io/fs" "os" "path/filepath" + "strings" "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/gnovm/pkg/gnomod" @@ -63,7 +64,7 @@ func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) } if isGnoFile(fname) { - memfile, pkgname, err := parseFile(fset, fname, body) + memfile, pkgname, err := parseGnoFile(fset, fname, body) if err != nil { return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) } @@ -72,21 +73,22 @@ func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) if name != "" && name != pkgname { return nil, fmt.Errorf("conflict package name between %q and %q", name, memfile.Name) } - name = pkgname } memFiles = append(memFiles, memfile) - continue // continue + continue } if isValidPackageFile(fname) { memFiles = append(memFiles, &gnovm.MemFile{ Name: fname, Body: string(body), }) + + continue } - // ignore the file + // skip, not supported file } // Empty package, skipping @@ -135,6 +137,28 @@ func parseFile(fset *token.FileSet, fname string, body []byte) (*gnovm.MemFile, return nil, "", fmt.Errorf("unable to parse file %q: %w", fname, err) } + pkgname := f.Name.Name + if isTestFile(fname) { + pkgname, _, _ = strings.Cut(pkgname, "_") + } + + return &gnovm.MemFile{ + Name: fname, + Body: string(body), + }, f.Name.Name, nil +} + +func parseGnoFile(fset *token.FileSet, fname string, body []byte) (*gnovm.MemFile, string, error) { + f, err := parser.ParseFile(fset, fname, body, parser.PackageClauseOnly) + if err != nil { + return nil, "", fmt.Errorf("unable to parse file %q: %w", fname, err) + } + + pkgname := f.Name.Name + if isTestFile(fname) { + pkgname, _, _ = strings.Cut(pkgname, "_") + } + return &gnovm.MemFile{ Name: fname, Body: string(body), diff --git a/contribs/gnodev/pkg/packages/resolver_remote.go b/contribs/gnodev/pkg/packages/resolver_remote.go index 2fb2f204c22..51277beede1 100644 --- a/contribs/gnodev/pkg/packages/resolver_remote.go +++ b/contribs/gnodev/pkg/packages/resolver_remote.go @@ -60,19 +60,34 @@ func (res *remoteResolver) Resolve(fset *token.FileSet, path string) (*Package, if err := qres.Response.Error; err != nil { return nil, fmt.Errorf("unable to query file %q on path %q: %w", fname, path, err) } - body := qres.Response.Data - memfile, pkgname, err := parseFile(fset, fname, body) - if err != nil { - return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) + + if isGnoFile(fname) { + memfile, pkgname, err := parseGnoFile(fset, fname, body) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) + } + + if !isTestFile(fname) { + if name != "" && name != pkgname { + return nil, fmt.Errorf("conflict package name between %q and %q", name, memfile.Name) + } + name = pkgname + } + + memFiles = append(memFiles, memfile) + continue } - if name != "" && name != pkgname { - return nil, fmt.Errorf("conflict package name between %q and %q", name, memfile.Name) + if isValidPackageFile(fname) { + memFiles = append(memFiles, &gnovm.MemFile{ + Name: fname, Body: string(body), + }) + + continue } - name = pkgname - memFiles = append(memFiles, memfile) + // skip, not supported file } return &Package{ From a7a9a5375e7fbb9994c30315f150e57e36eed7a3 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:06:52 +0100 Subject: [PATCH 17/24] fix: make custom resolvers override default resolver Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/setup_loader.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go index 5d1e792b7cd..3694678bb3e 100644 --- a/contribs/gnodev/cmd/gnodev/setup_loader.go +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -74,9 +74,13 @@ func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, dirs ...string) (pa resolver := packages.ChainResolvers( packages.ChainResolvers(localResolvers...), // Resolve local directories packages.ChainResolvers(cfg.resolvers...), // Use user's custom resolvers - packages.NewFSResolver(exampleRoot), // Ultimately use fs resolver from example folder ) + // If not resolvers are provided, fallback on example folder + if len(cfg.resolvers) == 0 { + resolver = packages.ChainResolvers(resolver, packages.NewFSResolver(exampleRoot)) + } + // Enrich resolver with middleware return packages.MiddlewareResolver(resolver, packages.CacheMiddleware(func(pkg *packages.Package) bool { From 9f80c6f0a70fc261e768d72f874de275d580ff74 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:07:27 +0100 Subject: [PATCH 18/24] chore: cleanup errors Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/setup_web.go | 1 - contribs/gnodev/pkg/packages/loader.go | 2 +- contribs/gnodev/pkg/packages/loader_glob.go | 2 +- contribs/gnodev/pkg/packages/resolver.go | 2 +- contribs/gnodev/pkg/packages/resolver_fs.go | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go index af335b70bbf..b6af798f43b 100644 --- a/contribs/gnodev/cmd/gnodev/setup_web.go +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -14,7 +14,6 @@ func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, remoteAddr string) (htt return http.HandlerFunc(http.NotFound), nil } - fmt.Printf("REMOTE: %+v\r\n", remoteAddr) appcfg := gnoweb.NewDefaultAppConfig() appcfg.UnsafeHTML = cfg.webHTML appcfg.NodeRemote = remoteAddr diff --git a/contribs/gnodev/pkg/packages/loader.go b/contribs/gnodev/pkg/packages/loader.go index 0447f6e309f..092eb91c5b9 100644 --- a/contribs/gnodev/pkg/packages/loader.go +++ b/contribs/gnodev/pkg/packages/loader.go @@ -59,7 +59,7 @@ func load(path string, fset *token.FileSet, resolver Resolver, visited, stack ma return nil, nil } - return nil, fmt.Errorf("unable to resolve package: %w", err) + return nil, fmt.Errorf("unable to resolve package %q: %w", path, err) } var name string diff --git a/contribs/gnodev/pkg/packages/loader_glob.go b/contribs/gnodev/pkg/packages/loader_glob.go index 685c8a18d4e..38d7f8cf5ee 100644 --- a/contribs/gnodev/pkg/packages/loader_glob.go +++ b/contribs/gnodev/pkg/packages/loader_glob.go @@ -71,7 +71,7 @@ func (l GlobLoader) MatchPaths(globs ...string) ([]string, error) { func (l GlobLoader) Load(gpaths ...string) ([]Package, error) { paths, err := l.MatchPaths(gpaths...) if err != nil { - return nil, fmt.Errorf("unable to resolve dir paths: %w", err) + return nil, fmt.Errorf("match glob pattern error: %w", err) } loader := &BaseLoader{Resolver: l.Resolver} diff --git a/contribs/gnodev/pkg/packages/resolver.go b/contribs/gnodev/pkg/packages/resolver.go index ea3bf19a633..bb3922b30a8 100644 --- a/contribs/gnodev/pkg/packages/resolver.go +++ b/contribs/gnodev/pkg/packages/resolver.go @@ -66,7 +66,7 @@ func (cr ChainedResolver) Resolve(fset *token.FileSet, path string) (*Package, e continue } - return nil, fmt.Errorf("unable to resolve %q: %w", path, err) + return nil, fmt.Errorf("resolver %q error: %w", resolver.Name(), err) } return nil, ErrResolverPackageNotFound diff --git a/contribs/gnodev/pkg/packages/resolver_fs.go b/contribs/gnodev/pkg/packages/resolver_fs.go index c5b6ba42955..a46cc8f9c05 100644 --- a/contribs/gnodev/pkg/packages/resolver_fs.go +++ b/contribs/gnodev/pkg/packages/resolver_fs.go @@ -23,7 +23,7 @@ func (r *fsResolver) Resolve(fset *token.FileSet, path string) (*Package, error) dir := filepath.Join(r.root, path) _, err := os.Stat(dir) if err != nil { - return nil, fmt.Errorf("unable to determine dir for path %q: %w", path, ErrResolverPackageNotFound) + return nil, ErrResolverPackageNotFound } return ReadPackageFromDir(fset, path, dir) From 5fb70a9351113910669476a9546453016da58436 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:38:04 +0100 Subject: [PATCH 19/24] fix: globl package Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/pkg/packages/glob.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contribs/gnodev/pkg/packages/glob.go b/contribs/gnodev/pkg/packages/glob.go index 36e1acccdf8..1b76425deb4 100644 --- a/contribs/gnodev/pkg/packages/glob.go +++ b/contribs/gnodev/pkg/packages/glob.go @@ -33,7 +33,10 @@ func parse(pattern string) (*Glob, string, error) { for len(pattern) > 0 { switch pattern[0] { case '/': - pattern = pattern[1:] + // Skip consecutive slashes + for len(pattern) > 0 && pattern[0] == '/' { + pattern = pattern[1:] + } g.elems = append(g.elems, slash{}) case '*': @@ -110,10 +113,11 @@ func match(elems []element, input string) (ok bool) { elem, elems = elems[0], elems[1:] switch elem := elem.(type) { case slash: + // Skip consecutive slashes in the input if len(input) == 0 || input[0] != '/' { return false } - for input[0] == '/' { + for len(input) > 0 && input[0] == '/' { input = input[1:] } From 09ea7cc561049b4f570106ac6a0d6902c15d1832 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:38:52 +0100 Subject: [PATCH 20/24] chore: lint Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/app.go | 6 +++--- contribs/gnodev/cmd/gnodev/setup_loader.go | 6 +++--- contribs/gnodev/cmd/gnodev/utils.go | 25 ---------------------- contribs/gnodev/pkg/dev/query_path.go | 2 -- contribs/gnodev/pkg/packages/package.go | 17 --------------- 5 files changed, 6 insertions(+), 50 deletions(-) delete mode 100644 contribs/gnodev/cmd/gnodev/utils.go diff --git a/contribs/gnodev/cmd/gnodev/app.go b/contribs/gnodev/cmd/gnodev/app.go index 09fab28358f..2af2b41efa0 100644 --- a/contribs/gnodev/cmd/gnodev/app.go +++ b/contribs/gnodev/cmd/gnodev/app.go @@ -219,7 +219,7 @@ func (ds *App) setupHandlers(ctx context.Context) (http.Handler, error) { } ds.proxy.HandlePath(func(paths ...string) { - new := false + newPath := false for _, path := range paths { // Check if the path is an initial path. if _, ok := initPaths[path]; ok { @@ -243,10 +243,10 @@ func (ds *App) setupHandlers(ctx context.Context) (http.Handler, error) { proxyLogger.Info("new monitored path", "path", path) - new = true + newPath = true } - if !new { + if !newPath { return } diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go index 3694678bb3e..f278228d8b6 100644 --- a/contribs/gnodev/cmd/gnodev/setup_loader.go +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -54,10 +54,10 @@ func (va *varResolver) Set(value string) error { func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, dirs ...string) (packages.Resolver, []string) { // Add root resolvers exampleRoot := filepath.Join(cfg.root, "examples") + localResolvers := make([]packages.Resolver, len(dirs)) - var localResolvers []packages.Resolver var paths []string - for _, dir := range dirs { + for i, dir := range dirs { path := guessPath(cfg, dir) resolver := packages.NewLocalResolver(path, dir) @@ -68,7 +68,7 @@ func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, dirs ...string) (pa logger.Warn("invalid local path", "dir", dir) } - localResolvers = append(localResolvers, resolver) + localResolvers[i] = resolver } resolver := packages.ChainResolvers( diff --git a/contribs/gnodev/cmd/gnodev/utils.go b/contribs/gnodev/cmd/gnodev/utils.go deleted file mode 100644 index ea315add499..00000000000 --- a/contribs/gnodev/cmd/gnodev/utils.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import "strings" - -type varStrings []string - -func (va *varStrings) Set(val string) error { - for _, subval := range strings.Split(val, ",") { - *va = append(*va, subval) - } - - return nil -} - -func (va *varStrings) String() string { - return strings.Join(*va, ",") -} - -func (va *varStrings) Strings() []string { - if va == nil { - return []string{} - } - - return []string(*va) -} diff --git a/contribs/gnodev/pkg/dev/query_path.go b/contribs/gnodev/pkg/dev/query_path.go index f1ac82ac64e..daf69a71b36 100644 --- a/contribs/gnodev/pkg/dev/query_path.go +++ b/contribs/gnodev/pkg/dev/query_path.go @@ -19,8 +19,6 @@ type PackageModifier struct { type PackageMetaMap struct { Creator crypto.Address Deposit std.Coins - - queries map[string]PackageModifier } func ResolvePackageModifierQuery(bk *address.Book, path string) (PackageModifier, error) { diff --git a/contribs/gnodev/pkg/packages/package.go b/contribs/gnodev/pkg/packages/package.go index 86b34da1105..ceb5fed264e 100644 --- a/contribs/gnodev/pkg/packages/package.go +++ b/contribs/gnodev/pkg/packages/package.go @@ -131,23 +131,6 @@ func isDraftPackages(dir string, files []fs.DirEntry) (bool, error) { return false, nil } -func parseFile(fset *token.FileSet, fname string, body []byte) (*gnovm.MemFile, string, error) { - f, err := parser.ParseFile(fset, fname, body, parser.PackageClauseOnly) - if err != nil { - return nil, "", fmt.Errorf("unable to parse file %q: %w", fname, err) - } - - pkgname := f.Name.Name - if isTestFile(fname) { - pkgname, _, _ = strings.Cut(pkgname, "_") - } - - return &gnovm.MemFile{ - Name: fname, - Body: string(body), - }, f.Name.Name, nil -} - func parseGnoFile(fset *token.FileSet, fname string, body []byte) (*gnovm.MemFile, string, error) { f, err := parser.ParseFile(fset, fname, body, parser.PackageClauseOnly) if err != nil { From 8b7a45719b8b2d6f05e375a3ac7f37a7f40b642f Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:39:00 +0100 Subject: [PATCH 21/24] fix: tests Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/setup_loader.go | 28 +- contribs/gnodev/pkg/dev/node_state_test.go | 387 +++++++------- contribs/gnodev/pkg/dev/node_test.go | 497 +++++++++--------- .../pkg/packages/resolver_middleware.go | 13 + 4 files changed, 463 insertions(+), 462 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go index f278228d8b6..1116b30289c 100644 --- a/contribs/gnodev/cmd/gnodev/setup_loader.go +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -86,9 +86,9 @@ func setupPackagesResolver(logger *slog.Logger, cfg *devCfg, dirs ...string) (pa packages.CacheMiddleware(func(pkg *packages.Package) bool { return pkg.Kind == packages.PackageKindRemote // Only cache remote package }), - packages.FilterPathMiddleware("stdlib", isStdPath), // Filter stdlib package from resolving - packages.PackageCheckerMiddleware(logger), // Pre-check syntax to avoid bothering the node reloading on invalid files - packages.LogMiddleware(logger), // Log request + packages.FilterStdlibs, // Filter stdlib package from resolving + packages.PackageCheckerMiddleware(logger), // Pre-check syntax to avoid bothering the node reloading on invalid files + packages.LogMiddleware(logger), // Log request ), paths } @@ -111,25 +111,3 @@ func guessPath(cfg *devCfg, dir string) (path string) { rname := reInvalidChar.ReplaceAllString(filepath.Base(dir), "-") return filepath.Join(cfg.chainDomain, "/r/dev/", rname) } - -func isStdPath(path string) bool { - if i := strings.IndexRune(path, '/'); i > 0 { - if j := strings.IndexRune(path[:i], '.'); j >= 0 { - return false - } - } - - return true -} - -// func guessPathFromRoots(dir string, roots ...string) (path string, ok bool) { -// for _, root := range roots { -// if !strings.HasPrefix(dir, root) { -// continue -// } - -// return strings.TrimPrefix(dir, root), true -// } - -// return "", false -// } diff --git a/contribs/gnodev/pkg/dev/node_state_test.go b/contribs/gnodev/pkg/dev/node_state_test.go index ab792b05740..cbde5a43d0b 100644 --- a/contribs/gnodev/pkg/dev/node_state_test.go +++ b/contribs/gnodev/pkg/dev/node_state_test.go @@ -1,193 +1,198 @@ package dev -// import ( -// emitter "github.com/gnolang/gno/contribs/gnodev/internal/mock" -// "github.com/gnolang/gno/contribs/gnodev/pkg/events" -// "github.com/gnolang/gno/gno.land/pkg/gnoland" -// "github.com/gnolang/gno/gno.land/pkg/sdk/vm" -// "github.com/stretchr/testify/assert" -// "github.com/stretchr/testify/require" -// ) - -// const testCounterRealm = "gno.land/r/dev/counter" - -// func TestNodeMovePreviousTX(t *testing.T) { -// const callInc = 5 - -// node, emitter := testingCounterRealm(t, callInc) - -// t.Run("Prev TX", func(t *testing.T) { -// ctx := testingContext(t) -// err := node.MoveToPreviousTX(ctx) -// require.NoError(t, err) -// assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) - -// // Check for correct render update -// render, err := testingRenderRealm(t, node, testCounterRealm) -// require.NoError(t, err) -// require.Equal(t, render, "4") -// }) - -// t.Run("Next TX", func(t *testing.T) { -// ctx := testingContext(t) -// err := node.MoveToNextTX(ctx) -// require.NoError(t, err) -// assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) - -// // Check for correct render update -// render, err := testingRenderRealm(t, node, testCounterRealm) -// require.NoError(t, err) -// require.Equal(t, render, "5") -// }) - -// t.Run("Multi Move TX", func(t *testing.T) { -// ctx := testingContext(t) -// moves := []struct { -// Move int -// ExpectedResult string -// }{ -// {-2, "3"}, -// {2, "5"}, -// {-5, "0"}, -// {5, "5"}, -// {-100, "0"}, -// {100, "5"}, -// {0, "5"}, -// } - -// t.Logf("initial state %d", callInc) -// for _, tc := range moves { -// t.Logf("moving from `%d`", tc.Move) -// err := node.MoveBy(ctx, tc.Move) -// require.NoError(t, err) -// if tc.Move != 0 { -// assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) -// } - -// // Check for correct render update -// render, err := testingRenderRealm(t, node, testCounterRealm) -// require.NoError(t, err) -// require.Equal(t, render, tc.ExpectedResult) -// } -// }) -// } - -// func TestSaveCurrentState(t *testing.T) { -// ctx := testingContext(t) - -// node, emitter := testingCounterRealm(t, 2) - -// // Save current state -// err := node.SaveCurrentState(ctx) -// require.NoError(t, err) - -// // Send a new tx -// msg := vm.MsgCall{ -// PkgPath: testCounterRealm, -// Func: "Inc", -// Args: []string{"10"}, -// } - -// res, err := testingCallRealm(t, node, msg) -// require.NoError(t, err) -// require.NoError(t, res.CheckTx.Error) -// require.NoError(t, res.DeliverTx.Error) -// assert.Equal(t, events.EvtTxResult, emitter.NextEvent().Type()) - -// // Test render -// render, err := testingRenderRealm(t, node, testCounterRealm) -// require.NoError(t, err) -// require.Equal(t, render, "12") // 2 + 10 - -// // Reset state -// err = node.Reset(ctx) -// require.NoError(t, err) -// assert.Equal(t, events.EvtReset, emitter.NextEvent().Type()) - -// render, err = testingRenderRealm(t, node, testCounterRealm) -// require.NoError(t, err) -// require.Equal(t, render, "2") // Back to the original state -// } - -// func TestExportState(t *testing.T) { -// node, _ := testingCounterRealm(t, 3) - -// t.Run("export state", func(t *testing.T) { -// ctx := testingContext(t) -// state, err := node.ExportCurrentState(ctx) -// require.NoError(t, err) -// assert.Equal(t, 3, len(state)) -// }) - -// t.Run("export genesis doc", func(t *testing.T) { -// ctx := testingContext(t) -// doc, err := node.ExportStateAsGenesis(ctx) -// require.NoError(t, err) -// require.NotNil(t, doc.AppState) - -// state, ok := doc.AppState.(gnoland.GnoGenesisState) -// require.True(t, ok) -// assert.Equal(t, 3, len(state.Txs)) -// }) -// } - -// func testingCounterRealm(t *testing.T, inc int) (*Node, *emitter.ServerEmitter) { -// t.Helper() - -// const ( -// // foo package -// counterGnoMod = "module gno.land/r/dev/counter\n" -// counterFile = `package counter -// import "strconv" - -// var value int = 0 -// func Inc(v int) { value += v } // method to increment value -// func Render(_ string) string { return strconv.Itoa(value) } -// ` -// ) - -// // Generate package counter -// counterPkg := generateTestingPackage(t, -// "gno.mod", counterGnoMod, -// "foo.gno", counterFile) - -// // Call NewDevNode with no package should work -// node, emitter := newTestingDevNode(t, counterPkg) -// assert.Len(t, node.ListPkgs(), 1) - -// // Test rendering -// render, err := testingRenderRealm(t, node, testCounterRealm) -// require.NoError(t, err) -// require.Equal(t, render, "0") - -// // Increment the counter 10 times -// for i := 0; i < inc; i++ { -// t.Logf("call %d", i) -// // Craft `Inc` msg -// msg := vm.MsgCall{ -// PkgPath: testCounterRealm, -// Func: "Inc", -// Args: []string{"1"}, -// } - -// res, err := testingCallRealm(t, node, msg) -// require.NoError(t, err) -// require.NoError(t, res.CheckTx.Error) -// require.NoError(t, res.DeliverTx.Error) -// assert.Equal(t, events.EvtTxResult, emitter.NextEvent().Type()) -// } - -// render, err = testingRenderRealm(t, node, testCounterRealm) -// require.NoError(t, err) -// require.Equal(t, render, strconv.Itoa(inc)) - -// return node, emitter -// } - -// func testingContext(t *testing.T) context.Context { -// t.Helper() - -// ctx, cancel := context.WithTimeout(context.Background(), time.Second*7) -// t.Cleanup(cancel) -// return ctx -// } +import ( + "context" + "strconv" + "testing" + "time" + + mock "github.com/gnolang/gno/contribs/gnodev/internal/mock" + "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const testCounterRealm = "gno.land/r/dev/counter" + +func TestNodeMovePreviousTX(t *testing.T) { + const callInc = 5 + + node, emitter := testingCounterRealm(t, callInc) + + t.Run("Prev TX", func(t *testing.T) { + ctx := testingContext(t) + err := node.MoveToPreviousTX(ctx) + require.NoError(t, err) + assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) + + // Check for correct render update + render, err := testingRenderRealm(t, node, testCounterRealm) + require.NoError(t, err) + require.Equal(t, render, "4") + }) + + t.Run("Next TX", func(t *testing.T) { + ctx := testingContext(t) + err := node.MoveToNextTX(ctx) + require.NoError(t, err) + assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) + + // Check for correct render update + render, err := testingRenderRealm(t, node, testCounterRealm) + require.NoError(t, err) + require.Equal(t, render, "5") + }) + + t.Run("Multi Move TX", func(t *testing.T) { + ctx := testingContext(t) + moves := []struct { + Move int + ExpectedResult string + }{ + {-2, "3"}, + {2, "5"}, + {-5, "0"}, + {5, "5"}, + {-100, "0"}, + {100, "5"}, + {0, "5"}, + } + + t.Logf("initial state %d", callInc) + for _, tc := range moves { + t.Logf("moving from `%d`", tc.Move) + err := node.MoveBy(ctx, tc.Move) + require.NoError(t, err) + if tc.Move != 0 { + assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) + } + + // Check for correct render update + render, err := testingRenderRealm(t, node, testCounterRealm) + require.NoError(t, err) + require.Equal(t, render, tc.ExpectedResult) + } + }) +} + +func TestSaveCurrentState(t *testing.T) { + ctx := testingContext(t) + + node, emitter := testingCounterRealm(t, 2) + + // Save current state + err := node.SaveCurrentState(ctx) + require.NoError(t, err) + + // Send a new tx + msg := vm.MsgCall{ + PkgPath: testCounterRealm, + Func: "Inc", + Args: []string{"10"}, + } + + res, err := testingCallRealm(t, node, msg) + require.NoError(t, err) + require.NoError(t, res.CheckTx.Error) + require.NoError(t, res.DeliverTx.Error) + assert.Equal(t, events.EvtTxResult, emitter.NextEvent().Type()) + + // Test render + render, err := testingRenderRealm(t, node, testCounterRealm) + require.NoError(t, err) + require.Equal(t, render, "12") // 2 + 10 + + // Reset state + err = node.Reset(ctx) + require.NoError(t, err) + assert.Equal(t, events.EvtReset, emitter.NextEvent().Type()) + + render, err = testingRenderRealm(t, node, testCounterRealm) + require.NoError(t, err) + require.Equal(t, render, "2") // Back to the original state +} + +func TestExportState(t *testing.T) { + node, _ := testingCounterRealm(t, 3) + + t.Run("export state", func(t *testing.T) { + ctx := testingContext(t) + state, err := node.ExportCurrentState(ctx) + require.NoError(t, err) + assert.Equal(t, 3, len(state)) + }) + + t.Run("export genesis doc", func(t *testing.T) { + ctx := testingContext(t) + doc, err := node.ExportStateAsGenesis(ctx) + require.NoError(t, err) + require.NotNil(t, doc.AppState) + + state, ok := doc.AppState.(gnoland.GnoGenesisState) + require.True(t, ok) + assert.Equal(t, 3, len(state.Txs)) + }) +} + +func testingCounterRealm(t *testing.T, inc int) (*Node, *mock.ServerEmitter) { + t.Helper() + + const ( + // foo package + counterPath = "gno.land/r/dev/counter" + counterFile = ` +package counter + +import "strconv" + +var value int = 0 +func Inc(v int) { value += v } // method to increment value +func Render(_ string) string { return strconv.Itoa(value) } +` + ) + + // Generate package counter + counterPkg := generateMemPackage(t, counterPath, "foo.gno", counterFile) + + // Call NewDevNode with no package should work + node, emitter := newTestingDevNode(t, &counterPkg) + assert.Len(t, node.ListPkgs(), 1) + + // Test rendering + render, err := testingRenderRealm(t, node, testCounterRealm) + require.NoError(t, err) + require.Equal(t, render, "0") + + // Increment the counter 10 times + for i := 0; i < inc; i++ { + t.Logf("call %d", i) + // Craft `Inc` msg + msg := vm.MsgCall{ + PkgPath: testCounterRealm, + Func: "Inc", + Args: []string{"1"}, + } + + res, err := testingCallRealm(t, node, msg) + require.NoError(t, err) + require.NoError(t, res.CheckTx.Error) + require.NoError(t, res.DeliverTx.Error) + assert.Equal(t, events.EvtTxResult, emitter.NextEvent().Type()) + } + + render, err = testingRenderRealm(t, node, testCounterRealm) + require.NoError(t, err) + require.Equal(t, render, strconv.Itoa(inc)) + + return node, emitter +} + +func testingContext(t *testing.T) context.Context { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*7) + t.Cleanup(cancel) + return ctx +} diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 7f69a92386a..b141369bbe7 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -2,8 +2,10 @@ package dev import ( "context" + "encoding/json" "path/filepath" "testing" + "time" mock "github.com/gnolang/gno/contribs/gnodev/internal/mock" "github.com/gnolang/gno/contribs/gnodev/pkg/events" @@ -15,18 +17,15 @@ import ( "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto/keys" + tm2events "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// XXX: We should probably use txtar to test this package. - -var nodeTestingAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - -// // TestNewNode_NoPackages tests the NewDevNode method with no package. +// TestNewNode_NoPackages tests the NewDevNode method with no package. func TestNewNode_NoPackages(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -44,7 +43,7 @@ func TestNewNode_NoPackages(t *testing.T) { require.NoError(t, node.Close()) } -// // TestNewNode_WithPackage tests the NewDevNode with a single package. +// TestNewNode_WithPackage tests the NewDevNode with a single package. func TestNewNode_WithLoader(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -165,244 +164,248 @@ func Render(_ string) string { return "bar" } assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) } -// func TestNodeReset(t *testing.T) { -// const ( -// // foo package -// foobarGnoMod = "module gno.land/r/dev/foo\n" -// fooFile = `package foo -// var str string = "foo" -// func UpdateStr(newStr string) { str = newStr } // method to update 'str' variable -// func Render(_ string) string { return str } -// ` -// ) - -// // Generate package foo -// foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) - -// // Call NewDevNode with no package should work -// node, emitter := newTestingDevNode(t, foopkg) -// assert.Len(t, node.ListPkgs(), 1) - -// // Test rendering -// render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") -// require.NoError(t, err) -// require.Equal(t, render, "foo") - -// // Call `UpdateStr` to update `str` value with "bar" -// msg := vm.MsgCall{ -// PkgPath: "gno.land/r/dev/foo", -// Func: "UpdateStr", -// Args: []string{"bar"}, -// Send: nil, -// } -// res, err := testingCallRealm(t, node, msg) -// require.NoError(t, err) -// require.NoError(t, res.CheckTx.Error) -// require.NoError(t, res.DeliverTx.Error) -// assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) - -// // Check for correct render update -// render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") -// require.NoError(t, err) -// require.Equal(t, render, "bar") - -// // Reset state -// err = node.Reset(context.Background()) -// require.NoError(t, err) -// assert.Equal(t, emitter.NextEvent().Type(), events.EvtReset) - -// // Test rendering should return initial `str` value -// render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") -// require.NoError(t, err) -// require.Equal(t, render, "foo") - -// assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) -// } - -// func TestTxTimestampRecover(t *testing.T) { -// const ( -// // foo package -// foobarGnoMod = "module gno.land/r/dev/foo\n" -// fooFile = `package foo -// import ( -// "strconv" -// "strings" -// "time" -// ) - -// var times = []time.Time{ -// time.Now(), // Evaluate at genesis -// } - -// func SpanTime() { -// times = append(times, time.Now()) -// } - -// func Render(_ string) string { -// var strs strings.Builder - -// strs.WriteRune('[') -// for i, t := range times { -// if i > 0 { -// strs.WriteRune(',') -// } -// strs.WriteString(strconv.Itoa(int(t.UnixNano()))) -// } -// strs.WriteRune(']') - -// return strs.String() -// } -// ` -// ) - -// // Add a hard deadline of 20 seconds to avoid potential deadlock and fail early -// ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) -// defer cancel() - -// parseJSONTimesList := func(t *testing.T, render string) []time.Time { -// t.Helper() - -// var times []time.Time -// var nanos []int64 - -// err := json.Unmarshal([]byte(render), &nanos) -// require.NoError(t, err) - -// for _, nano := range nanos { -// sec, nsec := nano/int64(time.Second), nano%int64(time.Second) -// times = append(times, time.Unix(sec, nsec)) -// } - -// return times -// } - -// // Generate package foo -// foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) - -// // Call NewDevNode with no package should work -// cfg := createDefaultTestingNodeConfig(foopkg) - -// // XXX(gfanton): Setting this to `false` somehow makes the time block -// // drift from the time spanned by the VM. -// cfg.TMConfig.Consensus.SkipTimeoutCommit = false -// cfg.TMConfig.Consensus.TimeoutCommit = 500 * time.Millisecond -// cfg.TMConfig.Consensus.TimeoutPropose = 100 * time.Millisecond -// cfg.TMConfig.Consensus.CreateEmptyBlocks = true - -// node, emitter := newTestingDevNodeWithConfig(t, cfg) - -// // We need to make sure that blocks are separated by at least 1 second -// // (minimal time between blocks). We can ensure this by listening for -// // new blocks and comparing timestamps -// cc := make(chan types.EventNewBlock) -// node.Node.EventSwitch().AddListener("test-timestamp", func(evt tm2events.Event) { -// newBlock, ok := evt.(types.EventNewBlock) -// if !ok { -// return -// } - -// select { -// case cc <- newBlock: -// default: -// } -// }) - -// // wait for first block for reference -// var refHeight, refTimestamp int64 - -// select { -// case <-ctx.Done(): -// require.FailNow(t, ctx.Err().Error()) -// case res := <-cc: -// refTimestamp = res.Block.Time.Unix() -// refHeight = res.Block.Height -// } - -// // number of span to process -// const nevents = 3 - -// // Span multiple time -// for i := 0; i < nevents; i++ { -// t.Logf("waiting for a bock greater than height(%d) and unix(%d)", refHeight, refTimestamp) -// for { -// var block types.EventNewBlock -// select { -// case <-ctx.Done(): -// require.FailNow(t, ctx.Err().Error()) -// case block = <-cc: -// } - -// t.Logf("got a block height(%d) and unix(%d)", -// block.Block.Height, block.Block.Time.Unix()) - -// // Ensure we consume every block before tx block -// if refHeight >= block.Block.Height { -// continue -// } - -// // Ensure new block timestamp is before previous reference timestamp -// if newRefTimestamp := block.Block.Time.Unix(); newRefTimestamp > refTimestamp { -// refTimestamp = newRefTimestamp -// break // break the loop -// } -// } - -// t.Logf("found a valid block(%d)! continue", refHeight) - -// // Span a new time -// msg := vm.MsgCall{ -// PkgPath: "gno.land/r/dev/foo", -// Func: "SpanTime", -// } - -// res, err := testingCallRealm(t, node, msg) - -// require.NoError(t, err) -// require.NoError(t, res.CheckTx.Error) -// require.NoError(t, res.DeliverTx.Error) -// assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) - -// // Set the new height from the tx as reference -// refHeight = res.Height -// } - -// // Render JSON times list -// render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") -// require.NoError(t, err) - -// // Parse times list -// timesList1 := parseJSONTimesList(t, render) -// t.Logf("list of times: %+v", timesList1) - -// // Ensure times are correctly expending. -// for i, t2 := range timesList1 { -// if i == 0 { -// continue -// } - -// t1 := timesList1[i-1] -// require.Greater(t, t2.UnixNano(), t1.UnixNano()) -// } - -// // Reload the node -// err = node.Reload(context.Background()) -// require.NoError(t, err) -// assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) - -// // Fetch time list again from render -// render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") -// require.NoError(t, err) - -// timesList2 := parseJSONTimesList(t, render) - -// // Times list should be identical from the orignal list -// require.Len(t, timesList2, len(timesList1)) -// for i := 0; i < len(timesList1); i++ { -// t1nsec, t2nsec := timesList1[i].UnixNano(), timesList2[i].UnixNano() -// assert.Equal(t, t1nsec, t2nsec, -// "comparing times1[%d](%d) == times2[%d](%d)", i, t1nsec, i, t2nsec) -// } -// } +func TestNodeReset(t *testing.T) { + const ( + // foo package + foobarPath = "gno.land/r/dev/foo" + fooFile = `package foo +var str string = "foo" +func UpdateStr(newStr string) { str = newStr } // method to update 'str' variable +func Render(_ string) string { return str } +` + ) + + // Generate package foo + foopkg := generateMemPackage(t, foobarPath, "foo.gno", fooFile) + + // Call NewDevNode with no package should work + node, emitter := newTestingDevNode(t, &foopkg) + assert.Len(t, node.ListPkgs(), 1) + + // Test rendering + render, err := testingRenderRealm(t, node, foobarPath) + require.NoError(t, err) + require.Equal(t, render, "foo") + + // Call `UpdateStr` to update `str` value with "bar" + msg := vm.MsgCall{ + PkgPath: foobarPath, + Func: "UpdateStr", + Args: []string{"bar"}, + Send: nil, + } + res, err := testingCallRealm(t, node, msg) + require.NoError(t, err) + require.NoError(t, res.CheckTx.Error) + require.NoError(t, res.DeliverTx.Error) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) + + // Check for correct render update + render, err = testingRenderRealm(t, node, foobarPath) + require.NoError(t, err) + require.Equal(t, render, "bar") + + // Reset state + err = node.Reset(context.Background()) + require.NoError(t, err) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtReset) + + // Test rendering should return initial `str` value + render, err = testingRenderRealm(t, node, foobarPath) + require.NoError(t, err) + require.Equal(t, render, "foo") + + assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) +} + +func TestTxTimestampRecover(t *testing.T) { + const ( + // foo package + foobarPath = "gno.land/r/dev/foo" + fooFile = ` +package foo + +import ( + "strconv" + "strings" + "time" +) + +var times = []time.Time{ + time.Now(), // Evaluate at genesis +} + +func SpanTime() { + times = append(times, time.Now()) +} + +func Render(_ string) string { + var strs strings.Builder + + strs.WriteRune('[') + for i, t := range times { + if i > 0 { + strs.WriteRune(',') + } + strs.WriteString(strconv.Itoa(int(t.UnixNano()))) + } + strs.WriteRune(']') + + return strs.String() +} +` + ) + + // Add a hard deadline of 20 seconds to avoid potential deadlock and fail early + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + + // Generate package foo + foopkg := generateMemPackage(t, foobarPath, "foo.gno", fooFile) + + // XXX(gfanton): Setting this to `false` somehow makes the time block + // drift from the time spanned by the VM. + cfg := newTestingNodeConfig(&foopkg) + cfg.TMConfig.Consensus.SkipTimeoutCommit = false + cfg.TMConfig.Consensus.TimeoutCommit = 500 * time.Millisecond + cfg.TMConfig.Consensus.TimeoutPropose = 100 * time.Millisecond + cfg.TMConfig.Consensus.CreateEmptyBlocks = true + + node, emitter := newTestingDevNodeWithConfig(t, cfg, foopkg.Path) + + render, err := testingRenderRealm(t, node, foobarPath) + require.NoError(t, err) + require.NotEmpty(t, render) + + parseJSONTimesList := func(t *testing.T, render string) []time.Time { + t.Helper() + + var times []time.Time + var nanos []int64 + + err := json.Unmarshal([]byte(render), &nanos) + require.NoError(t, err) + + for _, nano := range nanos { + sec, nsec := nano/int64(time.Second), nano%int64(time.Second) + times = append(times, time.Unix(sec, nsec)) + } + + return times + } + + // We need to make sure that blocks are separated by at least 1 second + // (minimal time between blocks). We can ensure this by listening for + // new blocks and comparing timestamps + cc := make(chan types.EventNewBlock) + node.Node.EventSwitch().AddListener("test-timestamp", func(evt tm2events.Event) { + newBlock, ok := evt.(types.EventNewBlock) + if !ok { + return + } + + select { + case cc <- newBlock: + default: + } + }) + + // wait for first block for reference + var refHeight, refTimestamp int64 + + select { + case <-ctx.Done(): + require.FailNow(t, ctx.Err().Error()) + case res := <-cc: + refTimestamp = res.Block.Time.Unix() + refHeight = res.Block.Height + } + + // number of span to process + const nevents = 3 + + // Span multiple time + for i := 0; i < nevents; i++ { + t.Logf("waiting for a block greater than height(%d) and unix(%d)", refHeight, refTimestamp) + for { + var block types.EventNewBlock + select { + case <-ctx.Done(): + require.FailNow(t, ctx.Err().Error()) + case block = <-cc: + } + + t.Logf("got a block height(%d) and unix(%d)", + block.Block.Height, block.Block.Time.Unix()) + + // Ensure we consume every block before tx block + if refHeight >= block.Block.Height { + continue + } + + // Ensure new block timestamp is before previous reference timestamp + if newRefTimestamp := block.Block.Time.Unix(); newRefTimestamp > refTimestamp { + refTimestamp = newRefTimestamp + break // break the loop + } + } + + t.Logf("found a valid block(%d)! continue", refHeight) + + // Span a new time + msg := vm.MsgCall{ + PkgPath: foobarPath, + Func: "SpanTime", + } + + res, err := testingCallRealm(t, node, msg) + + require.NoError(t, err) + require.NoError(t, res.CheckTx.Error) + require.NoError(t, res.DeliverTx.Error) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) + + // Set the new height from the tx as reference + refHeight = res.Height + } + + // Render JSON times list + render, err = testingRenderRealm(t, node, foobarPath) + require.NoError(t, err) + + // Parse times list + timesList1 := parseJSONTimesList(t, render) + t.Logf("list of times: %+v", timesList1) + + // Ensure times are correctly expending. + for i, t2 := range timesList1 { + if i == 0 { + continue + } + + t1 := timesList1[i-1] + require.Greater(t, t2.UnixNano(), t1.UnixNano()) + } + + // Reload the node + err = node.Reload(context.Background()) + require.NoError(t, err) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) + + // Fetch time list again from render + render, err = testingRenderRealm(t, node, foobarPath) + require.NoError(t, err) + + timesList2 := parseJSONTimesList(t, render) + + // Times list should be identical from the original list + require.Len(t, timesList2, len(timesList1)) + for i := 0; i < len(timesList1); i++ { + t1nsec, t2nsec := timesList1[i].UnixNano(), timesList2[i].UnixNano() + assert.Equal(t, t1nsec, t2nsec, + "comparing times1[%d](%d) == times2[%d](%d)", i, t1nsec, i, t2nsec) + } +} func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error) { t.Helper() @@ -476,7 +479,9 @@ func generateMemPackage(t *testing.T, path string, pairNameFile ...string) gnovm func newTestingNodeConfig(pkgs ...*gnovm.MemPackage) *NodeConfig { var loader packages.BaseLoader - loader.Resolver = packages.NewMockResolver(pkgs...) + loader.Resolver = packages.MiddlewareResolver( + packages.NewMockResolver(pkgs...), + packages.FilterStdlibs) cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") cfg.Loader = &loader return cfg diff --git a/contribs/gnodev/pkg/packages/resolver_middleware.go b/contribs/gnodev/pkg/packages/resolver_middleware.go index 9e9e5346e43..c64f17b20b2 100644 --- a/contribs/gnodev/pkg/packages/resolver_middleware.go +++ b/contribs/gnodev/pkg/packages/resolver_middleware.go @@ -7,6 +7,7 @@ import ( "go/scanner" "go/token" "log/slog" + "strings" "time" ) @@ -117,6 +118,18 @@ func FilterPathMiddleware(name string, filter FilterPathHandler) MiddlewareHandl } } +var FilterStdlibs = FilterPathMiddleware("stdlibs", isStdPath) + +func isStdPath(path string) bool { + if i := strings.IndexRune(path, '/'); i > 0 { + if j := strings.IndexRune(path[:i], '.'); j >= 0 { + return false + } + } + + return true +} + // PackageCheckerMiddleware creates a middleware handler for post-processing syntax. func PackageCheckerMiddleware(logger *slog.Logger) MiddlewareHandler { return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { From 3621f7fbf2eb401e86497c64eb36dca1ecaf7ba5 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:47:59 +0100 Subject: [PATCH 22/24] chore: fix back typo Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- examples/gno.land/r/demo/boards/render.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/boards/render.gno b/examples/gno.land/r/demo/boards/render.gno index 1cbf6aaa087..e52879cbd26 100644 --- a/examples/gno.land/r/demo/boards/render.gno +++ b/examples/gno.land/r/demo/boards/render.gno @@ -19,7 +19,7 @@ func RenderBoard(bid BoardID) string { func Render(path string) string { if path == "" { - str := "There are all the boards of this realm:\n\n" + str := "These are all the boards of this realm:\n\n" gBoards.Iterate("", "", func(key string, value interface{}) bool { board := value.(*Board) str += " * [" + board.url + "](" + board.url + ")\n" From 99d50990991cf527efb32e07d00321383fd62bde Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:31:13 +0100 Subject: [PATCH 23/24] fix: proxy Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/app.go | 4 +- contribs/gnodev/cmd/gnodev/setup_web.go | 8 ++ contribs/gnodev/pkg/proxy/path_interceptor.go | 99 ++++++++++--------- 3 files changed, 65 insertions(+), 46 deletions(-) diff --git a/contribs/gnodev/cmd/gnodev/app.go b/contribs/gnodev/cmd/gnodev/app.go index 2af2b41efa0..f840b450179 100644 --- a/contribs/gnodev/cmd/gnodev/app.go +++ b/contribs/gnodev/cmd/gnodev/app.go @@ -331,7 +331,9 @@ func (ds *App) RunServer(ctx context.Context, term *rawterm.RawTerm) error { cancelWith(err) }() - ds.logger.WithGroup(WebLogName).Info("gnoweb started", "lisn", fmt.Sprintf("http://%s", addr)) + ds.logger.WithGroup(WebLogName).Info("gnoweb started", + "lisn", fmt.Sprintf("http://%s", addr)) + } if ds.cfg.interactive { diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go index b6af798f43b..67aa5bcc19b 100644 --- a/contribs/gnodev/cmd/gnodev/setup_web.go +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -20,6 +20,8 @@ func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, remoteAddr string) (htt appcfg.ChainID = cfg.chainId if cfg.webRemoteHelperAddr != "" { appcfg.RemoteHelp = cfg.webRemoteHelperAddr + } else { + appcfg.RemoteHelp = remoteAddr } router, err := gnoweb.NewRouter(logger, appcfg) @@ -27,5 +29,11 @@ func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, remoteAddr string) (htt return nil, fmt.Errorf("unable to create router app: %w", err) } + logger.Debug("gnoweb router created", + "remote", appcfg.NodeRemote, + "helper_remote", appcfg.RemoteHelp, + "html", appcfg.UnsafeHTML, + "chain_id", cfg.chainId, + ) return router, nil } diff --git a/contribs/gnodev/pkg/proxy/path_interceptor.go b/contribs/gnodev/pkg/proxy/path_interceptor.go index 15aebf7419f..bd3bd99bbbe 100644 --- a/contribs/gnodev/pkg/proxy/path_interceptor.go +++ b/contribs/gnodev/pkg/proxy/path_interceptor.go @@ -30,25 +30,25 @@ type PathInterceptor struct { muHandlers sync.RWMutex } -// NewPathInterceptor creates a proxy loader with a logger and target address. +// NewPathInterceptor creates a path proxy interceptor. func NewPathInterceptor(logger *slog.Logger, target net.Addr) (*PathInterceptor, error) { - // Create a lisnener with target addr - porxyListener, err := net.Listen(target.Network(), target.String()) + // Create a listener with the target address + proxyListener, err := net.Listen(target.Network(), target.String()) if err != nil { return nil, fmt.Errorf("failed to listen on %s://%s", target.Network(), target.String()) } // Find a new random port for the target - targertListener, err := net.Listen("tcp", "127.0.0.1:0") + targetListener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { - return nil, fmt.Errorf("failed to listen on %s://%s", target.Network(), target.String()) + return nil, fmt.Errorf("failed to listen on tcp://127.0.0.1:0") } - proxyAddr := targertListener.Addr() - // Immedialty close this listener after proxy init - defer targertListener.Close() + proxyAddr := targetListener.Addr() + // Immediately close this listener after proxy init + defer targetListener.Close() proxy := &PathInterceptor{ - listener: porxyListener, + listener: proxyListener, logger: logger, targetAddr: target, proxyAddr: proxyAddr, @@ -71,7 +71,7 @@ func (proxy *PathInterceptor) ProxyAddress() string { return fmt.Sprintf("%s://%s", proxy.proxyAddr.Network(), proxy.proxyAddr.String()) } -// ProxyAddress returns the network address of the proxy. +// TargetAddress returns the network address of the target. func (proxy *PathInterceptor) TargetAddress() string { return fmt.Sprintf("%s://%s", proxy.targetAddr.Network(), proxy.targetAddr.String()) } @@ -86,6 +86,8 @@ func (proxy *PathInterceptor) handleConnections() { proxy.logger.Debug("failed to accept connection", "error", err) continue } + + proxy.logger.Debug("new connection", "from", conn.RemoteAddr()) go proxy.handleConnection(conn) } } @@ -94,59 +96,66 @@ func (proxy *PathInterceptor) handleConnections() { func (proxy *PathInterceptor) handleConnection(inConn net.Conn) { defer inConn.Close() + outConn, err := net.Dial(proxy.proxyAddr.Network(), proxy.proxyAddr.String()) + if err != nil { + proxy.logger.Error("failed to connect to remote socket", "address", proxy.proxyAddr, "error", err) + return + } + defer outConn.Close() + + // Redirect all the response directly to the incoming connection + go io.Copy(inConn, outConn) + var buffer bytes.Buffer teeReader := io.TeeReader(inConn, &buffer) - defer func() { proxy.forwardRequest(&buffer, inConn) }() + defer func() { + io.Copy(outConn, inConn) // forward everything left + proxy.logger.Debug("connection ended", "from", inConn.RemoteAddr()) + }() - request, err := http.ReadRequest(bufio.NewReader(teeReader)) - if err != nil { - proxy.logger.Debug("failed to read HTTP request", "error", err) - return - } + for { + request, err := http.ReadRequest(bufio.NewReader(teeReader)) + if err != nil { + proxy.logger.Debug("failed to read HTTP request", "error", err) + // not an actual HTTP request, forward connection directly + return + } - if request.Header.Get("Upgrade") == "websocket" { - proxy.logger.Debug("WebSocket connection detected, forwarding directly") - return - } + if request.Header.Get("Upgrade") == "websocket" { + proxy.logger.Debug("WebSocket connection detected, forwarding directly") + // WebSocket connection not supported (yet), forward connection directly + return + } - body, err := io.ReadAll(request.Body) - if err != nil { - proxy.logger.Warn("failed to read request body", "error", err) - return - } - defer request.Body.Close() + body, err := io.ReadAll(request.Body) + if err != nil { + proxy.logger.Warn("failed to read request body", "error", err) + return + } - if err := proxy.handleRequest(body); err != nil { - proxy.logger.Debug("error handling request", "error", err) - } -} + if err := proxy.handleRequest(body); err != nil { + proxy.logger.Debug("error handling request", "error", err) + } -// forwardRequest forwards the buffered request to the target address. -func (proxy *PathInterceptor) forwardRequest(buffer *bytes.Buffer, inConn net.Conn) { - outConn, err := net.Dial(proxy.proxyAddr.Network(), proxy.proxyAddr.String()) - if err != nil { - proxy.logger.Error("failed to connect to remote socket", "address", proxy.proxyAddr, "error", err) - return - } - defer outConn.Close() + request.Body.Close() - if buffer.Len() > 0 { if _, err := outConn.Write(buffer.Bytes()); err != nil { + // forward what we read to the outbound connection proxy.logger.Error("unable to write to socket", "error", err) return } - } - go io.Copy(outConn, inConn) - io.Copy(inConn, outConn) + // Reset buffer + buffer.Reset() + } } // handleRequest parses and processes the RPC request body. func (proxy *PathInterceptor) handleRequest(body []byte) error { paths, err := parseRPCRequest(body) if err != nil { - return fmt.Errorf("unable to parse rpc request: %w", err) + return fmt.Errorf("unable to parse RPC request: %w", err) } if len(paths) == 0 { @@ -178,11 +187,11 @@ type sDataQuery struct { func parseRPCRequest(body []byte) ([]string, error) { var req rpctypes.RPCRequest if err := json.Unmarshal(body, &req); err != nil { - return nil, fmt.Errorf("unable to unmarshal rpc request: %w", err) + return nil, fmt.Errorf("unable to unmarshal RPC request: %w", err) } if req.Method != "abci_query" { - return nil, fmt.Errorf("not an abci query") + return nil, fmt.Errorf("not an ABCI query") } var query sDataQuery From e818b65140e3e8c990533d908bc8bbdc19b0e611 Mon Sep 17 00:00:00 2001 From: gfanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:54:24 +0100 Subject: [PATCH 24/24] chore: lint Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/app.go | 1 - 1 file changed, 1 deletion(-) diff --git a/contribs/gnodev/cmd/gnodev/app.go b/contribs/gnodev/cmd/gnodev/app.go index f840b450179..656bc230147 100644 --- a/contribs/gnodev/cmd/gnodev/app.go +++ b/contribs/gnodev/cmd/gnodev/app.go @@ -333,7 +333,6 @@ func (ds *App) RunServer(ctx context.Context, term *rawterm.RawTerm) error { ds.logger.WithGroup(WebLogName).Info("gnoweb started", "lisn", fmt.Sprintf("http://%s", addr)) - } if ds.cfg.interactive {