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 }