github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/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/powerman/golang-tools/internal/jsonrpc2"
    26  	"github.com/powerman/golang-tools/internal/lsp"
    27  	"github.com/powerman/golang-tools/internal/lsp/cache"
    28  	"github.com/powerman/golang-tools/internal/lsp/debug"
    29  	"github.com/powerman/golang-tools/internal/lsp/lsprpc"
    30  	"github.com/powerman/golang-tools/internal/lsp/protocol"
    31  	"github.com/powerman/golang-tools/internal/lsp/source"
    32  	"github.com/powerman/golang-tools/internal/span"
    33  	"github.com/powerman/golang-tools/internal/tool"
    34  	"github.com/powerman/golang-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  		&vulncheck{app: app},
   277  	}
   278  }
   279  
   280  var (
   281  	internalMu          sync.Mutex
   282  	internalConnections = make(map[string]*connection)
   283  )
   284  
   285  func (app *Application) connect(ctx context.Context) (*connection, error) {
   286  	switch {
   287  	case app.Remote == "":
   288  		connection := newConnection(app)
   289  		connection.Server = lsp.NewServer(cache.New(app.options).NewSession(ctx), connection.Client)
   290  		ctx = protocol.WithClient(ctx, connection.Client)
   291  		return connection, connection.initialize(ctx, app.options)
   292  	case strings.HasPrefix(app.Remote, "internal@"):
   293  		internalMu.Lock()
   294  		defer internalMu.Unlock()
   295  		opts := source.DefaultOptions().Clone()
   296  		if app.options != nil {
   297  			app.options(opts)
   298  		}
   299  		key := fmt.Sprintf("%s %v %v %v", app.wd, opts.PreferredContentFormat, opts.HierarchicalDocumentSymbolSupport, opts.SymbolMatcher)
   300  		if c := internalConnections[key]; c != nil {
   301  			return c, nil
   302  		}
   303  		remote := app.Remote[len("internal@"):]
   304  		ctx := xcontext.Detach(ctx) //TODO:a way of shutting down the internal server
   305  		connection, err := app.connectRemote(ctx, remote)
   306  		if err != nil {
   307  			return nil, err
   308  		}
   309  		internalConnections[key] = connection
   310  		return connection, nil
   311  	default:
   312  		return app.connectRemote(ctx, app.Remote)
   313  	}
   314  }
   315  
   316  // CloseTestConnections terminates shared connections used in command tests. It
   317  // should only be called from tests.
   318  func CloseTestConnections(ctx context.Context) {
   319  	for _, c := range internalConnections {
   320  		c.Shutdown(ctx)
   321  		c.Exit(ctx)
   322  	}
   323  }
   324  
   325  func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) {
   326  	connection := newConnection(app)
   327  	conn, err := lsprpc.ConnectToRemote(ctx, remote)
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  	stream := jsonrpc2.NewHeaderStream(conn)
   332  	cc := jsonrpc2.NewConn(stream)
   333  	connection.Server = protocol.ServerDispatcher(cc)
   334  	ctx = protocol.WithClient(ctx, connection.Client)
   335  	cc.Go(ctx,
   336  		protocol.Handlers(
   337  			protocol.ClientHandler(connection.Client,
   338  				jsonrpc2.MethodNotFound)))
   339  	return connection, connection.initialize(ctx, app.options)
   340  }
   341  
   342  var matcherString = map[source.SymbolMatcher]string{
   343  	source.SymbolFuzzy:           "fuzzy",
   344  	source.SymbolCaseSensitive:   "caseSensitive",
   345  	source.SymbolCaseInsensitive: "caseInsensitive",
   346  }
   347  
   348  func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error {
   349  	params := &protocol.ParamInitialize{}
   350  	params.RootURI = protocol.URIFromPath(c.Client.app.wd)
   351  	params.Capabilities.Workspace.Configuration = true
   352  
   353  	// Make sure to respect configured options when sending initialize request.
   354  	opts := source.DefaultOptions().Clone()
   355  	if options != nil {
   356  		options(opts)
   357  	}
   358  	// If you add an additional option here, you must update the map key in connect.
   359  	params.Capabilities.TextDocument.Hover = protocol.HoverClientCapabilities{
   360  		ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat},
   361  	}
   362  	params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport
   363  	params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{}
   364  	params.Capabilities.TextDocument.SemanticTokens.Formats = []string{"relative"}
   365  	params.Capabilities.TextDocument.SemanticTokens.Requests.Range = true
   366  	params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true
   367  	params.Capabilities.TextDocument.SemanticTokens.TokenTypes = lsp.SemanticTypes()
   368  	params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = lsp.SemanticModifiers()
   369  	params.InitializationOptions = map[string]interface{}{
   370  		"symbolMatcher": matcherString[opts.SymbolMatcher],
   371  	}
   372  	if _, err := c.Server.Initialize(ctx, params); err != nil {
   373  		return err
   374  	}
   375  	if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
   376  		return err
   377  	}
   378  	return nil
   379  }
   380  
   381  type connection struct {
   382  	protocol.Server
   383  	Client *cmdClient
   384  }
   385  
   386  type cmdClient struct {
   387  	protocol.Server
   388  	app  *Application
   389  	fset *token.FileSet
   390  
   391  	diagnosticsMu   sync.Mutex
   392  	diagnosticsDone chan struct{}
   393  
   394  	filesMu sync.Mutex
   395  	files   map[span.URI]*cmdFile
   396  }
   397  
   398  type cmdFile struct {
   399  	uri         span.URI
   400  	mapper      *protocol.ColumnMapper
   401  	err         error
   402  	added       bool
   403  	diagnostics []protocol.Diagnostic
   404  }
   405  
   406  func newConnection(app *Application) *connection {
   407  	return &connection{
   408  		Client: &cmdClient{
   409  			app:   app,
   410  			fset:  token.NewFileSet(),
   411  			files: make(map[span.URI]*cmdFile),
   412  		},
   413  	}
   414  }
   415  
   416  // fileURI converts a DocumentURI to a file:// span.URI, panicking if it's not a file.
   417  func fileURI(uri protocol.DocumentURI) span.URI {
   418  	sURI := uri.SpanURI()
   419  	if !sURI.IsFile() {
   420  		panic(fmt.Sprintf("%q is not a file URI", uri))
   421  	}
   422  	return sURI
   423  }
   424  
   425  func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil }
   426  
   427  func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) {
   428  	return nil, nil
   429  }
   430  
   431  func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error {
   432  	switch p.Type {
   433  	case protocol.Error:
   434  		log.Print("Error:", p.Message)
   435  	case protocol.Warning:
   436  		log.Print("Warning:", p.Message)
   437  	case protocol.Info:
   438  		if c.app.verbose() {
   439  			log.Print("Info:", p.Message)
   440  		}
   441  	case protocol.Log:
   442  		if c.app.verbose() {
   443  			log.Print("Log:", p.Message)
   444  		}
   445  	default:
   446  		if c.app.verbose() {
   447  			log.Print(p.Message)
   448  		}
   449  	}
   450  	return nil
   451  }
   452  
   453  func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil }
   454  
   455  func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error {
   456  	return nil
   457  }
   458  
   459  func (c *cmdClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error {
   460  	return nil
   461  }
   462  
   463  func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) {
   464  	return nil, nil
   465  }
   466  
   467  func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) {
   468  	results := make([]interface{}, len(p.Items))
   469  	for i, item := range p.Items {
   470  		if item.Section != "gopls" {
   471  			continue
   472  		}
   473  		env := map[string]interface{}{}
   474  		for _, value := range c.app.env {
   475  			l := strings.SplitN(value, "=", 2)
   476  			if len(l) != 2 {
   477  				continue
   478  			}
   479  			env[l[0]] = l[1]
   480  		}
   481  		m := map[string]interface{}{
   482  			"env": env,
   483  			"analyses": map[string]bool{
   484  				"fillreturns":    true,
   485  				"nonewvars":      true,
   486  				"noresultvalues": true,
   487  				"undeclaredname": true,
   488  			},
   489  		}
   490  		if c.app.VeryVerbose {
   491  			m["verboseOutput"] = true
   492  		}
   493  		results[i] = m
   494  	}
   495  	return results, nil
   496  }
   497  
   498  func (c *cmdClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) {
   499  	return &protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: "not implemented"}, nil
   500  }
   501  
   502  func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error {
   503  	if p.URI == "gopls://diagnostics-done" {
   504  		close(c.diagnosticsDone)
   505  	}
   506  	// Don't worry about diagnostics without versions.
   507  	if p.Version == 0 {
   508  		return nil
   509  	}
   510  
   511  	c.filesMu.Lock()
   512  	defer c.filesMu.Unlock()
   513  
   514  	file := c.getFile(ctx, fileURI(p.URI))
   515  	file.diagnostics = p.Diagnostics
   516  	return nil
   517  }
   518  
   519  func (c *cmdClient) Progress(context.Context, *protocol.ProgressParams) error {
   520  	return nil
   521  }
   522  
   523  func (c *cmdClient) ShowDocument(context.Context, *protocol.ShowDocumentParams) (*protocol.ShowDocumentResult, error) {
   524  	return nil, nil
   525  }
   526  
   527  func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error {
   528  	return nil
   529  }
   530  
   531  func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile {
   532  	file, found := c.files[uri]
   533  	if !found || file.err != nil {
   534  		file = &cmdFile{
   535  			uri: uri,
   536  		}
   537  		c.files[uri] = file
   538  	}
   539  	if file.mapper == nil {
   540  		fname := uri.Filename()
   541  		content, err := ioutil.ReadFile(fname)
   542  		if err != nil {
   543  			file.err = errors.Errorf("getFile: %v: %v", uri, err)
   544  			return file
   545  		}
   546  		f := c.fset.AddFile(fname, -1, len(content))
   547  		f.SetLinesForContent(content)
   548  		converter := span.NewContentConverter(fname, content)
   549  		file.mapper = &protocol.ColumnMapper{
   550  			URI:       uri,
   551  			Converter: converter,
   552  			Content:   content,
   553  		}
   554  	}
   555  	return file
   556  }
   557  
   558  func (c *connection) AddFile(ctx context.Context, uri span.URI) *cmdFile {
   559  	c.Client.filesMu.Lock()
   560  	defer c.Client.filesMu.Unlock()
   561  
   562  	file := c.Client.getFile(ctx, uri)
   563  	// This should never happen.
   564  	if file == nil {
   565  		return &cmdFile{
   566  			uri: uri,
   567  			err: fmt.Errorf("no file found for %s", uri),
   568  		}
   569  	}
   570  	if file.err != nil || file.added {
   571  		return file
   572  	}
   573  	file.added = true
   574  	p := &protocol.DidOpenTextDocumentParams{
   575  		TextDocument: protocol.TextDocumentItem{
   576  			URI:        protocol.URIFromSpanURI(uri),
   577  			LanguageID: "go",
   578  			Version:    1,
   579  			Text:       string(file.mapper.Content),
   580  		},
   581  	}
   582  	if err := c.Server.DidOpen(ctx, p); err != nil {
   583  		file.err = errors.Errorf("%v: %v", uri, err)
   584  	}
   585  	return file
   586  }
   587  
   588  func (c *connection) semanticTokens(ctx context.Context, p *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) {
   589  	// use range to avoid limits on full
   590  	resp, err := c.Server.SemanticTokensRange(ctx, p)
   591  	if err != nil {
   592  		return nil, err
   593  	}
   594  	return resp, nil
   595  }
   596  
   597  func (c *connection) diagnoseFiles(ctx context.Context, files []span.URI) error {
   598  	var untypedFiles []interface{}
   599  	for _, file := range files {
   600  		untypedFiles = append(untypedFiles, string(file))
   601  	}
   602  	c.Client.diagnosticsMu.Lock()
   603  	defer c.Client.diagnosticsMu.Unlock()
   604  
   605  	c.Client.diagnosticsDone = make(chan struct{})
   606  	_, err := c.Server.NonstandardRequest(ctx, "gopls/diagnoseFiles", map[string]interface{}{"files": untypedFiles})
   607  	if err != nil {
   608  		close(c.Client.diagnosticsDone)
   609  		return err
   610  	}
   611  
   612  	<-c.Client.diagnosticsDone
   613  	return nil
   614  }
   615  
   616  func (c *connection) terminate(ctx context.Context) {
   617  	if strings.HasPrefix(c.Client.app.Remote, "internal@") {
   618  		// internal connections need to be left alive for the next test
   619  		return
   620  	}
   621  	//TODO: do we need to handle errors on these calls?
   622  	c.Shutdown(ctx)
   623  	//TODO: right now calling exit terminates the process, we should rethink that
   624  	//server.Exit(ctx)
   625  }
   626  
   627  // Implement io.Closer.
   628  func (c *cmdClient) Close() error {
   629  	return nil
   630  }