github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/cmd/cmd.go (about)

     1  // Copyright 2018 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package cmd handles the gopls command line.
     6  // It contains a handler for each of the modes, along with all the flag handling
     7  // and the command line output format.
     8  package cmd
     9  
    10  import (
    11  	"context"
    12  	"flag"
    13  	"fmt"
    14  	"go/token"
    15  	"io/ioutil"
    16  	"log"
    17  	"os"
    18  	"reflect"
    19  	"sort"
    20  	"strings"
    21  	"sync"
    22  	"text/tabwriter"
    23  	"time"
    24  
    25  	"github.com/jhump/golang-x-tools/internal/jsonrpc2"
    26  	"github.com/jhump/golang-x-tools/internal/lsp"
    27  	"github.com/jhump/golang-x-tools/internal/lsp/cache"
    28  	"github.com/jhump/golang-x-tools/internal/lsp/debug"
    29  	"github.com/jhump/golang-x-tools/internal/lsp/lsprpc"
    30  	"github.com/jhump/golang-x-tools/internal/lsp/protocol"
    31  	"github.com/jhump/golang-x-tools/internal/lsp/source"
    32  	"github.com/jhump/golang-x-tools/internal/span"
    33  	"github.com/jhump/golang-x-tools/internal/tool"
    34  	"github.com/jhump/golang-x-tools/internal/xcontext"
    35  	errors "golang.org/x/xerrors"
    36  )
    37  
    38  // Application is the main application as passed to tool.Main
    39  // It handles the main command line parsing and dispatch to the sub commands.
    40  type Application struct {
    41  	// Core application flags
    42  
    43  	// Embed the basic profiling flags supported by the tool package
    44  	tool.Profile
    45  
    46  	// We include the server configuration directly for now, so the flags work
    47  	// even without the verb.
    48  	// TODO: Remove this when we stop allowing the serve verb by default.
    49  	Serve Serve
    50  
    51  	// the options configuring function to invoke when building a server
    52  	options func(*source.Options)
    53  
    54  	// The name of the binary, used in help and telemetry.
    55  	name string
    56  
    57  	// The working directory to run commands in.
    58  	wd string
    59  
    60  	// The environment variables to use.
    61  	env []string
    62  
    63  	// Support for remote LSP server.
    64  	Remote string `flag:"remote" help:"forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment."`
    65  
    66  	// Verbose enables verbose logging.
    67  	Verbose bool `flag:"v,verbose" help:"verbose output"`
    68  
    69  	// VeryVerbose enables a higher level of verbosity in logging output.
    70  	VeryVerbose bool `flag:"vv,veryverbose" help:"very verbose output"`
    71  
    72  	// Control ocagent export of telemetry
    73  	OCAgent string `flag:"ocagent" help:"the address of the ocagent (e.g. http://localhost:55678), or off"`
    74  
    75  	// PrepareOptions is called to update the options when a new view is built.
    76  	// It is primarily to allow the behavior of gopls to be modified by hooks.
    77  	PrepareOptions func(*source.Options)
    78  }
    79  
    80  func (app *Application) verbose() bool {
    81  	return app.Verbose || app.VeryVerbose
    82  }
    83  
    84  // New returns a new Application ready to run.
    85  func New(name, wd string, env []string, options func(*source.Options)) *Application {
    86  	if wd == "" {
    87  		wd, _ = os.Getwd()
    88  	}
    89  	app := &Application{
    90  		options: options,
    91  		name:    name,
    92  		wd:      wd,
    93  		env:     env,
    94  		OCAgent: "off", //TODO: Remove this line to default the exporter to on
    95  
    96  		Serve: Serve{
    97  			RemoteListenTimeout: 1 * time.Minute,
    98  		},
    99  	}
   100  	app.Serve.app = app
   101  	return app
   102  }
   103  
   104  // Name implements tool.Application returning the binary name.
   105  func (app *Application) Name() string { return app.name }
   106  
   107  // Usage implements tool.Application returning empty extra argument usage.
   108  func (app *Application) Usage() string { return "" }
   109  
   110  // ShortHelp implements tool.Application returning the main binary help.
   111  func (app *Application) ShortHelp() string {
   112  	return ""
   113  }
   114  
   115  // DetailedHelp implements tool.Application returning the main binary help.
   116  // This includes the short help for all the sub commands.
   117  func (app *Application) DetailedHelp(f *flag.FlagSet) {
   118  	w := tabwriter.NewWriter(f.Output(), 0, 0, 2, ' ', 0)
   119  	defer w.Flush()
   120  
   121  	fmt.Fprint(w, `
   122  gopls is a Go language server.
   123  
   124  It is typically used with an editor to provide language features. When no
   125  command is specified, gopls will default to the 'serve' command. The language
   126  features can also be accessed via the gopls command-line interface.
   127  
   128  Usage:
   129    gopls help [<subject>]
   130  
   131  Command:
   132  `)
   133  	fmt.Fprint(w, "\nMain\t\n")
   134  	for _, c := range app.mainCommands() {
   135  		fmt.Fprintf(w, "  %s\t%s\n", c.Name(), c.ShortHelp())
   136  	}
   137  	fmt.Fprint(w, "\t\nFeatures\t\n")
   138  	for _, c := range app.featureCommands() {
   139  		fmt.Fprintf(w, "  %s\t%s\n", c.Name(), c.ShortHelp())
   140  	}
   141  	fmt.Fprint(w, "\nflags:\n")
   142  	printFlagDefaults(f)
   143  }
   144  
   145  // this is a slightly modified version of flag.PrintDefaults to give us control
   146  func printFlagDefaults(s *flag.FlagSet) {
   147  	var flags [][]*flag.Flag
   148  	seen := map[flag.Value]int{}
   149  	s.VisitAll(func(f *flag.Flag) {
   150  		if i, ok := seen[f.Value]; !ok {
   151  			seen[f.Value] = len(flags)
   152  			flags = append(flags, []*flag.Flag{f})
   153  		} else {
   154  			flags[i] = append(flags[i], f)
   155  		}
   156  	})
   157  	for _, entry := range flags {
   158  		sort.SliceStable(entry, func(i, j int) bool {
   159  			return len(entry[i].Name) < len(entry[j].Name)
   160  		})
   161  		var b strings.Builder
   162  		for i, f := range entry {
   163  			switch i {
   164  			case 0:
   165  				b.WriteString("  -")
   166  			default:
   167  				b.WriteString(",-")
   168  			}
   169  			b.WriteString(f.Name)
   170  		}
   171  
   172  		f := entry[0]
   173  		name, usage := flag.UnquoteUsage(f)
   174  		if len(name) > 0 {
   175  			b.WriteString("=")
   176  			b.WriteString(name)
   177  		}
   178  		// Boolean flags of one ASCII letter are so common we
   179  		// treat them specially, putting their usage on the same line.
   180  		if b.Len() <= 4 { // space, space, '-', 'x'.
   181  			b.WriteString("\t")
   182  		} else {
   183  			// Four spaces before the tab triggers good alignment
   184  			// for both 4- and 8-space tab stops.
   185  			b.WriteString("\n    \t")
   186  		}
   187  		b.WriteString(strings.ReplaceAll(usage, "\n", "\n    \t"))
   188  		if !isZeroValue(f, f.DefValue) {
   189  			if reflect.TypeOf(f.Value).Elem().Name() == "stringValue" {
   190  				fmt.Fprintf(&b, " (default %q)", f.DefValue)
   191  			} else {
   192  				fmt.Fprintf(&b, " (default %v)", f.DefValue)
   193  			}
   194  		}
   195  		fmt.Fprint(s.Output(), b.String(), "\n")
   196  	}
   197  }
   198  
   199  // isZeroValue is copied from the flags package
   200  func isZeroValue(f *flag.Flag, value string) bool {
   201  	// Build a zero value of the flag's Value type, and see if the
   202  	// result of calling its String method equals the value passed in.
   203  	// This works unless the Value type is itself an interface type.
   204  	typ := reflect.TypeOf(f.Value)
   205  	var z reflect.Value
   206  	if typ.Kind() == reflect.Ptr {
   207  		z = reflect.New(typ.Elem())
   208  	} else {
   209  		z = reflect.Zero(typ)
   210  	}
   211  	return value == z.Interface().(flag.Value).String()
   212  }
   213  
   214  // Run takes the args after top level flag processing, and invokes the correct
   215  // sub command as specified by the first argument.
   216  // If no arguments are passed it will invoke the server sub command, as a
   217  // temporary measure for compatibility.
   218  func (app *Application) Run(ctx context.Context, args ...string) error {
   219  	ctx = debug.WithInstance(ctx, app.wd, app.OCAgent)
   220  	if len(args) == 0 {
   221  		s := flag.NewFlagSet(app.Name(), flag.ExitOnError)
   222  		return tool.Run(ctx, s, &app.Serve, args)
   223  	}
   224  	command, args := args[0], args[1:]
   225  	for _, c := range app.Commands() {
   226  		if c.Name() == command {
   227  			s := flag.NewFlagSet(app.Name(), flag.ExitOnError)
   228  			return tool.Run(ctx, s, c, args)
   229  		}
   230  	}
   231  	return tool.CommandLineErrorf("Unknown command %v", command)
   232  }
   233  
   234  // commands returns the set of commands supported by the gopls tool on the
   235  // command line.
   236  // The command is specified by the first non flag argument.
   237  func (app *Application) Commands() []tool.Application {
   238  	var commands []tool.Application
   239  	commands = append(commands, app.mainCommands()...)
   240  	commands = append(commands, app.featureCommands()...)
   241  	return commands
   242  }
   243  
   244  func (app *Application) mainCommands() []tool.Application {
   245  	return []tool.Application{
   246  		&app.Serve,
   247  		&version{app: app},
   248  		&bug{app: app},
   249  		&apiJSON{app: app},
   250  		&licenses{app: app},
   251  	}
   252  }
   253  
   254  func (app *Application) featureCommands() []tool.Application {
   255  	return []tool.Application{
   256  		&callHierarchy{app: app},
   257  		&check{app: app},
   258  		&definition{app: app},
   259  		&foldingRanges{app: app},
   260  		&format{app: app},
   261  		&highlight{app: app},
   262  		&implementation{app: app},
   263  		&imports{app: app},
   264  		newRemote(app, ""),
   265  		newRemote(app, "inspect"),
   266  		&links{app: app},
   267  		&prepareRename{app: app},
   268  		&references{app: app},
   269  		&rename{app: app},
   270  		&semtok{app: app},
   271  		&signature{app: app},
   272  		&suggestedFix{app: app},
   273  		&symbols{app: app},
   274  		newWorkspace(app),
   275  		&workspaceSymbol{app: app},
   276  	}
   277  }
   278  
   279  var (
   280  	internalMu          sync.Mutex
   281  	internalConnections = make(map[string]*connection)
   282  )
   283  
   284  func (app *Application) connect(ctx context.Context) (*connection, error) {
   285  	switch {
   286  	case app.Remote == "":
   287  		connection := newConnection(app)
   288  		connection.Server = lsp.NewServer(cache.New(app.options).NewSession(ctx), connection.Client)
   289  		ctx = protocol.WithClient(ctx, connection.Client)
   290  		return connection, connection.initialize(ctx, app.options)
   291  	case strings.HasPrefix(app.Remote, "internal@"):
   292  		internalMu.Lock()
   293  		defer internalMu.Unlock()
   294  		opts := source.DefaultOptions().Clone()
   295  		if app.options != nil {
   296  			app.options(opts)
   297  		}
   298  		key := fmt.Sprintf("%s %v %v %v", app.wd, opts.PreferredContentFormat, opts.HierarchicalDocumentSymbolSupport, opts.SymbolMatcher)
   299  		if c := internalConnections[key]; c != nil {
   300  			return c, nil
   301  		}
   302  		remote := app.Remote[len("internal@"):]
   303  		ctx := xcontext.Detach(ctx) //TODO:a way of shutting down the internal server
   304  		connection, err := app.connectRemote(ctx, remote)
   305  		if err != nil {
   306  			return nil, err
   307  		}
   308  		internalConnections[key] = connection
   309  		return connection, nil
   310  	default:
   311  		return app.connectRemote(ctx, app.Remote)
   312  	}
   313  }
   314  
   315  // CloseTestConnections terminates shared connections used in command tests. It
   316  // should only be called from tests.
   317  func CloseTestConnections(ctx context.Context) {
   318  	for _, c := range internalConnections {
   319  		c.Shutdown(ctx)
   320  		c.Exit(ctx)
   321  	}
   322  }
   323  
   324  func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) {
   325  	connection := newConnection(app)
   326  	conn, err := lsprpc.ConnectToRemote(ctx, remote)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  	stream := jsonrpc2.NewHeaderStream(conn)
   331  	cc := jsonrpc2.NewConn(stream)
   332  	connection.Server = protocol.ServerDispatcher(cc)
   333  	ctx = protocol.WithClient(ctx, connection.Client)
   334  	cc.Go(ctx,
   335  		protocol.Handlers(
   336  			protocol.ClientHandler(connection.Client,
   337  				jsonrpc2.MethodNotFound)))
   338  	return connection, connection.initialize(ctx, app.options)
   339  }
   340  
   341  var matcherString = map[source.SymbolMatcher]string{
   342  	source.SymbolFuzzy:           "fuzzy",
   343  	source.SymbolCaseSensitive:   "caseSensitive",
   344  	source.SymbolCaseInsensitive: "caseInsensitive",
   345  }
   346  
   347  func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error {
   348  	params := &protocol.ParamInitialize{}
   349  	params.RootURI = protocol.URIFromPath(c.Client.app.wd)
   350  	params.Capabilities.Workspace.Configuration = true
   351  
   352  	// Make sure to respect configured options when sending initialize request.
   353  	opts := source.DefaultOptions().Clone()
   354  	if options != nil {
   355  		options(opts)
   356  	}
   357  	// If you add an additional option here, you must update the map key in connect.
   358  	params.Capabilities.TextDocument.Hover = protocol.HoverClientCapabilities{
   359  		ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat},
   360  	}
   361  	params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport
   362  	params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{}
   363  	params.Capabilities.TextDocument.SemanticTokens.Formats = []string{"relative"}
   364  	params.Capabilities.TextDocument.SemanticTokens.Requests.Range = true
   365  	params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true
   366  	params.Capabilities.TextDocument.SemanticTokens.TokenTypes = lsp.SemanticTypes()
   367  	params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = lsp.SemanticModifiers()
   368  	params.InitializationOptions = map[string]interface{}{
   369  		"symbolMatcher": matcherString[opts.SymbolMatcher],
   370  	}
   371  	if _, err := c.Server.Initialize(ctx, params); err != nil {
   372  		return err
   373  	}
   374  	if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
   375  		return err
   376  	}
   377  	return nil
   378  }
   379  
   380  type connection struct {
   381  	protocol.Server
   382  	Client *cmdClient
   383  }
   384  
   385  type cmdClient struct {
   386  	protocol.Server
   387  	app  *Application
   388  	fset *token.FileSet
   389  
   390  	diagnosticsMu   sync.Mutex
   391  	diagnosticsDone chan struct{}
   392  
   393  	filesMu sync.Mutex
   394  	files   map[span.URI]*cmdFile
   395  }
   396  
   397  type cmdFile struct {
   398  	uri         span.URI
   399  	mapper      *protocol.ColumnMapper
   400  	err         error
   401  	added       bool
   402  	diagnostics []protocol.Diagnostic
   403  }
   404  
   405  func newConnection(app *Application) *connection {
   406  	return &connection{
   407  		Client: &cmdClient{
   408  			app:   app,
   409  			fset:  token.NewFileSet(),
   410  			files: make(map[span.URI]*cmdFile),
   411  		},
   412  	}
   413  }
   414  
   415  // fileURI converts a DocumentURI to a file:// span.URI, panicking if it's not a file.
   416  func fileURI(uri protocol.DocumentURI) span.URI {
   417  	sURI := uri.SpanURI()
   418  	if !sURI.IsFile() {
   419  		panic(fmt.Sprintf("%q is not a file URI", uri))
   420  	}
   421  	return sURI
   422  }
   423  
   424  func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil }
   425  
   426  func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) {
   427  	return nil, nil
   428  }
   429  
   430  func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error {
   431  	switch p.Type {
   432  	case protocol.Error:
   433  		log.Print("Error:", p.Message)
   434  	case protocol.Warning:
   435  		log.Print("Warning:", p.Message)
   436  	case protocol.Info:
   437  		if c.app.verbose() {
   438  			log.Print("Info:", p.Message)
   439  		}
   440  	case protocol.Log:
   441  		if c.app.verbose() {
   442  			log.Print("Log:", p.Message)
   443  		}
   444  	default:
   445  		if c.app.verbose() {
   446  			log.Print(p.Message)
   447  		}
   448  	}
   449  	return nil
   450  }
   451  
   452  func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil }
   453  
   454  func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error {
   455  	return nil
   456  }
   457  
   458  func (c *cmdClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error {
   459  	return nil
   460  }
   461  
   462  func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) {
   463  	return nil, nil
   464  }
   465  
   466  func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) {
   467  	results := make([]interface{}, len(p.Items))
   468  	for i, item := range p.Items {
   469  		if item.Section != "gopls" {
   470  			continue
   471  		}
   472  		env := map[string]interface{}{}
   473  		for _, value := range c.app.env {
   474  			l := strings.SplitN(value, "=", 2)
   475  			if len(l) != 2 {
   476  				continue
   477  			}
   478  			env[l[0]] = l[1]
   479  		}
   480  		m := map[string]interface{}{
   481  			"env": env,
   482  			"analyses": map[string]bool{
   483  				"fillreturns":    true,
   484  				"nonewvars":      true,
   485  				"noresultvalues": true,
   486  				"undeclaredname": true,
   487  			},
   488  		}
   489  		if c.app.VeryVerbose {
   490  			m["verboseOutput"] = true
   491  		}
   492  		results[i] = m
   493  	}
   494  	return results, nil
   495  }
   496  
   497  func (c *cmdClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) {
   498  	return &protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: "not implemented"}, nil
   499  }
   500  
   501  func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error {
   502  	if p.URI == "gopls://diagnostics-done" {
   503  		close(c.diagnosticsDone)
   504  	}
   505  	// Don't worry about diagnostics without versions.
   506  	if p.Version == 0 {
   507  		return nil
   508  	}
   509  
   510  	c.filesMu.Lock()
   511  	defer c.filesMu.Unlock()
   512  
   513  	file := c.getFile(ctx, fileURI(p.URI))
   514  	file.diagnostics = p.Diagnostics
   515  	return nil
   516  }
   517  
   518  func (c *cmdClient) Progress(context.Context, *protocol.ProgressParams) error {
   519  	return nil
   520  }
   521  
   522  func (c *cmdClient) ShowDocument(context.Context, *protocol.ShowDocumentParams) (*protocol.ShowDocumentResult, error) {
   523  	return nil, nil
   524  }
   525  
   526  func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error {
   527  	return nil
   528  }
   529  
   530  func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile {
   531  	file, found := c.files[uri]
   532  	if !found || file.err != nil {
   533  		file = &cmdFile{
   534  			uri: uri,
   535  		}
   536  		c.files[uri] = file
   537  	}
   538  	if file.mapper == nil {
   539  		fname := uri.Filename()
   540  		content, err := ioutil.ReadFile(fname)
   541  		if err != nil {
   542  			file.err = errors.Errorf("getFile: %v: %v", uri, err)
   543  			return file
   544  		}
   545  		f := c.fset.AddFile(fname, -1, len(content))
   546  		f.SetLinesForContent(content)
   547  		converter := span.NewContentConverter(fname, content)
   548  		file.mapper = &protocol.ColumnMapper{
   549  			URI:       uri,
   550  			Converter: converter,
   551  			Content:   content,
   552  		}
   553  	}
   554  	return file
   555  }
   556  
   557  func (c *connection) AddFile(ctx context.Context, uri span.URI) *cmdFile {
   558  	c.Client.filesMu.Lock()
   559  	defer c.Client.filesMu.Unlock()
   560  
   561  	file := c.Client.getFile(ctx, uri)
   562  	// This should never happen.
   563  	if file == nil {
   564  		return &cmdFile{
   565  			uri: uri,
   566  			err: fmt.Errorf("no file found for %s", uri),
   567  		}
   568  	}
   569  	if file.err != nil || file.added {
   570  		return file
   571  	}
   572  	file.added = true
   573  	p := &protocol.DidOpenTextDocumentParams{
   574  		TextDocument: protocol.TextDocumentItem{
   575  			URI:        protocol.URIFromSpanURI(uri),
   576  			LanguageID: "go",
   577  			Version:    1,
   578  			Text:       string(file.mapper.Content),
   579  		},
   580  	}
   581  	if err := c.Server.DidOpen(ctx, p); err != nil {
   582  		file.err = errors.Errorf("%v: %v", uri, err)
   583  	}
   584  	return file
   585  }
   586  
   587  func (c *connection) semanticTokens(ctx context.Context, p *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) {
   588  	// use range to avoid limits on full
   589  	resp, err := c.Server.SemanticTokensRange(ctx, p)
   590  	if err != nil {
   591  		return nil, err
   592  	}
   593  	return resp, nil
   594  }
   595  
   596  func (c *connection) diagnoseFiles(ctx context.Context, files []span.URI) error {
   597  	var untypedFiles []interface{}
   598  	for _, file := range files {
   599  		untypedFiles = append(untypedFiles, string(file))
   600  	}
   601  	c.Client.diagnosticsMu.Lock()
   602  	defer c.Client.diagnosticsMu.Unlock()
   603  
   604  	c.Client.diagnosticsDone = make(chan struct{})
   605  	_, err := c.Server.NonstandardRequest(ctx, "gopls/diagnoseFiles", map[string]interface{}{"files": untypedFiles})
   606  	if err != nil {
   607  		close(c.Client.diagnosticsDone)
   608  		return err
   609  	}
   610  
   611  	<-c.Client.diagnosticsDone
   612  	return nil
   613  }
   614  
   615  func (c *connection) terminate(ctx context.Context) {
   616  	if strings.HasPrefix(c.Client.app.Remote, "internal@") {
   617  		// internal connections need to be left alive for the next test
   618  		return
   619  	}
   620  	//TODO: do we need to handle errors on these calls?
   621  	c.Shutdown(ctx)
   622  	//TODO: right now calling exit terminates the process, we should rethink that
   623  	//server.Exit(ctx)
   624  }
   625  
   626  // Implement io.Closer.
   627  func (c *cmdClient) Close() error {
   628  	return nil
   629  }