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 }