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