github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/cmd/snap/cmd_advise.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package main
    21  
    22  import (
    23  	"bufio"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io"
    27  	"net"
    28  	"os"
    29  	"sort"
    30  	"strconv"
    31  
    32  	"github.com/jessevdk/go-flags"
    33  
    34  	"github.com/snapcore/snapd/advisor"
    35  	"github.com/snapcore/snapd/i18n"
    36  	"github.com/snapcore/snapd/osutil"
    37  )
    38  
    39  type cmdAdviseSnap struct {
    40  	Positionals struct {
    41  		CommandOrPkg string
    42  	} `positional-args:"true"`
    43  
    44  	Format string `long:"format" default:"pretty" choice:"pretty" choice:"json"`
    45  	// Command makes advise try to find snaps that provide this command
    46  	Command bool `long:"command"`
    47  
    48  	// FromApt tells advise that it got started from an apt hook
    49  	// and needs to communicate over a socket
    50  	FromApt bool `long:"from-apt"`
    51  
    52  	// DumpDb dumps the whole advise database
    53  	DumpDb bool `long:"dump-db"`
    54  }
    55  
    56  var shortAdviseSnapHelp = i18n.G("Advise on available snaps")
    57  var longAdviseSnapHelp = i18n.G(`
    58  The advise-snap command searches for and suggests the installation of snaps.
    59  
    60  If --command is given, it suggests snaps that provide the given command.
    61  Otherwise it suggests snaps with the given name.
    62  `)
    63  
    64  func init() {
    65  	cmd := addCommand("advise-snap", shortAdviseSnapHelp, longAdviseSnapHelp, func() flags.Commander {
    66  		return &cmdAdviseSnap{}
    67  	}, map[string]string{
    68  		// TRANSLATORS: This should not start with a lowercase letter.
    69  		"command": i18n.G("Advise on snaps that provide the given command"),
    70  		// TRANSLATORS: This should not start with a lowercase letter.
    71  		"dump-db": i18n.G("Dump advise database for use by command-not-found."),
    72  		// TRANSLATORS: This should not start with a lowercase letter.
    73  		"from-apt": i18n.G("Run as an apt hook"),
    74  		// TRANSLATORS: This should not start with a lowercase letter.
    75  		"format": i18n.G("Use the given output format"),
    76  	}, []argDesc{
    77  		// TRANSLATORS: This needs to begin with < and end with >
    78  		{name: i18n.G("<command or pkg>")},
    79  	})
    80  	cmd.hidden = true
    81  }
    82  
    83  func outputAdviseExactText(command string, result []advisor.Command) error {
    84  	fmt.Fprintf(Stdout, "\n")
    85  	// TRANSLATORS: %q is a command name (like "gimp" or "loimpress")
    86  	fmt.Fprintf(Stdout, i18n.G("Command %q not found, but can be installed with:\n"), command)
    87  	fmt.Fprintf(Stdout, "\n")
    88  	for _, snap := range result {
    89  		fmt.Fprintf(Stdout, "sudo snap install %s\n", snap.Snap)
    90  	}
    91  	fmt.Fprintf(Stdout, "\n")
    92  	fmt.Fprintln(Stdout, i18n.G("See 'snap info <snap name>' for additional versions."))
    93  	fmt.Fprintf(Stdout, "\n")
    94  	return nil
    95  }
    96  
    97  func outputAdviseMisspellText(command string, result []advisor.Command) error {
    98  	fmt.Fprintf(Stdout, "\n")
    99  	fmt.Fprintf(Stdout, i18n.G("Command %q not found, did you mean:\n"), command)
   100  	fmt.Fprintf(Stdout, "\n")
   101  	for _, snap := range result {
   102  		fmt.Fprintf(Stdout, i18n.G(" command %q from snap %q\n"), snap.Command, snap.Snap)
   103  	}
   104  	fmt.Fprintf(Stdout, "\n")
   105  	fmt.Fprintln(Stdout, i18n.G("See 'snap info <snap name>' for additional versions."))
   106  	fmt.Fprintf(Stdout, "\n")
   107  	return nil
   108  }
   109  
   110  func outputAdviseJSON(command string, results []advisor.Command) error {
   111  	enc := json.NewEncoder(Stdout)
   112  	enc.Encode(results)
   113  	return nil
   114  }
   115  
   116  type jsonRPC struct {
   117  	JsonRPC string `json:"jsonrpc"`
   118  	Method  string `json:"method"`
   119  	Params  struct {
   120  		Command         string   `json:"command"`
   121  		UnknownPackages []string `json:"unknown-packages"`
   122  	}
   123  }
   124  
   125  // readRpc reads a apt json rpc protocol 0.1 message as described in
   126  // https://salsa.debian.org/apt-team/apt/blob/master/doc/json-hooks-protocol.md#wire-protocol
   127  func readRpc(r *bufio.Reader) (*jsonRPC, error) {
   128  	line, err := r.ReadBytes('\n')
   129  	if err != nil && err != io.EOF {
   130  		return nil, fmt.Errorf("cannot read json-rpc: %v", err)
   131  	}
   132  	if osutil.GetenvBool("SNAP_APT_HOOK_DEBUG") {
   133  		fmt.Fprintf(os.Stderr, "%s\n", line)
   134  	}
   135  
   136  	var rpc jsonRPC
   137  	if err := json.Unmarshal(line, &rpc); err != nil {
   138  		return nil, err
   139  	}
   140  	// empty \n
   141  	emptyNL, _, err := r.ReadLine()
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	if string(emptyNL) != "" {
   146  		return nil, fmt.Errorf("unexpected line: %q (empty)", emptyNL)
   147  	}
   148  
   149  	return &rpc, nil
   150  }
   151  
   152  func adviseViaAptHook() error {
   153  	sockFd := os.Getenv("APT_HOOK_SOCKET")
   154  	if sockFd == "" {
   155  		return fmt.Errorf("cannot find APT_HOOK_SOCKET env")
   156  	}
   157  	fd, err := strconv.Atoi(sockFd)
   158  	if err != nil {
   159  		return fmt.Errorf("expected APT_HOOK_SOCKET to be a decimal integer, found %q", sockFd)
   160  	}
   161  
   162  	f := os.NewFile(uintptr(fd), "apt-hook-socket")
   163  	if f == nil {
   164  		return fmt.Errorf("cannot open file descriptor %v", fd)
   165  	}
   166  	defer f.Close()
   167  
   168  	conn, err := net.FileConn(f)
   169  	if err != nil {
   170  		return fmt.Errorf("cannot connect to %v: %v", fd, err)
   171  	}
   172  	defer conn.Close()
   173  
   174  	r := bufio.NewReader(conn)
   175  
   176  	// handshake
   177  	rpc, err := readRpc(r)
   178  	if err != nil {
   179  		return err
   180  	}
   181  	if rpc.Method != "org.debian.apt.hooks.hello" {
   182  		return fmt.Errorf("expected 'hello' method, got: %v", rpc.Method)
   183  	}
   184  	if _, err := conn.Write([]byte(`{"jsonrpc":"2.0","id":0,"result":{"version":"0.1"}}` + "\n\n")); err != nil {
   185  		return err
   186  	}
   187  
   188  	// payload
   189  	rpc, err = readRpc(r)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	if rpc.Method == "org.debian.apt.hooks.install.fail" {
   194  		for _, pkgName := range rpc.Params.UnknownPackages {
   195  			match, err := advisor.FindPackage(pkgName)
   196  			if err == nil && match != nil {
   197  				fmt.Fprintf(Stdout, "\n")
   198  				fmt.Fprintf(Stdout, i18n.G("No apt package %q, but there is a snap with that name.\n"), pkgName)
   199  				fmt.Fprintf(Stdout, i18n.G("Try \"snap install %s\"\n"), pkgName)
   200  				fmt.Fprintf(Stdout, "\n")
   201  			}
   202  		}
   203  
   204  	}
   205  	// if rpc.Method == "org.debian.apt.hooks.search.post" {
   206  	// 	// FIXME: do a snap search here
   207  	// 	// FIXME2: figure out why apt does not tell us the search results
   208  	// }
   209  
   210  	// bye
   211  	rpc, err = readRpc(r)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	if rpc.Method != "org.debian.apt.hooks.bye" {
   216  		return fmt.Errorf("expected 'bye' method, got: %v", rpc.Method)
   217  	}
   218  
   219  	return nil
   220  }
   221  
   222  type Snap struct {
   223  	Snap    string
   224  	Version string
   225  	Command string
   226  }
   227  
   228  func dumpDbHook() error {
   229  	commands, err := advisor.DumpCommands()
   230  	if err != nil {
   231  		return err
   232  	}
   233  
   234  	commands_processed := make([]string, 0)
   235  	var b []Snap
   236  
   237  	var sortedCmds []string
   238  	for cmd := range commands {
   239  		sortedCmds = append(sortedCmds, cmd)
   240  	}
   241  	sort.Strings(sortedCmds)
   242  
   243  	for _, key := range sortedCmds {
   244  		value := commands[key]
   245  		err := json.Unmarshal([]byte(value), &b)
   246  		if err != nil {
   247  			return err
   248  		}
   249  		for i := range b {
   250  			var s = fmt.Sprintf("%s %s %s\n", key, b[i].Snap, b[i].Version)
   251  			commands_processed = append(commands_processed, s)
   252  		}
   253  	}
   254  
   255  	for _, value := range commands_processed {
   256  		fmt.Fprint(Stdout, value)
   257  	}
   258  
   259  	return nil
   260  }
   261  
   262  func (x *cmdAdviseSnap) Execute(args []string) error {
   263  	if len(args) > 0 {
   264  		return ErrExtraArgs
   265  	}
   266  
   267  	if x.DumpDb {
   268  		return dumpDbHook()
   269  	}
   270  
   271  	if x.FromApt {
   272  		return adviseViaAptHook()
   273  	}
   274  
   275  	if len(x.Positionals.CommandOrPkg) == 0 {
   276  		return fmt.Errorf("the required argument `<command or pkg>` was not provided")
   277  	}
   278  
   279  	if x.Command {
   280  		return adviseCommand(x.Positionals.CommandOrPkg, x.Format)
   281  	}
   282  
   283  	return advisePkg(x.Positionals.CommandOrPkg)
   284  }
   285  
   286  func advisePkg(pkgName string) error {
   287  	match, err := advisor.FindPackage(pkgName)
   288  	if err != nil {
   289  		return fmt.Errorf("advise for pkgname failed: %s", err)
   290  	}
   291  	if match != nil {
   292  		fmt.Fprintf(Stdout, i18n.G("Packages matching %q:\n"), pkgName)
   293  		fmt.Fprintf(Stdout, " * %s - %s\n", match.Snap, match.Summary)
   294  		fmt.Fprintf(Stdout, i18n.G("Try: snap install <selected snap>\n"))
   295  	}
   296  
   297  	// FIXME: find mispells
   298  
   299  	return nil
   300  }
   301  
   302  func adviseCommand(cmd string, format string) error {
   303  	// find exact matches
   304  	matches, err := advisor.FindCommand(cmd)
   305  	if err != nil {
   306  		return fmt.Errorf("advise for command failed: %s", err)
   307  	}
   308  	if len(matches) > 0 {
   309  		switch format {
   310  		case "json":
   311  			return outputAdviseJSON(cmd, matches)
   312  		case "pretty":
   313  			return outputAdviseExactText(cmd, matches)
   314  		default:
   315  			return fmt.Errorf("unsupported format %q", format)
   316  		}
   317  	}
   318  
   319  	// find misspellings
   320  	matches, err = advisor.FindMisspelledCommand(cmd)
   321  	if err != nil {
   322  		return err
   323  	}
   324  	if len(matches) > 0 {
   325  		switch format {
   326  		case "json":
   327  			return outputAdviseJSON(cmd, matches)
   328  		case "pretty":
   329  			return outputAdviseMisspellText(cmd, matches)
   330  		default:
   331  			return fmt.Errorf("unsupported format %q", format)
   332  		}
   333  	}
   334  
   335  	return fmt.Errorf("%s: command not found", cmd)
   336  }