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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-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  	"fmt"
    24  	"io"
    25  	"path/filepath"
    26  	"strconv"
    27  	"strings"
    28  	"text/tabwriter"
    29  	"time"
    30  	"unicode"
    31  	"unicode/utf8"
    32  
    33  	"github.com/jessevdk/go-flags"
    34  	"gopkg.in/yaml.v2"
    35  
    36  	"github.com/snapcore/snapd/asserts"
    37  	"github.com/snapcore/snapd/client"
    38  	"github.com/snapcore/snapd/client/clientutil"
    39  	"github.com/snapcore/snapd/i18n"
    40  	"github.com/snapcore/snapd/osutil"
    41  	"github.com/snapcore/snapd/snap"
    42  	"github.com/snapcore/snapd/snap/snapfile"
    43  	"github.com/snapcore/snapd/snap/squashfs"
    44  	"github.com/snapcore/snapd/strutil"
    45  )
    46  
    47  type infoCmd struct {
    48  	clientMixin
    49  	colorMixin
    50  	timeMixin
    51  
    52  	Verbose    bool `long:"verbose"`
    53  	Positional struct {
    54  		Snaps []anySnapName `positional-arg-name:"<snap>" required:"1"`
    55  	} `positional-args:"yes" required:"yes"`
    56  }
    57  
    58  var shortInfoHelp = i18n.G("Show detailed information about snaps")
    59  var longInfoHelp = i18n.G(`
    60  The info command shows detailed information about snaps.
    61  
    62  The snaps can be specified by name or by path; names are looked for both in the
    63  store and in the installed snaps; paths can refer to a .snap file, or to a
    64  directory that contains an unpacked snap suitable for 'snap try' (an example
    65  of this would be the 'prime' directory snapcraft produces).
    66  `)
    67  
    68  func init() {
    69  	addCommand("info",
    70  		shortInfoHelp,
    71  		longInfoHelp,
    72  		func() flags.Commander {
    73  			return &infoCmd{}
    74  		}, colorDescs.also(timeDescs).also(map[string]string{
    75  			// TRANSLATORS: This should not start with a lowercase letter.
    76  			"verbose": i18n.G("Include more details on the snap (expanded notes, base, etc.)"),
    77  		}), nil)
    78  }
    79  
    80  func (iw *infoWriter) maybePrintHealth() {
    81  	if iw.localSnap == nil {
    82  		return
    83  	}
    84  	health := iw.localSnap.Health
    85  	if health == nil {
    86  		if !iw.verbose {
    87  			return
    88  		}
    89  		health = &client.SnapHealth{
    90  			Status:  "unknown",
    91  			Message: "health has not been set",
    92  		}
    93  	}
    94  	if health.Status == "okay" && !iw.verbose {
    95  		return
    96  	}
    97  
    98  	fmt.Fprintln(iw, "health:")
    99  	fmt.Fprintf(iw, "  status:\t%s\n", health.Status)
   100  	if health.Message != "" {
   101  		wrapGeneric(iw, quotedIfNeeded(health.Message), "  message:\t", "    ", iw.termWidth)
   102  	}
   103  	if health.Code != "" {
   104  		fmt.Fprintf(iw, "  code:\t%s\n", health.Code)
   105  	}
   106  	if !health.Timestamp.IsZero() {
   107  		fmt.Fprintf(iw, "  checked:\t%s\n", iw.fmtTime(health.Timestamp))
   108  	}
   109  	if !health.Revision.Unset() {
   110  		fmt.Fprintf(iw, "  revision:\t%s\n", health.Revision)
   111  	}
   112  	iw.Flush()
   113  }
   114  
   115  func clientSnapFromPath(path string) (*client.Snap, error) {
   116  	snapf, err := snapfile.Open(path)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	info, err := snap.ReadInfoFromSnapFile(snapf, nil)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	direct, err := clientutil.ClientSnapFromSnapInfo(info, nil)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	return direct, nil
   131  }
   132  
   133  func norm(path string) string {
   134  	path = filepath.Clean(path)
   135  	if osutil.IsDirectory(path) {
   136  		path = path + "/"
   137  	}
   138  
   139  	return path
   140  }
   141  
   142  // runesTrimRightSpace returns text, with any trailing whitespace dropped.
   143  func runesTrimRightSpace(text []rune) []rune {
   144  	j := len(text)
   145  	for j > 0 && unicode.IsSpace(text[j-1]) {
   146  		j--
   147  	}
   148  	return text[:j]
   149  }
   150  
   151  // runesLastIndexSpace returns the index of the last whitespace rune
   152  // in the text. If the text has no whitespace, returns -1.
   153  func runesLastIndexSpace(text []rune) int {
   154  	for i := len(text) - 1; i >= 0; i-- {
   155  		if unicode.IsSpace(text[i]) {
   156  			return i
   157  		}
   158  	}
   159  	return -1
   160  }
   161  
   162  // wrapLine wraps a line, assumed to be part of a block-style yaml
   163  // string, to fit into termWidth, preserving the line's indent, and
   164  // writes it out prepending padding to each line.
   165  func wrapLine(out io.Writer, text []rune, pad string, termWidth int) error {
   166  	// discard any trailing whitespace
   167  	text = runesTrimRightSpace(text)
   168  	// establish the indent of the whole block
   169  	idx := 0
   170  	for idx < len(text) && unicode.IsSpace(text[idx]) {
   171  		idx++
   172  	}
   173  	indent := pad + string(text[:idx])
   174  	text = text[idx:]
   175  	if len(indent) > termWidth/2 {
   176  		// If indent is too big there's not enough space for the actual
   177  		// text, in the pathological case the indent can even be bigger
   178  		// than the terminal which leads to lp:1828425.
   179  		// Rather than let that happen, give up.
   180  		indent = pad + "  "
   181  	}
   182  	return wrapGeneric(out, text, indent, indent, termWidth)
   183  }
   184  
   185  // wrapFlow wraps the text using yaml's flow style, allowing indent
   186  // characters for the first line.
   187  func wrapFlow(out io.Writer, text []rune, indent string, termWidth int) error {
   188  	return wrapGeneric(out, text, indent, "  ", termWidth)
   189  }
   190  
   191  // wrapGeneric wraps the given text to the given width, prefixing the
   192  // first line with indent and the remaining lines with indent2
   193  func wrapGeneric(out io.Writer, text []rune, indent, indent2 string, termWidth int) error {
   194  	// Note: this is _wrong_ for much of unicode (because the width of a rune on
   195  	//       the terminal is anything between 0 and 2, not always 1 as this code
   196  	//       assumes) but fixing that is Hard. Long story short, you can get close
   197  	//       using a couple of big unicode tables (which is what wcwidth
   198  	//       does). Getting it 100% requires a terminfo-alike of unicode behaviour.
   199  	//       However, before this we'd count bytes instead of runes, so we'd be
   200  	//       even more broken. Think of it as successive approximations... at least
   201  	//       with this work we share tabwriter's opinion on the width of things!
   202  
   203  	// This (and possibly printDescr below) should move to strutil once
   204  	// we're happy with it getting wider (heh heh) use.
   205  
   206  	indentWidth := utf8.RuneCountInString(indent)
   207  	delta := indentWidth - utf8.RuneCountInString(indent2)
   208  	width := termWidth - indentWidth
   209  
   210  	// establish the indent of the whole block
   211  	var err error
   212  	for len(text) > width && err == nil {
   213  		// find a good place to chop the text
   214  		idx := runesLastIndexSpace(text[:width+1])
   215  		if idx < 0 {
   216  			// there's no whitespace; just chop at line width
   217  			idx = width
   218  		}
   219  		_, err = fmt.Fprint(out, indent, string(text[:idx]), "\n")
   220  		// prune any remaining whitespace before the start of the next line
   221  		for idx < len(text) && unicode.IsSpace(text[idx]) {
   222  			idx++
   223  		}
   224  		text = text[idx:]
   225  		width += delta
   226  		indent = indent2
   227  		delta = 0
   228  	}
   229  	if err != nil {
   230  		return err
   231  	}
   232  	_, err = fmt.Fprint(out, indent, string(text), "\n")
   233  	return err
   234  }
   235  
   236  func quotedIfNeeded(raw string) []rune {
   237  	// simplest way of checking to see if it needs quoting is to try
   238  	raw = strings.TrimSpace(raw)
   239  	type T struct {
   240  		S string
   241  	}
   242  	if len(raw) == 0 {
   243  		raw = `""`
   244  	} else if err := yaml.UnmarshalStrict([]byte("s: "+raw), &T{}); err != nil {
   245  		raw = strconv.Quote(raw)
   246  	}
   247  	return []rune(raw)
   248  }
   249  
   250  // printDescr formats a given string (typically a snap description)
   251  // in a user friendly way.
   252  //
   253  // The rules are (intentionally) very simple:
   254  // - trim trailing whitespace
   255  // - word wrap at "max" chars preserving line indent
   256  // - keep \n intact and break there
   257  func printDescr(w io.Writer, descr string, termWidth int) error {
   258  	var err error
   259  	descr = strings.TrimRightFunc(descr, unicode.IsSpace)
   260  	for _, line := range strings.Split(descr, "\n") {
   261  		err = wrapLine(w, []rune(line), "  ", termWidth)
   262  		if err != nil {
   263  			break
   264  		}
   265  	}
   266  	return err
   267  }
   268  
   269  type writeflusher interface {
   270  	io.Writer
   271  	Flush() error
   272  }
   273  
   274  type infoWriter struct {
   275  	// fields that are set every iteration
   276  	theSnap    *client.Snap
   277  	diskSnap   *client.Snap
   278  	localSnap  *client.Snap
   279  	remoteSnap *client.Snap
   280  	resInfo    *client.ResultInfo
   281  	path       string
   282  	// fields that don't change and so can be set once
   283  	writeflusher
   284  	esc       *escapes
   285  	termWidth int
   286  	fmtTime   func(time.Time) string
   287  	absTime   bool
   288  	verbose   bool
   289  }
   290  
   291  func (iw *infoWriter) setupDiskSnap(path string, diskSnap *client.Snap) {
   292  	iw.localSnap, iw.remoteSnap, iw.resInfo = nil, nil, nil
   293  	iw.path = path
   294  	iw.diskSnap = diskSnap
   295  	iw.theSnap = diskSnap
   296  }
   297  
   298  func (iw *infoWriter) setupSnap(localSnap, remoteSnap *client.Snap, resInfo *client.ResultInfo) {
   299  	iw.path, iw.diskSnap = "", nil
   300  	iw.localSnap = localSnap
   301  	iw.remoteSnap = remoteSnap
   302  	iw.resInfo = resInfo
   303  	if localSnap != nil {
   304  		iw.theSnap = localSnap
   305  	} else {
   306  		iw.theSnap = remoteSnap
   307  	}
   308  }
   309  
   310  func (iw *infoWriter) maybePrintPrice() {
   311  	if iw.resInfo == nil {
   312  		return
   313  	}
   314  	price, currency, err := getPrice(iw.remoteSnap.Prices, iw.resInfo.SuggestedCurrency)
   315  	if err != nil {
   316  		return
   317  	}
   318  	fmt.Fprintf(iw, "price:\t%s\n", formatPrice(price, currency))
   319  }
   320  
   321  func (iw *infoWriter) maybePrintType() {
   322  	// XXX: using literals here until we reshuffle snap & client properly
   323  	// (and os->core rename happens, etc)
   324  	t := iw.theSnap.Type
   325  	switch t {
   326  	case "", "app", "application":
   327  		return
   328  	case "os":
   329  		t = "core"
   330  	}
   331  
   332  	fmt.Fprintf(iw, "type:\t%s\n", t)
   333  }
   334  
   335  func (iw *infoWriter) maybePrintID() {
   336  	if iw.theSnap.ID != "" {
   337  		fmt.Fprintf(iw, "snap-id:\t%s\n", iw.theSnap.ID)
   338  	}
   339  }
   340  
   341  func (iw *infoWriter) maybePrintTrackingChannel() {
   342  	if iw.localSnap == nil {
   343  		return
   344  	}
   345  	if iw.localSnap.TrackingChannel == "" {
   346  		return
   347  	}
   348  	fmt.Fprintf(iw, "tracking:\t%s\n", iw.localSnap.TrackingChannel)
   349  }
   350  
   351  func (iw *infoWriter) maybePrintInstallDate() {
   352  	if iw.localSnap == nil {
   353  		return
   354  	}
   355  	if iw.localSnap.InstallDate.IsZero() {
   356  		return
   357  	}
   358  	fmt.Fprintf(iw, "refresh-date:\t%s\n", iw.fmtTime(iw.localSnap.InstallDate))
   359  }
   360  
   361  func (iw *infoWriter) maybePrintChinfo() {
   362  	if iw.diskSnap != nil {
   363  		return
   364  	}
   365  	chInfos := channelInfos{
   366  		chantpl:     "%s%s:\t%s %s%*s %*s %s\n",
   367  		releasedfmt: "2006-01-02",
   368  		esc:         iw.esc,
   369  	}
   370  	if iw.absTime {
   371  		chInfos.releasedfmt = time.RFC3339
   372  	}
   373  	if iw.remoteSnap != nil && iw.remoteSnap.Channels != nil && iw.remoteSnap.Tracks != nil {
   374  		iw.Flush()
   375  		chInfos.chantpl = "%s%s:\t%s\t%s\t%*s\t%*s\t%s\n"
   376  		chInfos.addFromRemote(iw.remoteSnap)
   377  	}
   378  	if iw.localSnap != nil {
   379  		chInfos.addFromLocal(iw.localSnap)
   380  	}
   381  	chInfos.dump(iw)
   382  }
   383  
   384  func (iw *infoWriter) maybePrintBase() {
   385  	if iw.verbose && iw.theSnap.Base != "" {
   386  		fmt.Fprintf(iw, "base:\t%s\n", iw.theSnap.Base)
   387  	}
   388  }
   389  
   390  func (iw *infoWriter) maybePrintPath() {
   391  	if iw.path != "" {
   392  		fmt.Fprintf(iw, "path:\t%q\n", iw.path)
   393  	}
   394  }
   395  
   396  func (iw *infoWriter) printName() {
   397  	fmt.Fprintf(iw, "name:\t%s\n", iw.theSnap.Name)
   398  }
   399  
   400  func (iw *infoWriter) printSummary() {
   401  	wrapFlow(iw, quotedIfNeeded(iw.theSnap.Summary), "summary:\t", iw.termWidth)
   402  }
   403  
   404  func (iw *infoWriter) maybePrintStoreURL() {
   405  	storeURL := ""
   406  	// XXX: store-url for local snaps comes from aux data, but that gets
   407  	// updated only when the snap is refreshed, be smart and poke remote
   408  	// snap info if available
   409  	switch {
   410  	case iw.theSnap.StoreURL != "":
   411  		storeURL = iw.theSnap.StoreURL
   412  	case iw.remoteSnap != nil && iw.remoteSnap.StoreURL != "":
   413  		storeURL = iw.remoteSnap.StoreURL
   414  	}
   415  	if storeURL == "" {
   416  		return
   417  	}
   418  	fmt.Fprintf(iw, "store-url:\t%s\n", storeURL)
   419  }
   420  
   421  func (iw *infoWriter) maybePrintPublisher() {
   422  	if iw.diskSnap != nil {
   423  		// snaps read from disk won't have a publisher
   424  		return
   425  	}
   426  	fmt.Fprintf(iw, "publisher:\t%s\n", longPublisher(iw.esc, iw.theSnap.Publisher))
   427  }
   428  
   429  func (iw *infoWriter) maybePrintStandaloneVersion() {
   430  	if iw.diskSnap == nil {
   431  		// snaps not read from disk will have version information shown elsewhere
   432  		return
   433  	}
   434  	version := iw.diskSnap.Version
   435  	if version == "" {
   436  		version = iw.esc.dash
   437  	}
   438  	// NotesFromRemote might be better called NotesFromNotInstalled but that's nasty
   439  	fmt.Fprintf(iw, "version:\t%s %s\n", version, NotesFromRemote(iw.diskSnap, nil))
   440  }
   441  
   442  func (iw *infoWriter) maybePrintBuildDate() {
   443  	if iw.diskSnap == nil {
   444  		return
   445  	}
   446  	if osutil.IsDirectory(iw.path) {
   447  		return
   448  	}
   449  	buildDate := squashfs.BuildDate(iw.path)
   450  	if buildDate.IsZero() {
   451  		return
   452  	}
   453  	fmt.Fprintf(iw, "build-date:\t%s\n", iw.fmtTime(buildDate))
   454  }
   455  
   456  func (iw *infoWriter) maybePrintContact() error {
   457  	contact := strings.TrimPrefix(iw.theSnap.Contact, "mailto:")
   458  	if contact == "" {
   459  		return nil
   460  	}
   461  	_, err := fmt.Fprintf(iw, "contact:\t%s\n", contact)
   462  	return err
   463  }
   464  
   465  func (iw *infoWriter) printLicense() {
   466  	license := iw.theSnap.License
   467  	if license == "" {
   468  		license = "unset"
   469  	}
   470  	fmt.Fprintf(iw, "license:\t%s\n", license)
   471  }
   472  
   473  func (iw *infoWriter) printDescr() {
   474  	fmt.Fprintln(iw, "description: |")
   475  	printDescr(iw, iw.theSnap.Description, iw.termWidth)
   476  }
   477  
   478  func (iw *infoWriter) maybePrintCommands() {
   479  	if len(iw.theSnap.Apps) == 0 {
   480  		return
   481  	}
   482  
   483  	commands := make([]string, 0, len(iw.theSnap.Apps))
   484  	for _, app := range iw.theSnap.Apps {
   485  		if app.IsService() {
   486  			continue
   487  		}
   488  
   489  		cmdStr := snap.JoinSnapApp(iw.theSnap.Name, app.Name)
   490  		commands = append(commands, cmdStr)
   491  	}
   492  	if len(commands) == 0 {
   493  		return
   494  	}
   495  
   496  	fmt.Fprintf(iw, "commands:\n")
   497  	for _, cmd := range commands {
   498  		fmt.Fprintf(iw, "  - %s\n", cmd)
   499  	}
   500  }
   501  
   502  func (iw *infoWriter) maybePrintServices() {
   503  	if len(iw.theSnap.Apps) == 0 {
   504  		return
   505  	}
   506  
   507  	services := make([]string, 0, len(iw.theSnap.Apps))
   508  	for _, app := range iw.theSnap.Apps {
   509  		if !app.IsService() {
   510  			continue
   511  		}
   512  
   513  		var active, enabled string
   514  		if app.Active {
   515  			active = "active"
   516  		} else {
   517  			active = "inactive"
   518  		}
   519  		if app.Enabled {
   520  			enabled = "enabled"
   521  		} else {
   522  			enabled = "disabled"
   523  		}
   524  		services = append(services, fmt.Sprintf("  %s:\t%s, %s, %s", snap.JoinSnapApp(iw.theSnap.Name, app.Name), app.Daemon, enabled, active))
   525  	}
   526  	if len(services) == 0 {
   527  		return
   528  	}
   529  
   530  	fmt.Fprintf(iw, "services:\n")
   531  	for _, svc := range services {
   532  		fmt.Fprintln(iw, svc)
   533  	}
   534  }
   535  
   536  func (iw *infoWriter) maybePrintNotes() {
   537  	if !iw.verbose {
   538  		return
   539  	}
   540  	fmt.Fprintln(iw, "notes:\t")
   541  	fmt.Fprintf(iw, "  private:\t%t\n", iw.theSnap.Private)
   542  	fmt.Fprintf(iw, "  confinement:\t%s\n", iw.theSnap.Confinement)
   543  	if iw.localSnap == nil {
   544  		return
   545  	}
   546  	jailMode := iw.localSnap.Confinement == client.DevModeConfinement && !iw.localSnap.DevMode
   547  	fmt.Fprintf(iw, "  devmode:\t%t\n", iw.localSnap.DevMode)
   548  	fmt.Fprintf(iw, "  jailmode:\t%t\n", jailMode)
   549  	fmt.Fprintf(iw, "  trymode:\t%t\n", iw.localSnap.TryMode)
   550  	fmt.Fprintf(iw, "  enabled:\t%t\n", iw.localSnap.Status == client.StatusActive)
   551  	if iw.localSnap.Broken == "" {
   552  		fmt.Fprintf(iw, "  broken:\t%t\n", false)
   553  	} else {
   554  		fmt.Fprintf(iw, "  broken:\t%t (%s)\n", true, iw.localSnap.Broken)
   555  	}
   556  
   557  	fmt.Fprintf(iw, "  ignore-validation:\t%t\n", iw.localSnap.IgnoreValidation)
   558  }
   559  
   560  func (iw *infoWriter) maybePrintCohortKey() {
   561  	if !iw.verbose {
   562  		return
   563  	}
   564  	if iw.localSnap == nil {
   565  		return
   566  	}
   567  	coh := iw.localSnap.CohortKey
   568  	if coh == "" {
   569  		return
   570  	}
   571  	if isStdoutTTY {
   572  		// 15 is 1 + the length of "refresh-date: "
   573  		coh = strutil.ElliptLeft(iw.localSnap.CohortKey, iw.termWidth-15)
   574  	}
   575  	fmt.Fprintf(iw, "cohort:\t%s\n", coh)
   576  }
   577  
   578  func (iw *infoWriter) maybePrintSum() {
   579  	if !iw.verbose {
   580  		return
   581  	}
   582  	if iw.diskSnap == nil {
   583  		// TODO: expose the sha via /v2/snaps and /v2/find
   584  		return
   585  	}
   586  	if osutil.IsDirectory(iw.path) {
   587  		// no sha3_384 of a directory :-)
   588  		return
   589  	}
   590  	sha3_384, _, _ := asserts.SnapFileSHA3_384(iw.path)
   591  	if sha3_384 == "" {
   592  		return
   593  	}
   594  	fmt.Fprintf(iw, "sha3-384:\t%s\n", sha3_384)
   595  }
   596  
   597  var channelRisks = []string{"stable", "candidate", "beta", "edge"}
   598  
   599  type channelInfo struct {
   600  	indent, name, version, released, revision, size, notes string
   601  }
   602  
   603  type channelInfos struct {
   604  	channels              []*channelInfo
   605  	maxRevLen, maxSizeLen int
   606  	releasedfmt, chantpl  string
   607  	needsHeader           bool
   608  	esc                   *escapes
   609  }
   610  
   611  func (chInfos *channelInfos) add(indent, name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) {
   612  	chInfo := &channelInfo{
   613  		indent:   indent,
   614  		name:     name,
   615  		version:  version,
   616  		revision: fmt.Sprintf("(%s)", revision),
   617  		size:     strutil.SizeToStr(size),
   618  		notes:    notes.String(),
   619  	}
   620  	if !released.IsZero() {
   621  		chInfo.released = released.Format(chInfos.releasedfmt)
   622  	}
   623  	if len(chInfo.revision) > chInfos.maxRevLen {
   624  		chInfos.maxRevLen = len(chInfo.revision)
   625  	}
   626  	if len(chInfo.size) > chInfos.maxSizeLen {
   627  		chInfos.maxSizeLen = len(chInfo.size)
   628  	}
   629  	chInfos.channels = append(chInfos.channels, chInfo)
   630  }
   631  
   632  func (chInfos *channelInfos) addFromLocal(local *client.Snap) {
   633  	chInfos.add("", "installed", local.Version, local.Revision, time.Time{}, local.InstalledSize, NotesFromLocal(local))
   634  }
   635  
   636  func (chInfos *channelInfos) addOpenChannel(name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) {
   637  	chInfos.add("  ", name, version, revision, released, size, notes)
   638  }
   639  
   640  func (chInfos *channelInfos) addClosedChannel(name string, trackHasOpenChannel bool) {
   641  	chInfo := &channelInfo{indent: "  ", name: name}
   642  	if trackHasOpenChannel {
   643  		chInfo.version = chInfos.esc.uparrow
   644  	} else {
   645  		chInfo.version = chInfos.esc.dash
   646  	}
   647  
   648  	chInfos.channels = append(chInfos.channels, chInfo)
   649  }
   650  
   651  func (chInfos *channelInfos) addFromRemote(remote *client.Snap) {
   652  	// order by tracks
   653  	for _, tr := range remote.Tracks {
   654  		trackHasOpenChannel := false
   655  		for _, risk := range channelRisks {
   656  			chName := fmt.Sprintf("%s/%s", tr, risk)
   657  			ch, ok := remote.Channels[chName]
   658  			if ok {
   659  				chInfos.addOpenChannel(chName, ch.Version, ch.Revision, ch.ReleasedAt, ch.Size, NotesFromChannelSnapInfo(ch))
   660  				trackHasOpenChannel = true
   661  			} else {
   662  				chInfos.addClosedChannel(chName, trackHasOpenChannel)
   663  			}
   664  		}
   665  	}
   666  	chInfos.needsHeader = len(chInfos.channels) > 0
   667  }
   668  
   669  func (chInfos *channelInfos) dump(w io.Writer) {
   670  	if chInfos.needsHeader {
   671  		fmt.Fprintln(w, "channels:")
   672  	}
   673  	for _, c := range chInfos.channels {
   674  		fmt.Fprintf(w, chInfos.chantpl, c.indent, c.name, c.version, c.released, chInfos.maxRevLen, c.revision, chInfos.maxSizeLen, c.size, c.notes)
   675  	}
   676  }
   677  
   678  func (x *infoCmd) Execute([]string) error {
   679  	termWidth, _ := termSize()
   680  	termWidth -= 3
   681  	if termWidth > 100 {
   682  		// any wider than this and it gets hard to read
   683  		termWidth = 100
   684  	}
   685  
   686  	esc := x.getEscapes()
   687  	w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0)
   688  	iw := &infoWriter{
   689  		writeflusher: w,
   690  		esc:          esc,
   691  		termWidth:    termWidth,
   692  		verbose:      x.Verbose,
   693  		fmtTime:      x.fmtTime,
   694  		absTime:      x.AbsTime,
   695  	}
   696  
   697  	noneOK := true
   698  	for i, snapName := range x.Positional.Snaps {
   699  		snapName := string(snapName)
   700  		if i > 0 {
   701  			fmt.Fprintln(w, "---")
   702  		}
   703  		if snapName == "system" {
   704  			fmt.Fprintln(w, "system: You can't have it.")
   705  			continue
   706  		}
   707  
   708  		if diskSnap, err := clientSnapFromPath(snapName); err == nil {
   709  			iw.setupDiskSnap(norm(snapName), diskSnap)
   710  		} else {
   711  			remoteSnap, resInfo, _ := x.client.FindOne(snap.InstanceSnap(snapName))
   712  			localSnap, _, _ := x.client.Snap(snapName)
   713  			iw.setupSnap(localSnap, remoteSnap, resInfo)
   714  		}
   715  		// note diskSnap == nil, or localSnap == nil and remoteSnap == nil
   716  
   717  		if iw.theSnap == nil {
   718  			if len(x.Positional.Snaps) == 1 {
   719  				w.Flush()
   720  				return fmt.Errorf("no snap found for %q", snapName)
   721  			}
   722  
   723  			fmt.Fprintf(w, fmt.Sprintf(i18n.G("warning:\tno snap found for %q\n"), snapName))
   724  			continue
   725  		}
   726  		noneOK = false
   727  
   728  		iw.maybePrintPath()
   729  		iw.printName()
   730  		iw.printSummary()
   731  		iw.maybePrintHealth()
   732  		iw.maybePrintPublisher()
   733  		iw.maybePrintStoreURL()
   734  		iw.maybePrintStandaloneVersion()
   735  		iw.maybePrintBuildDate()
   736  		iw.maybePrintContact()
   737  		iw.printLicense()
   738  		iw.maybePrintPrice()
   739  		iw.printDescr()
   740  		iw.maybePrintCommands()
   741  		iw.maybePrintServices()
   742  		iw.maybePrintNotes()
   743  		// stops the notes etc trying to be aligned with channels
   744  		iw.Flush()
   745  		iw.maybePrintType()
   746  		iw.maybePrintBase()
   747  		iw.maybePrintSum()
   748  		iw.maybePrintID()
   749  		iw.maybePrintCohortKey()
   750  		iw.maybePrintTrackingChannel()
   751  		iw.maybePrintInstallDate()
   752  		iw.maybePrintChinfo()
   753  	}
   754  	w.Flush()
   755  
   756  	if noneOK {
   757  		return fmt.Errorf(i18n.G("no valid snaps given"))
   758  	}
   759  
   760  	return nil
   761  }