Skip to content

Commit b60cc7f

Browse files
committed
(wip)o/h/ctlcmd, t/main/snapctl-tasks: start work on unit and spread testing
1 parent d0b39f1 commit b60cc7f

8 files changed

Lines changed: 490 additions & 55 deletions

File tree

overlord/hookstate/ctlcmd/helpers.go

Lines changed: 20 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,18 @@ var (
5656
snapctlRemoveChangeKind = swfeats.RegisterChangeKind("snapctl-remove")
5757
)
5858

59+
var (
60+
changeNotFoundError = "change %q not found"
61+
)
62+
5963
func init() {
6064
finalTasks = make(map[string]bool, len(snapstate.FinalTasks))
6165
for _, kind := range snapstate.FinalTasks {
6266
finalTasks[kind] = true
6367
}
6468
}
6569

66-
const snapctlDebounceWindow = 200 * time.Millisecond
70+
const snapctlDebounceWindow = 100 * time.Millisecond
6771

6872
// finalSeedTask is the last task that should run during seeding. This is used
6973
// in the special handling of the "seed" change, which requires that we
@@ -521,17 +525,17 @@ func isReady(hctx *hookstate.Context, changeID string) (state.Status, error) {
521525
chg := st.Change(changeID)
522526

523527
if chg == nil {
524-
return state.DefaultStatus, fmt.Errorf("change %q not found", changeID)
528+
return state.DefaultStatus, fmt.Errorf(changeNotFoundError, changeID)
525529
}
526530

527531
var initiatorSnapName string
528532
err := chg.Get("initiated-by-snap", &initiatorSnapName)
529533
if err != nil {
530-
return state.DefaultStatus, fmt.Errorf("change %q not found", changeID)
534+
return state.DefaultStatus, fmt.Errorf(changeNotFoundError, changeID)
531535
}
532536

533537
if initiatorSnapName != callerSnapName {
534-
return state.DefaultStatus, fmt.Errorf("change %q not found", changeID)
538+
return state.DefaultStatus, fmt.Errorf(changeNotFoundError, changeID)
535539
}
536540

537541
wait, err := rateLimit(st, changeID, snapctlDebounceWindow)
@@ -640,8 +644,9 @@ func setChangeAccessedAt(st *state.State, accessed time.Time, changeID string) {
640644
st.Cache(key, accessed.UnixNano())
641645
}
642646

643-
// changeStatus checks if the change is ready, if it is, it returns the status, otherwise st.Doing.
644-
func changeStatus(hctx *hookstate.Context, changeID string) (state.Change, error) {
647+
// getAssociatedChange returns a change associated with the snapctl context and passed change ID,
648+
// otherwise nil with error
649+
func getAssociatedChange(hctx *hookstate.Context, changeID string) (*state.Change, error) {
645650
callerSnapName := hctx.InstanceName()
646651

647652
st := hctx.State()
@@ -651,55 +656,29 @@ func changeStatus(hctx *hookstate.Context, changeID string) (state.Change, error
651656
chg := st.Change(changeID)
652657

653658
if chg == nil {
654-
return state.Change{}, fmt.Errorf("change %q not found", changeID)
659+
return nil, fmt.Errorf(changeNotFoundError, changeID)
655660
}
656661

657662
var initiatorSnapName string
658663
err := chg.Get("initiated-by-snap", &initiatorSnapName)
659664
if err != nil {
660-
return state.Change{}, fmt.Errorf("could not find initiator attribute for change %q", changeID)
665+
return nil, fmt.Errorf(changeNotFoundError, changeID)
661666
}
662667

663668
if initiatorSnapName != callerSnapName {
664-
return state.Change{}, fmt.Errorf("change %q was initiated by another snap", changeID)
669+
return nil, fmt.Errorf(changeNotFoundError, changeID)
665670
}
666671

667-
lastAccess := st.Cached(fmt.Sprintf("snapctl-%s-last-accessed", callerSnapName))
668-
st.Cache(fmt.Sprintf("snapctl-%s-last-accessed", hctx.InstanceName()), time.Now().UnixNano())
669-
670-
// Compute how long to wait before checking the change status.
671-
var toWait time.Duration
672-
if lastAccess != nil {
673-
lastAccessNano, ok := lastAccess.(int64)
674-
if !ok {
675-
return state.Change{}, fmt.Errorf("invalid last accessed time format for change %q", changeID)
676-
}
677-
toWait = 200*time.Millisecond - time.Since(time.Unix(0, lastAccessNano))
672+
wait, err := rateLimit(st, changeID, snapctlDebounceWindow)
673+
if err != nil {
674+
return nil, err
678675
}
679676

680677
st.Unlock()
678+
<-timeAfter(wait)
679+
st.Lock()
681680

682-
ready := chg.Ready()
683-
684-
if toWait <= 0 {
685-
select {
686-
case <-ready:
687-
st.Lock()
688-
return *chg, nil
689-
default:
690-
st.Lock()
691-
return state.Change{}, nil
692-
}
693-
}
694-
695-
select {
696-
case <-ready:
697-
st.Lock()
698-
return *chg, nil
699-
case <-timeAfter(toWait):
700-
st.Lock()
701-
return state.Change{}, nil
702-
}
681+
return chg, nil
703682
}
704683

705684
// getAttribute unmarshals into result the value of the provided key from attributes map.

overlord/hookstate/ctlcmd/tasks.go

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,35 +20,60 @@
2020
package ctlcmd
2121

2222
import (
23+
"encoding/json"
2324
"fmt"
25+
"time"
26+
27+
"io"
28+
"text/tabwriter"
2429

2530
"github.com/snapcore/snapd/i18n"
2631
"github.com/snapcore/snapd/overlord/state"
32+
"github.com/snapcore/snapd/timeutil"
2733
)
2834

2935
type tasksCommand struct {
3036
baseCommand
31-
json bool
37+
Format string `long:"format" required:"false" description:"Output format (json)"`
3238
}
3339

3440
var shortTasksHelp = i18n.G(`Return a list of information associated with all change-ids.`)
3541
var longTasksHelp = i18n.G(`
36-
The tasks command is used to query the status of all change ids associated with
42+
Used to query the status of all change ids associated with
3743
snapctl commands running in asynchronous mode.
3844
39-
$ snapctl tasks [--json]
45+
$ snapctl (tasks/change) [--format FORMAT]
4046
0: successfully reported change information, regardless of state of change
4147
1: any error (invalid change ID, permissions error)
42-
stdout: table of tasks, mirroring "snap tasks <change-id>" output
48+
stdout: table of tasks, mirroring "snap changes <change-id>" output
4349
stderr: empty for exit code 0. Contains relevant errors for exit code 1.
4450
`)
4551

4652
func init() {
53+
// Not using standard alias declaration as that's in *cmdInfo, used by client, we have *commandInfo
54+
addCommand("change", shortTasksHelp, longTasksHelp, func() command {
55+
return &tasksCommand{}
56+
})
4757
addCommand("tasks", shortTasksHelp, longTasksHelp, func() command {
4858
return &tasksCommand{}
4959
})
5060
}
5161

62+
func (c *tasksCommand) newTabWriter(output io.Writer) *tabwriter.Writer {
63+
minWidth := 2
64+
tabWidth := 2
65+
padding := 2
66+
padchar := byte(' ')
67+
return tabwriter.NewWriter(output, minWidth, tabWidth, padding, padchar, 0)
68+
}
69+
70+
func fmtTime(t time.Time, abs bool) string {
71+
if abs {
72+
return t.Format(time.RFC3339)
73+
}
74+
return timeutil.Human(t)
75+
}
76+
5277
func (c *tasksCommand) Execute(args []string) error {
5378
ctx, err := c.ensureContext()
5479
if err != nil {
@@ -59,21 +84,48 @@ func (c *tasksCommand) Execute(args []string) error {
5984
return fmt.Errorf("invalid number of arguments: expected 1, got %d", len(args))
6085
}
6186

62-
c.json = c.flagSet.Lookup("json").Value.String() == "true"
87+
changeID := args[0]
6388

64-
ready, err := isReady(ctx, c.changeID)
89+
change, err := getAssociatedChange(ctx, changeID)
6590

6691
if err != nil {
67-
fmt.Fprintf(c.stderr, err.Error())
68-
return &UnsuccessfulError{ExitCode: otherErrorExitCode}
69-
}
70-
71-
if !ready.Ready() {
72-
return &UnsuccessfulError{ExitCode: changeNotReadyExitCode}
92+
return err
7393
}
7494

75-
if ready != state.DoneStatus {
76-
return &UnsuccessfulError{ExitCode: changeUnsuccessfulExitCode}
95+
st := ctx.State()
96+
97+
if c.Format == "json" {
98+
st.Lock()
99+
data, err := json.Marshal(change)
100+
st.Unlock()
101+
if err != nil {
102+
return err
103+
}
104+
fmt.Fprint(c.stdout, string(data))
105+
} else {
106+
w := c.newTabWriter(c.stdout)
107+
108+
fmt.Fprint(w, i18n.G("ID\tStatus\tSpawn\tReady\tSummary\n"))
109+
110+
st.Lock()
111+
for _, t := range change.Tasks() {
112+
spawnTime := fmtTime(t.SpawnTime(), false)
113+
readyTime := fmtTime(t.ReadyTime(), false)
114+
if t.ReadyTime().IsZero() {
115+
readyTime = "-"
116+
}
117+
summary := t.Summary()
118+
status := t.Status()
119+
_, done, total := t.Progress()
120+
if status == state.DoingStatus && total > 1 {
121+
summary = fmt.Sprintf("%s (%.2f%%)", summary, float64(done)/float64(total)*100.0)
122+
}
123+
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", status, spawnTime, readyTime, summary)
124+
}
125+
st.Unlock()
126+
127+
w.Flush()
128+
fmt.Fprintln(c.stdout)
77129
}
78130

79131
return nil

0 commit comments

Comments
 (0)