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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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  	"os"
    25  	"sort"
    26  	"strconv"
    27  	"strings"
    28  	"text/tabwriter"
    29  
    30  	"github.com/jessevdk/go-flags"
    31  
    32  	"github.com/snapcore/snapd/i18n"
    33  	"github.com/snapcore/snapd/overlord/state"
    34  )
    35  
    36  type cmdDebugState struct {
    37  	timeMixin
    38  
    39  	Changes  bool   `long:"changes"`
    40  	TaskID   string `long:"task"`
    41  	ChangeID string `long:"change"`
    42  
    43  	IsSeeded bool `long:"is-seeded"`
    44  
    45  	// flags for --change=N output
    46  	DotOutput bool `long:"dot"` // XXX: mildly useful (too crowded in many cases), but let's have it just in case
    47  	// When inspecting errors/undone tasks, those in Hold state are usually irrelevant, make it possible to ignore them
    48  	NoHoldState bool `long:"no-hold"`
    49  
    50  	Positional struct {
    51  		StateFilePath string `positional-args:"yes" positional-arg-name:"<state-file>"`
    52  	} `positional-args:"yes"`
    53  }
    54  
    55  var cmdDebugStateShortHelp = i18n.G("Inspect a snapd state file.")
    56  var cmdDebugStateLongHelp = i18n.G("Inspect a snapd state file, bypassing snapd API.")
    57  
    58  type byChangeID []*state.Change
    59  
    60  func (c byChangeID) Len() int           { return len(c) }
    61  func (c byChangeID) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
    62  func (c byChangeID) Less(i, j int) bool { return c[i].ID() < c[j].ID() }
    63  
    64  func loadState(path string) (*state.State, error) {
    65  	if path == "" {
    66  		path = "state.json"
    67  	}
    68  	r, err := os.Open(path)
    69  	if err != nil {
    70  		return nil, fmt.Errorf("cannot read the state file: %s", err)
    71  	}
    72  	defer r.Close()
    73  
    74  	return state.ReadState(nil, r)
    75  }
    76  
    77  func init() {
    78  	addDebugCommand("state", cmdDebugStateShortHelp, cmdDebugStateLongHelp, func() flags.Commander {
    79  		return &cmdDebugState{}
    80  	}, timeDescs.also(map[string]string{
    81  		// TRANSLATORS: This should not start with a lowercase letter.
    82  		"change":    i18n.G("ID of the change to inspect"),
    83  		"task":      i18n.G("ID of the task to inspect"),
    84  		"dot":       i18n.G("Dot (graphviz) output"),
    85  		"no-hold":   i18n.G("Omit tasks in 'Hold' state in the change output"),
    86  		"changes":   i18n.G("List all changes"),
    87  		"is-seeded": i18n.G("Output seeding status (true or false)"),
    88  	}), nil)
    89  }
    90  
    91  type byLaneAndWaitTaskChain []*state.Task
    92  
    93  func (t byLaneAndWaitTaskChain) Len() int      { return len(t) }
    94  func (t byLaneAndWaitTaskChain) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
    95  func (t byLaneAndWaitTaskChain) Less(i, j int) bool {
    96  	if t[i].ID() == t[j].ID() {
    97  		return false
    98  	}
    99  	// cover the typical case (just one lane), and order by first lane
   100  	if t[i].Lanes()[0] == t[j].Lanes()[0] {
   101  		seenTasks := make(map[string]bool)
   102  		return t.waitChainSearch(t[i], t[j], seenTasks)
   103  	}
   104  	return t[i].Lanes()[0] < t[j].Lanes()[0]
   105  }
   106  
   107  func (t *byLaneAndWaitTaskChain) waitChainSearch(startT, searchT *state.Task, seenTasks map[string]bool) bool {
   108  	if seenTasks[startT.ID()] {
   109  		return false
   110  	}
   111  	seenTasks[startT.ID()] = true
   112  	for _, cand := range startT.HaltTasks() {
   113  		if cand == searchT {
   114  			return true
   115  		}
   116  		if t.waitChainSearch(cand, searchT, seenTasks) {
   117  			return true
   118  		}
   119  	}
   120  
   121  	return false
   122  }
   123  
   124  func (c *cmdDebugState) writeDotOutput(st *state.State, changeID string) error {
   125  	st.Lock()
   126  	defer st.Unlock()
   127  
   128  	chg := st.Change(changeID)
   129  	if chg == nil {
   130  		return fmt.Errorf("no such change: %s", changeID)
   131  	}
   132  
   133  	fmt.Fprintf(Stdout, "digraph D{\n")
   134  	tasks := chg.Tasks()
   135  	for _, t := range tasks {
   136  		if c.NoHoldState && t.Status() == state.HoldStatus {
   137  			continue
   138  		}
   139  		fmt.Fprintf(Stdout, "  %s [label=%q];\n", t.ID(), t.Kind())
   140  		for _, wt := range t.WaitTasks() {
   141  			if c.NoHoldState && wt.Status() == state.HoldStatus {
   142  				continue
   143  			}
   144  			fmt.Fprintf(Stdout, "  %s -> %s;\n", t.ID(), wt.ID())
   145  		}
   146  	}
   147  	fmt.Fprintf(Stdout, "}\n")
   148  
   149  	return nil
   150  }
   151  
   152  func (c *cmdDebugState) showTasks(st *state.State, changeID string) error {
   153  	st.Lock()
   154  	defer st.Unlock()
   155  
   156  	chg := st.Change(changeID)
   157  	if chg == nil {
   158  		return fmt.Errorf("no such change: %s", changeID)
   159  	}
   160  
   161  	tasks := chg.Tasks()
   162  	sort.Sort(byLaneAndWaitTaskChain(tasks))
   163  
   164  	w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0)
   165  	fmt.Fprintf(w, "Lanes\tID\tStatus\tSpawn\tReady\tKind\tSummary\n")
   166  	for _, t := range tasks {
   167  		if c.NoHoldState && t.Status() == state.HoldStatus {
   168  			continue
   169  		}
   170  		var lanes []string
   171  		for _, lane := range t.Lanes() {
   172  			lanes = append(lanes, fmt.Sprintf("%d", lane))
   173  		}
   174  		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
   175  			strings.Join(lanes, ","),
   176  			t.ID(),
   177  			t.Status().String(),
   178  			c.fmtTime(t.SpawnTime()),
   179  			c.fmtTime(t.ReadyTime()),
   180  			t.Kind(),
   181  			t.Summary())
   182  	}
   183  
   184  	w.Flush()
   185  
   186  	for _, t := range tasks {
   187  		logs := t.Log()
   188  		if len(logs) > 0 {
   189  			fmt.Fprintf(Stdout, "---\n")
   190  			fmt.Fprintf(Stdout, "%s %s\n", t.ID(), t.Summary())
   191  			for _, log := range logs {
   192  				fmt.Fprintf(Stdout, "  %s\n", log)
   193  			}
   194  		}
   195  	}
   196  
   197  	return nil
   198  }
   199  
   200  func (c *cmdDebugState) showChanges(st *state.State) error {
   201  	st.Lock()
   202  	defer st.Unlock()
   203  
   204  	changes := st.Changes()
   205  	sort.Sort(byChangeID(changes))
   206  
   207  	w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0)
   208  	fmt.Fprintf(w, "ID\tStatus\tSpawn\tReady\tLabel\tSummary\n")
   209  	for _, chg := range changes {
   210  		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
   211  			chg.ID(),
   212  			chg.Status().String(),
   213  			c.fmtTime(chg.SpawnTime()),
   214  			c.fmtTime(chg.ReadyTime()),
   215  			chg.Kind(),
   216  			chg.Summary())
   217  	}
   218  	w.Flush()
   219  
   220  	return nil
   221  }
   222  
   223  func (c *cmdDebugState) showIsSeeded(st *state.State) error {
   224  	st.Lock()
   225  	defer st.Unlock()
   226  
   227  	var isSeeded bool
   228  	err := st.Get("seeded", &isSeeded)
   229  	if err != nil && err != state.ErrNoState {
   230  		return err
   231  	}
   232  	fmt.Fprintf(Stdout, "%v\n", isSeeded)
   233  
   234  	return nil
   235  }
   236  
   237  func (c *cmdDebugState) showTask(st *state.State, taskID string) error {
   238  	st.Lock()
   239  	defer st.Unlock()
   240  
   241  	task := st.Task(taskID)
   242  	if task == nil {
   243  		return fmt.Errorf("no such task: %s", taskID)
   244  	}
   245  
   246  	termWidth, _ := termSize()
   247  	termWidth -= 3
   248  	if termWidth > 100 {
   249  		// any wider than this and it gets hard to read
   250  		termWidth = 100
   251  	}
   252  
   253  	// the output of 'debug task' is yaml'ish
   254  	fmt.Fprintf(Stdout, "id: %s\nkind: %s\nsummary: %s\nstatus: %s\n",
   255  		taskID, task.Kind(),
   256  		task.Summary(),
   257  		task.Status().String())
   258  	log := task.Log()
   259  	if len(log) > 0 {
   260  		fmt.Fprintf(Stdout, "log: |\n")
   261  		for _, msg := range log {
   262  			if err := wrapLine(Stdout, []rune(msg), "  ", termWidth); err != nil {
   263  				break
   264  			}
   265  		}
   266  		fmt.Fprintln(Stdout)
   267  	}
   268  
   269  	fmt.Fprintf(Stdout, "halt-tasks:")
   270  	if len(task.HaltTasks()) == 0 {
   271  		fmt.Fprintln(Stdout, " []")
   272  	} else {
   273  		fmt.Fprintln(Stdout)
   274  		for _, ht := range task.HaltTasks() {
   275  			fmt.Fprintf(Stdout, " - %s (%s)\n", ht.Kind(), ht.ID())
   276  		}
   277  	}
   278  
   279  	return nil
   280  }
   281  
   282  func (c *cmdDebugState) Execute(args []string) error {
   283  	st, err := loadState(c.Positional.StateFilePath)
   284  	if err != nil {
   285  		return err
   286  	}
   287  
   288  	// check valid combinations of args
   289  	var cmds []string
   290  	if c.Changes {
   291  		cmds = append(cmds, "--changes")
   292  	}
   293  	if c.ChangeID != "" {
   294  		cmds = append(cmds, "--change=")
   295  	}
   296  	if c.TaskID != "" {
   297  		cmds = append(cmds, "--task=")
   298  	}
   299  	if c.IsSeeded {
   300  		cmds = append(cmds, "--is-seeded")
   301  	}
   302  	if len(cmds) > 1 {
   303  		return fmt.Errorf("cannot use %s and %s together", cmds[0], cmds[1])
   304  	}
   305  
   306  	if c.IsSeeded {
   307  		return c.showIsSeeded(st)
   308  	}
   309  
   310  	if c.DotOutput && c.ChangeID == "" {
   311  		return fmt.Errorf("--dot can only be used with --change=")
   312  	}
   313  	if c.NoHoldState && c.ChangeID == "" {
   314  		return fmt.Errorf("--no-hold can only be used with --change=")
   315  	}
   316  
   317  	if c.Changes {
   318  		return c.showChanges(st)
   319  	}
   320  
   321  	if c.ChangeID != "" {
   322  		_, err := strconv.ParseInt(c.ChangeID, 0, 64)
   323  		if err != nil {
   324  			return fmt.Errorf("invalid change: %s", c.ChangeID)
   325  		}
   326  		if c.DotOutput {
   327  			return c.writeDotOutput(st, c.ChangeID)
   328  		}
   329  		return c.showTasks(st, c.ChangeID)
   330  	}
   331  
   332  	if c.TaskID != "" {
   333  		_, err := strconv.ParseInt(c.TaskID, 0, 64)
   334  		if err != nil {
   335  			return fmt.Errorf("invalid task: %s", c.TaskID)
   336  		}
   337  		return c.showTask(st, c.TaskID)
   338  	}
   339  
   340  	// show changes by default
   341  	return c.showChanges(st)
   342  }