diff --git a/changelog.md b/changelog.md index f89ce6de56..3e4a54e532 100644 --- a/changelog.md +++ b/changelog.md @@ -17,6 +17,7 @@ - [#2995](https://github.com/ignite/cli/pull/2995/) Add `ignite network request remove-validator` command. - [#2999](https://github.com/ignite/cli/pull/2999/) Add `ignite network request remove-account` command. - [#2458](https://github.com/ignite/cli/issues/2458) New `chain serve` command UI. +- [#2992](https://github.com/ignite/cli/issues/2992) Add `ignite chain debug` command. ### Changes diff --git a/go.mod b/go.mod index c043b24d98..260b49b994 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/emicklei/proto v1.11.0 github.com/emicklei/proto-contrib v0.12.0 github.com/ghodss/yaml v1.0.0 + github.com/go-delve/delve v1.9.1 github.com/go-git/go-git/v5 v5.4.2 github.com/gobuffalo/genny/v2 v2.1.0 github.com/gobuffalo/logger v1.0.7 @@ -120,12 +121,14 @@ require ( github.com/charmbracelet/glamour v0.2.1-0.20210402234443-abe9cda419ba // indirect github.com/chavacava/garif v0.0.0-20220630083739-93517212f375 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/cilium/ebpf v0.7.0 // indirect github.com/cloudflare/circl v1.1.0 // indirect github.com/coinbase/rosetta-sdk-go v0.7.9 // indirect github.com/confio/ics23/go v0.7.0 // indirect github.com/containerd/cgroups v1.0.3 // indirect github.com/containerd/console v1.0.3 // indirect github.com/containerd/containerd v1.6.8 // indirect + github.com/cosiner/argv v0.1.0 // indirect github.com/cosmos/btcutil v1.0.4 // indirect github.com/cosmos/cosmos-proto v1.0.0-alpha7 // indirect github.com/cosmos/gorocksdb v1.2.0 // indirect @@ -140,6 +143,7 @@ require ( github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/denis-tingaikin/go-header v0.4.3 // indirect + github.com/derekparker/trie v0.0.0-20200317170641-1fdf38b7b0e9 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect @@ -161,6 +165,7 @@ require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect github.com/go-critic/go-critic v0.6.5 // indirect + github.com/go-delve/liner v1.2.3-0.20220127212407-d32d89dd2a5d // indirect github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.3.1 // indirect github.com/go-kit/kit v0.12.0 // indirect @@ -199,6 +204,7 @@ require ( github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-dap v0.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/orderedcode v0.0.1 // indirect github.com/google/uuid v1.3.0 // indirect @@ -358,9 +364,11 @@ require ( github.com/zondax/hid v0.9.1-0.20220302062450-5552068d2266 // indirect gitlab.com/bosi/decorder v0.2.3 // indirect go.opencensus.io v0.23.0 // indirect + go.starlark.net v0.0.0-20220816155156-cfacd8902214 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.23.0 // indirect + golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4 // indirect golang.org/x/crypto v0.3.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/exp/typeparams v0.0.0-20220827204233-334a2380cb91 // indirect diff --git a/go.sum b/go.sum index a575d4b0c4..141425f01b 100644 --- a/go.sum +++ b/go.sum @@ -336,6 +336,7 @@ github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLI github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.7.0 h1:1k/q3ATgxSXRdrmPfH8d7YK0GfqVsEKZAX9dQZvs56k= github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= @@ -468,6 +469,8 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cosiner/argv v0.1.0 h1:BVDiEL32lwHukgJKP87btEPenzrrHUjajs/8yzaqcXg= +github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8= github.com/cosmos/btcutil v1.0.4 h1:n7C2ngKXo7UC9gNyMNLbzqz7Asuf+7Qv4gnX/rOdQ44= github.com/cosmos/btcutil v1.0.4/go.mod h1:Ffqc8Hn6TJUdDgHBwIZLtrLQC1KdJ9jGJl/TvgUaxbU= github.com/cosmos/cosmos-proto v1.0.0-alpha7 h1:yqYUOHF2jopwZh4dVQp3xgqwftE5/2hkrwIV6vkUbO0= @@ -530,6 +533,8 @@ github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRk github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU= github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/derekparker/trie v0.0.0-20200317170641-1fdf38b7b0e9 h1:G765iDCq7bP5opdrPkXk+4V3yfkgV9iGFuheWZ/X/zY= +github.com/derekparker/trie v0.0.0-20200317170641-1fdf38b7b0e9/go.mod h1:D6ICZm05D9VN1n/8iOtBxLpXtoGp6HDFUJ1RNVieOSE= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= @@ -660,6 +665,10 @@ github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1T github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-critic/go-critic v0.6.5 h1:fDaR/5GWURljXwF8Eh31T2GZNz9X4jeboS912mWF8Uo= github.com/go-critic/go-critic v0.6.5/go.mod h1:ezfP/Lh7MA6dBNn4c6ab5ALv3sKnZVLx37tr00uuaOY= +github.com/go-delve/delve v1.9.1 h1:HBvHO5anAntm2ORXKQJqH7R8bezmCuPO+Tf2SJZ2Ojw= +github.com/go-delve/delve v1.9.1/go.mod h1:CET1wODsRJ2vlNepWyFEatwXRJ8rnrbgqaf1d4+Hgi4= +github.com/go-delve/liner v1.2.3-0.20220127212407-d32d89dd2a5d h1:pxjSLshkZJGLVm0wv20f/H0oTWiq/egkoJQ2ja6LEvo= +github.com/go-delve/liner v1.2.3-0.20220127212407-d32d89dd2a5d/go.mod h1:biJCRbqp51wS+I92HMqn5H8/A0PAhxn2vyOT+JqhiGI= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= @@ -868,6 +877,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= +github.com/google/go-dap v0.6.0 h1:Y1RHGUtv3R8y6sXq2dtGRMYrFB2hSqyFVws7jucrzX4= +github.com/google/go-dap v0.6.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ= github.com/google/go-github/v48 v48.1.0 h1:nqPqq+0oRY2AMR/SRskGrrP4nnewPB7e/m2+kbT/UvM= github.com/google/go-github/v48 v48.1.0/go.mod h1:dDlehKBDo850ZPvCTK0sEqTCVWcrGl2LcDiajkYi89Y= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -1006,6 +1017,7 @@ github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -1853,6 +1865,8 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.starlark.net v0.0.0-20220816155156-cfacd8902214 h1:MqijAN3S61c7KWasOk+zIqIjHQPN6WUra/X3+YAkQxQ= +go.starlark.net v0.0.0-20220816155156-cfacd8902214/go.mod h1:VZcBMdr3cT3PnBoWunTabuSEXwVAH+ZJ5zxfs3AdASk= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -1871,6 +1885,8 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4 h1:QlVATYS7JBoZMVaf+cNjb90WD/beKVHnIxFKT4QaHVI= +golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -2163,6 +2179,7 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/ignite/cmd/chain.go b/ignite/cmd/chain.go index d3f954d237..f9876708b0 100644 --- a/ignite/cmd/chain.go +++ b/ignite/cmd/chain.go @@ -85,11 +85,14 @@ chain. c.PersistentFlags().AddFlagSet(flagSetConfig()) c.PersistentFlags().AddFlagSet(flagSetYes()) - c.AddCommand(NewChainServe()) - c.AddCommand(NewChainBuild()) - c.AddCommand(NewChainInit()) - c.AddCommand(NewChainFaucet()) - c.AddCommand(NewChainSimulate()) + c.AddCommand( + NewChainServe(), + NewChainBuild(), + NewChainInit(), + NewChainFaucet(), + NewChainSimulate(), + NewChainDebug(), + ) return c } diff --git a/ignite/cmd/chain_build.go b/ignite/cmd/chain_build.go index 137eefaad9..b539e393ee 100644 --- a/ignite/cmd/chain_build.go +++ b/ignite/cmd/chain_build.go @@ -14,6 +14,7 @@ import ( const ( flagCheckDependencies = "check-dependencies" + flagDebug = "debug" flagOutput = "output" flagRelease = "release" flagReleasePrefix = "release.prefix" @@ -85,6 +86,7 @@ for your current environment. flagSetClearCache(c) c.Flags().AddFlagSet(flagSetCheckDependencies()) c.Flags().AddFlagSet(flagSetSkipProto()) + c.Flags().AddFlagSet(flagSetDebug()) c.Flags().Bool(flagRelease, false, "build for a release") c.Flags().StringSliceP(flagReleaseTargets, "t", []string{}, "release targets. Available only with --release flag") c.Flags().String(flagReleasePrefix, "", "tarball prefix for each release target. Available only with --release flag") @@ -127,8 +129,9 @@ func chainBuildHandler(cmd *cobra.Command, _ []string) error { return err } + ctx := cmd.Context() if isRelease { - releasePath, err := c.BuildRelease(cmd.Context(), cacheStorage, output, releasePrefix, releaseTargets...) + releasePath, err := c.BuildRelease(ctx, cacheStorage, output, releasePrefix, releaseTargets...) if err != nil { return err } @@ -136,7 +139,7 @@ func chainBuildHandler(cmd *cobra.Command, _ []string) error { return session.Printf("🗃 Release created: %s\n", colors.Info(releasePath)) } - binaryName, err := c.Build(cmd.Context(), cacheStorage, output, flagGetSkipProto(cmd)) + binaryName, err := c.Build(ctx, cacheStorage, output, flagGetSkipProto(cmd), flagGetDebug(cmd)) if err != nil { return err } @@ -160,3 +163,14 @@ func flagGetCheckDependencies(cmd *cobra.Command) (check bool) { check, _ = cmd.Flags().GetBool(flagCheckDependencies) return } + +func flagSetDebug() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.Bool(flagDebug, false, "build a debug binary") + return fs +} + +func flagGetDebug(cmd *cobra.Command) (debug bool) { + debug, _ = cmd.Flags().GetBool(flagDebug) + return +} diff --git a/ignite/cmd/chain_debug.go b/ignite/cmd/chain_debug.go new file mode 100644 index 0000000000..dc539748aa --- /dev/null +++ b/ignite/cmd/chain_debug.go @@ -0,0 +1,182 @@ +package ignitecmd + +import ( + "context" + "errors" + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/spf13/cobra" + + cmdmodel "github.com/ignite/cli/ignite/cmd/model" + "github.com/ignite/cli/ignite/pkg/chaincmd" + "github.com/ignite/cli/ignite/pkg/cliui" + "github.com/ignite/cli/ignite/pkg/cliui/icons" + cliuimodel "github.com/ignite/cli/ignite/pkg/cliui/model" + "github.com/ignite/cli/ignite/pkg/debugger" + "github.com/ignite/cli/ignite/pkg/events" + "github.com/ignite/cli/ignite/pkg/xurl" + "github.com/ignite/cli/ignite/services/chain" +) + +const ( + flagServer = "server" + flagServerAddress = "server-address" +) + +// NewChainDebug returns a new debug command to debug a blockchain app. +func NewChainDebug() *cobra.Command { + c := &cobra.Command{ + Use: "debug", + Short: "Launch a debugger for a blockchain app", + Long: `The debug command starts a debug server and launches a debugger. + +Ignite uses the Delve debugger by default. Delve enables you to interact with +your program by controlling the execution of the process, evaluating variables, +and providing information of thread / goroutine state, CPU register state and +more. + +A debug server can optionally be started in cases where default terminal client +is not desirable. When the server starts it first runs the blockchain app, +attaches to it and finally waits for a client connection. It accepts both +JSON-RPC or DAP client connections. + +To start a debug server use the following flag: + + ignite chain debug --server + +To start a debug server with a custom address use the following flags: + + ignite chain debug --server --server-address 127.0.0.1:30500 + +The debug server stops automatically when the client connection is closed. +`, + Args: cobra.NoArgs, + RunE: chainDebugHandler, + } + + flagSetPath(c) + c.Flags().Bool(flagServer, false, "start a debug server") + c.Flags().String(flagServerAddress, debugger.DefaultAddress, "debug server address") + + return c +} + +func chainDebugHandler(cmd *cobra.Command, _ []string) error { + // Prepare session options. + // Events are ignored by the session when the debug server UI is used. + options := []cliui.Option{cliui.StartSpinnerWithText("Initializing...")} + server, _ := cmd.Flags().GetBool(flagServer) + if server { + options = append(options, cliui.IgnoreEvents()) + } + + session := cliui.New(options...) + defer session.End() + + // Start debug server + if server { + bus := session.EventBus() + m := cmdmodel.NewChainDebug(cmd, bus, chainDebugCmd(cmd, session)) + return tea.NewProgram(m).Start() + } + + return chainDebug(cmd, session) +} + +func chainDebugCmd(cmd *cobra.Command, session *cliui.Session) tea.Cmd { + return func() tea.Msg { + if err := chainDebug(cmd, session); err != nil && !errors.Is(err, context.Canceled) { + return cliuimodel.ErrorMsg{Error: err} + } + return cliuimodel.QuitMsg{} + } +} + +func chainDebug(cmd *cobra.Command, session *cliui.Session) error { + chainOptions := []chain.Option{ + chain.KeyringBackend(chaincmd.KeyringBackendTest), + } + + config, err := cmd.Flags().GetString(flagConfig) + if err != nil { + return err + } + if config != "" { + chainOptions = append(chainOptions, chain.ConfigFile(config)) + } + + c, err := newChainWithHomeFlags(cmd, chainOptions...) + if err != nil { + return err + } + + cfg, err := c.Config() + if err != nil { + return err + } + + // TODO: Replace by config.FirstValidator when PR #3199 is merged + validator := cfg.Validators[0] + servers, err := validator.GetServers() + if err != nil { + return err + } + + home, err := c.Home() + if err != nil { + return err + } + + binPath, err := c.AbsBinaryPath() + if err != nil { + return err + } + + // Common debugger options + debugOptions := []debugger.Option{ + debugger.WorkingDir(flagGetPath(cmd)), + debugger.BinaryArgs( + "start", + "--pruning", "nothing", + "--grpc.address", servers.GRPC.Address, + "--home", home, + ), + } + + // Start debug server + ctx := cmd.Context() + bus := session.EventBus() + if server, _ := cmd.Flags().GetBool(flagServer); server { + addr, _ := cmd.Flags().GetString(flagServerAddress) + tcpAddr, err := xurl.TCP(addr) + if err != nil { + return err + } + + debugOptions = append(debugOptions, + debugger.Address(addr), + debugger.ServerStartHook(func() { + bus.Send( + fmt.Sprintf("Debug server: %s", tcpAddr), + events.Icon(icons.Earth), + events.ProgressFinish(), + ) + }), + ) + + bus.Send("Launching debug server", events.ProgressUpdate()) + return debugger.Start(ctx, binPath, debugOptions...) + } + + // Launch a debugger client + debugOptions = append(debugOptions, + debugger.ClientRunHook(func() { + // End session to allow debugger to gain control of stdout + session.End() + }), + ) + + bus.Send("Launching debugger", events.ProgressUpdate()) + return debugger.Run(ctx, binPath, debugOptions...) +} diff --git a/ignite/cmd/chain_init.go b/ignite/cmd/chain_init.go index 25beff1c54..1e8986aeb8 100644 --- a/ignite/cmd/chain_init.go +++ b/ignite/cmd/chain_init.go @@ -87,6 +87,7 @@ commands manually to ensure a production-level node initialization. c.Flags().AddFlagSet(flagSetHome()) c.Flags().AddFlagSet(flagSetCheckDependencies()) c.Flags().AddFlagSet(flagSetSkipProto()) + c.Flags().AddFlagSet(flagSetDebug()) return c } @@ -118,11 +119,12 @@ func chainInitHandler(cmd *cobra.Command, _ []string) error { return err } - if _, err := c.Build(cmd.Context(), cacheStorage, "", flagGetSkipProto(cmd)); err != nil { + ctx := cmd.Context() + if _, err = c.Build(ctx, cacheStorage, "", flagGetSkipProto(cmd), flagGetDebug(cmd)); err != nil { return err } - if err := c.Init(cmd.Context(), chain.InitArgsAll); err != nil { + if err := c.Init(ctx, chain.InitArgsAll); err != nil { return err } diff --git a/ignite/cmd/model/chain_debug.go b/ignite/cmd/model/chain_debug.go new file mode 100644 index 0000000000..ac5ad6f205 --- /dev/null +++ b/ignite/cmd/model/chain_debug.go @@ -0,0 +1,145 @@ +package cmdmodel + +import ( + "context" + "fmt" + "strings" + + tea "github.com/charmbracelet/bubbletea" + + "github.com/ignite/cli/ignite/pkg/cliui/colors" + cliuimodel "github.com/ignite/cli/ignite/pkg/cliui/model" + "github.com/ignite/cli/ignite/pkg/events" + "github.com/ignite/cli/ignite/pkg/xstrings" +) + +const ( + stateChainDebugStarting uint = iota + stateChainDebugRunning +) + +var msgStopDebug = colors.Faint("Press the 'q' key to stop debug server") + +// NewChainDebug returns a new UI model for the chain debug command. +func NewChainDebug(mCtx Context, bus events.Provider, cmd tea.Cmd) ChainDebug { + // Initialize a context and cancel function to stop execution + ctx, quit := context.WithCancel(mCtx.Context()) + + // Update the context to allow stopping by using the 'q' key + mCtx.SetContext(ctx) + + return ChainDebug{ + cmd: cmd, + quit: quit, + model: cliuimodel.NewEvents(bus), + } +} + +// ChainDebug defines a UI model for the chain debug command. +type ChainDebug struct { + cmd tea.Cmd + quit context.CancelFunc + state uint + error error + model cliuimodel.Events +} + +// Init is the first function that will be called. +// It returns a batch command that listen events and also runs the debug server. +func (m ChainDebug) Init() tea.Cmd { + return tea.Batch(m.model.WaitEvent, m.cmd) +} + +// Update is called when a message is received. +// It handles messages and executes the logic that updates the model. +func (m ChainDebug) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case cliuimodel.QuitMsg: + return m.processQuitMsg(msg) + case cliuimodel.ErrorMsg: + return m.processErrorMsg(msg) + case tea.KeyMsg: + return m.processKeyMsg(msg) + case cliuimodel.EventMsg: + return m.processEventMsg(msg) + } + + return m.updateModel(msg) +} + +// View renders the UI after every update. +func (m ChainDebug) View() string { + if m.error != nil { + // Make sure that the error is displayed in the same way + // that the output used by non UI based commands to keep + // consistency on error when the debug command is run + // without the `--server` flag. + return fmt.Sprintln(xstrings.ToUpperFirst(m.error.Error())) + } + + var view strings.Builder + + switch m.state { + case stateChainServeStarting: + view.WriteString(m.renderStartView()) + case stateChainServeRunning: + view.WriteString(m.renderRunView()) + view.WriteString(m.renderActions()) + } + + return cliuimodel.FormatView(view.String()) +} + +func (m ChainDebug) updateModel(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + m.model, cmd = m.model.Update(msg) + return m, cmd +} + +func (m ChainDebug) processQuitMsg(msg cliuimodel.QuitMsg) (tea.Model, tea.Cmd) { + return m, tea.Quit +} + +func (m ChainDebug) processErrorMsg(msg cliuimodel.ErrorMsg) (tea.Model, tea.Cmd) { + m.error = msg.Error + return m, tea.Quit +} + +func (m ChainDebug) processKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + if checkQuitKeyMsg(msg) { + m.quit() + } + + return m, nil +} + +func (m ChainDebug) processEventMsg(msg cliuimodel.EventMsg) (tea.Model, tea.Cmd) { + if m.state == stateChainDebugStarting { + // Start view displays status events until the debug server is running. + // When the status finish event is not an error it means that the debug + // server started successfully and the run view is displayed. + if msg.ProgressIndication == events.IndicationFinish { + m.model.ClearEvents() + m.state = stateChainDebugRunning + } + } + + return m.updateModel(msg) +} + +func (m ChainDebug) renderActions() string { + return fmt.Sprintf("\n%s\n", msgStopDebug) +} + +func (m ChainDebug) renderStartView() string { + return m.model.View() +} + +func (m ChainDebug) renderRunView() string { + var view strings.Builder + + view.WriteString("Blockchain is running\n\n") + view.WriteString(m.model.View()) + + return view.String() +} diff --git a/ignite/cmd/model/chain_debug_test.go b/ignite/cmd/model/chain_debug_test.go new file mode 100644 index 0000000000..4e622daffb --- /dev/null +++ b/ignite/cmd/model/chain_debug_test.go @@ -0,0 +1,83 @@ +package cmdmodel_test + +import ( + "errors" + "fmt" + "testing" + + tea "github.com/charmbracelet/bubbletea" + "github.com/stretchr/testify/require" + + cmdmodel "github.com/ignite/cli/ignite/cmd/model" + "github.com/ignite/cli/ignite/cmd/model/testdata" + "github.com/ignite/cli/ignite/pkg/cliui/colors" + "github.com/ignite/cli/ignite/pkg/cliui/icons" + cliuimodel "github.com/ignite/cli/ignite/pkg/cliui/model" + "github.com/ignite/cli/ignite/pkg/events" +) + +func TestChainDebugErrorView(t *testing.T) { + // Arrange + var model tea.Model + + err := errors.New("Test error") + model = cmdmodel.NewChainDebug(testdata.ModelContext{}, testdata.DummyEventsProvider{}, testdata.FooCmd) + want := fmt.Sprintf("%s %s\n", icons.NotOK, colors.Error(err.Error())) + + // Arrange: Update model with an error message + model, _ = model.Update(cliuimodel.ErrorMsg{Error: err}) + + // Act + view := model.View() + + // Assert + require.Equal(t, want, view) +} + +func TestChainDebugStartView(t *testing.T) { + // Arrange + var model tea.Model + + spinner := cliuimodel.NewSpinner() + queue := []string{"Event 1...", "Event 2..."} + model = cmdmodel.NewChainDebug(testdata.ModelContext{}, testdata.DummyEventsProvider{}, testdata.FooCmd) + + want := fmt.Sprintf("\n%s%s\n", spinner.View(), queue[1]) + want = cliuimodel.FormatView(want) + + // Arrange: Update model with status events + for _, s := range queue { + model, _ = model.Update(cliuimodel.EventMsg{ + Event: events.New(s, events.ProgressStart()), + }) + } + + // Act + view := model.View() + + // Assert + require.Equal(t, want, view) +} + +func TestChainDebugRunView(t *testing.T) { + // Arrange + var model tea.Model + + evt := "Debug server: tcp://127.0.0.1:30500" + actions := colors.Faint("Press the 'q' key to stop debug server") + model = cmdmodel.NewChainDebug(testdata.ModelContext{}, testdata.DummyEventsProvider{}, testdata.FooCmd) + + want := fmt.Sprintf("Blockchain is running\n\n%s\n\n%s\n", evt, actions) + want = cliuimodel.FormatView(want) + + // Arrange: Update model with a server running event + model, _ = model.Update(cliuimodel.EventMsg{ + Event: events.New(evt, events.ProgressFinish()), + }) + + // Act + view := model.View() + + // Assert + require.Equal(t, want, view) +} diff --git a/ignite/cmd/model/chain_serve.go b/ignite/cmd/model/chain_serve.go index 62648a8768..d7af1c1d4e 100644 --- a/ignite/cmd/model/chain_serve.go +++ b/ignite/cmd/model/chain_serve.go @@ -71,11 +71,15 @@ type ChainServe struct { quitModel cliuimodel.Events } +// Init is the first function that will be called. +// It returns a batch command that listen events and also runs the blockchain app. func (m ChainServe) Init() tea.Cmd { // On initialization wait for status events and start serving the blockchain return tea.Batch(m.startModel.WaitEvent, m.cmd) } +// Update is called when a message is received. +// It handles messages and executes the logic that updates the model. func (m ChainServe) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if checkQuitKeyMsg(msg) { m.state = stateChainServeQuitting @@ -95,6 +99,7 @@ func (m ChainServe) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } +// View renders the UI after every update. func (m ChainServe) View() string { if m.error != nil { return fmt.Sprintf("%s %s\n", icons.NotOK, colors.Error(m.error.Error())) diff --git a/ignite/cmd/model/chain_serve_test.go b/ignite/cmd/model/chain_serve_test.go index 011713dfdf..99c47eaa96 100644 --- a/ignite/cmd/model/chain_serve_test.go +++ b/ignite/cmd/model/chain_serve_test.go @@ -18,7 +18,7 @@ import ( "github.com/ignite/cli/ignite/pkg/events" ) -var actions = colors.Faint("Press the 'q' key to stop serve") +var chainServeActions = colors.Faint("Press the 'q' key to stop serve") func TestChainServeErrorView(t *testing.T) { // Arrange @@ -53,7 +53,7 @@ func TestChainServeStartView(t *testing.T) { icons.OK, strings.TrimSuffix(queue[0], "..."), colors.Faint("0s"), - actions, + chainServeActions, ) want = cliuimodel.FormatView(want) @@ -79,7 +79,7 @@ func TestChainServeRunView(t *testing.T) { queue := []string{"Event 1", "Event 2"} model = cmdmodel.NewChainServe(testdata.ModelContext{}, testdata.DummyEventsProvider{}, testdata.FooCmd) - want := fmt.Sprintf("Blockchain is running\n\n%s\n%s\n\n%s\n", queue[0], queue[1], actions) + want := fmt.Sprintf("Blockchain is running\n\n%s\n%s\n\n%s\n", queue[0], queue[1], chainServeActions) want = cliuimodel.FormatView(want) // Arrange: Update model with events @@ -104,7 +104,7 @@ func TestChainServeRunBrokenView(t *testing.T) { traceback := "Error traceback\nFoo" waitingFix := colors.Info("Waiting for a fix before retrying...") - want := fmt.Sprintf("%s\n\n%s\n\n%s\n", traceback, waitingFix, actions) + want := fmt.Sprintf("%s\n\n%s\n\n%s\n", traceback, waitingFix, chainServeActions) want = cliuimodel.FormatView(want) // Arrange: Update model to display the run view @@ -142,7 +142,7 @@ func TestChainServeRebuildView(t *testing.T) { duration, icons.OK, duration, - actions, + chainServeActions, ) want = cliuimodel.FormatView(want) diff --git a/ignite/pkg/cliui/cliui.go b/ignite/pkg/cliui/cliui.go index 240accaef9..c04844e66d 100644 --- a/ignite/pkg/cliui/cliui.go +++ b/ignite/pkg/cliui/cliui.go @@ -34,6 +34,7 @@ type Session struct { spinner *clispinner.Spinner out uilog.Output wg *sync.WaitGroup + ended bool } // Option configures session options. @@ -257,10 +258,15 @@ func (s Session) PrintTable(header []string, entries ...[]string) error { // End finishes the session by stopping the spinner and the event bus. // Once the session is ended it should not be used anymore. -func (s Session) End() { +func (s *Session) End() { + if s.ended { + return + } + s.StopSpinner() s.ev.Stop() s.wg.Wait() + s.ended = true } func (s *Session) handleEvents() { diff --git a/ignite/pkg/debugger/server.go b/ignite/pkg/debugger/server.go new file mode 100644 index 0000000000..48041b6f00 --- /dev/null +++ b/ignite/pkg/debugger/server.go @@ -0,0 +1,186 @@ +package debugger + +import ( + "context" + "fmt" + "net" + + "github.com/go-delve/delve/pkg/logflags" + "github.com/go-delve/delve/pkg/terminal" + "github.com/go-delve/delve/service" + "github.com/go-delve/delve/service/debugger" + "github.com/go-delve/delve/service/rpc2" + "github.com/go-delve/delve/service/rpccommon" + "golang.org/x/sync/errgroup" +) + +const ( + // DefaultAddress defines the default debug server address. + DefaultAddress = "127.0.0.1:30500" + + // DefaultWorkingDir defines the default directory to use as + // working dir when running the app binary that will be debugged. + DefaultWorkingDir = "." +) + +// Options configures debugging. +type Option func(*debuggerOptions) + +type debuggerOptions struct { + disconnectChan chan struct{} + address, workingDir string + listener net.Listener + binaryArgs []string + clientRunHook, serverStartHook func() +} + +// Address sets the address for the debug server. +func Address(address string) Option { + return func(o *debuggerOptions) { + o.address = address + } +} + +// DisconnectChannel sets the channel used by the server to signal when the client disconnects. +func DisconnectChannel(c chan struct{}) Option { + return func(o *debuggerOptions) { + o.disconnectChan = c + } +} + +// Listener sets a custom listener to serve requests. +func Listener(l net.Listener) Option { + return func(o *debuggerOptions) { + o.listener = l + } +} + +// WorkingDir sets the working directory of the new process. +func WorkingDir(path string) Option { + return func(o *debuggerOptions) { + o.workingDir = path + } +} + +// BinaryArgs sets command line argument for the new process. +func BinaryArgs(args ...string) Option { + return func(o *debuggerOptions) { + o.binaryArgs = args + } +} + +// ClientRunHook sets a function to be executed right before debug client is run. +func ClientRunHook(fn func()) Option { + return func(o *debuggerOptions) { + o.clientRunHook = fn + } +} + +// ServerStartHook sets a function to be executed right before debug server starts. +func ServerStartHook(fn func()) Option { + return func(o *debuggerOptions) { + o.serverStartHook = fn + } +} + +// Start starts a debug server. +func Start(ctx context.Context, binaryPath string, options ...Option) (err error) { + o := applyDebuggerOptions(options...) + + listener := o.listener + if listener == nil { + var c net.ListenConfig + + listener, err = c.Listen(ctx, "tcp", o.address) + if err != nil { + return err + } + + defer listener.Close() + } + + if err = disableDelveLogging(); err != nil { + return err + } + + server := rpccommon.NewServer(&service.Config{ + Listener: listener, + AcceptMulti: false, + APIVersion: 2, + CheckLocalConnUser: true, + DisconnectChan: o.disconnectChan, + ProcessArgs: append([]string{binaryPath}, o.binaryArgs...), + Debugger: debugger.Config{ + WorkingDir: o.workingDir, + Backend: "default", + }, + }) + + if o.serverStartHook != nil { + o.serverStartHook() + } + + if err = server.Run(); err != nil { + return fmt.Errorf("failed to run debug server: %w", err) + } + + defer server.Stop() + + // Wait until the context is done or the connected client disconnects + select { + case <-ctx.Done(): + case <-o.disconnectChan: + } + + return nil +} + +// Run runs a debug client. +func Run(ctx context.Context, binaryPath string, options ...Option) error { + listener, conn := service.ListenerPipe() + defer listener.Close() + + o := applyDebuggerOptions(options...) + + options = append(options, Listener(listener)) + g, ctx := errgroup.WithContext(ctx) + + // Start the debbugger server + g.Go(func() error { + return Start(ctx, binaryPath, options...) + }) + + // Start the debug client + g.Go(func() error { + client := rpc2.NewClientFromConn(conn) + term := terminal.New(client, nil) + + if o.clientRunHook != nil { + o.clientRunHook() + } + + _, err := term.Run() + return err + }) + + return g.Wait() +} + +func applyDebuggerOptions(options ...Option) debuggerOptions { + o := debuggerOptions{ + address: DefaultAddress, + workingDir: DefaultWorkingDir, + disconnectChan: make(chan struct{}), + } + for _, apply := range options { + apply(&o) + } + return o +} + +func disableDelveLogging() error { + if err := logflags.Setup(false, "", ""); err != nil { + return err + } + return nil +} diff --git a/ignite/pkg/gocmd/gocmd.go b/ignite/pkg/gocmd/gocmd.go index 3680dda39d..518227715c 100644 --- a/ignite/pkg/gocmd/gocmd.go +++ b/ignite/pkg/gocmd/gocmd.go @@ -38,19 +38,19 @@ const ( // CommandEnv represents go "env" command. CommandEnv = "env" -) - -const ( - FlagMod = "-mod" - FlagModValueReadOnly = "readonly" - FlagLdflags = "-ldflags" - FlagOut = "-o" -) -const ( - EnvGOOS = "GOOS" + // Go environment variable names EnvGOARCH = "GOARCH" EnvGOMOD = "GOMOD" + EnvGOOS = "GOOS" + + // Go command flags and values + FlagGcflags = "-gcflags" + FlagGcflagsValueDebug = "all=-N -l" + FlagLdflags = "-ldflags" + FlagMod = "-mod" + FlagModValueReadOnly = "readonly" + FlagOut = "-o" ) // Env returns the value of `go env name` diff --git a/ignite/services/chain/build.go b/ignite/services/chain/build.go index 1b6f8a02f0..2250c6f90b 100644 --- a/ignite/services/chain/build.go +++ b/ignite/services/chain/build.go @@ -35,13 +35,13 @@ func (c *Chain) Build( ctx context.Context, cacheStorage cache.Storage, output string, - skipProto bool, + skipProto, debug bool, ) (binaryName string, err error) { if err := c.setup(); err != nil { return "", err } - if err := c.build(ctx, cacheStorage, output, skipProto, false); err != nil { + if err := c.build(ctx, cacheStorage, output, skipProto, false, debug); err != nil { return "", err } @@ -52,7 +52,7 @@ func (c *Chain) build( ctx context.Context, cacheStorage cache.Storage, output string, - skipProto, generateClients bool, + skipProto, generateClients, debug bool, ) (err error) { defer func() { var exitErr *exec.ExitError @@ -74,6 +74,11 @@ func (c *Chain) build( return err } + if debug { + // Add flags to disable binary optimizations and inlining to allow debugging + buildFlags = append(buildFlags, gocmd.FlagGcflags, gocmd.FlagGcflagsValueDebug) + } + binary, err := c.Binary() if err != nil { return err diff --git a/ignite/services/chain/serve.go b/ignite/services/chain/serve.go index 51487ffa6c..9f4f133962 100644 --- a/ignite/services/chain/serve.go +++ b/ignite/services/chain/serve.go @@ -389,7 +389,7 @@ func (c *Chain) serve( // build phase if !isInit || appModified { // build the blockchain app - if err := c.build(ctx, cacheStorage, "", skipProto, generateClients); err != nil { + if err := c.build(ctx, cacheStorage, "", skipProto, generateClients, true); err != nil { return err } } diff --git a/ignite/services/network/networkchain/networkchain.go b/ignite/services/network/networkchain/networkchain.go index 11339f17d6..5a775f8a65 100644 --- a/ignite/services/network/networkchain/networkchain.go +++ b/ignite/services/network/networkchain/networkchain.go @@ -328,7 +328,7 @@ func (c *Chain) Build(ctx context.Context, cacheStorage cache.Storage) (binaryNa c.ev.Send("Building the chain's binary", events.ProgressStart()) // build binary - if binaryName, err = c.chain.Build(ctx, cacheStorage, "", true); err != nil { + if binaryName, err = c.chain.Build(ctx, cacheStorage, "", true, false); err != nil { return "", err }