Skip to content

Commit d0b39f1

Browse files
committed
o/h/ctlcmd: (wip) start work on snapctl tasks
1 parent 6ef8a8e commit d0b39f1

2 files changed

Lines changed: 142 additions & 0 deletions

File tree

overlord/hookstate/ctlcmd/helpers.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,68 @@ func setChangeAccessedAt(st *state.State, accessed time.Time, changeID string) {
640640
st.Cache(key, accessed.UnixNano())
641641
}
642642

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) {
645+
callerSnapName := hctx.InstanceName()
646+
647+
st := hctx.State()
648+
st.Lock()
649+
defer st.Unlock()
650+
651+
chg := st.Change(changeID)
652+
653+
if chg == nil {
654+
return state.Change{}, fmt.Errorf("change %q not found", changeID)
655+
}
656+
657+
var initiatorSnapName string
658+
err := chg.Get("initiated-by-snap", &initiatorSnapName)
659+
if err != nil {
660+
return state.Change{}, fmt.Errorf("could not find initiator attribute for change %q", changeID)
661+
}
662+
663+
if initiatorSnapName != callerSnapName {
664+
return state.Change{}, fmt.Errorf("change %q was initiated by another snap", changeID)
665+
}
666+
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))
678+
}
679+
680+
st.Unlock()
681+
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+
}
703+
}
704+
643705
// getAttribute unmarshals into result the value of the provided key from attributes map.
644706
// If the key does not exist, an error of type *NoAttributeError is returned.
645707
// The provided key may be formed as a dotted key path through nested maps.

overlord/hookstate/ctlcmd/tasks.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// -*- Mode: Go; indent-tabs-mode: t -*-
2+
3+
/*
4+
* Copyright (C) 2026 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 ctlcmd
21+
22+
import (
23+
"fmt"
24+
25+
"github.com/snapcore/snapd/i18n"
26+
"github.com/snapcore/snapd/overlord/state"
27+
)
28+
29+
type tasksCommand struct {
30+
baseCommand
31+
json bool
32+
}
33+
34+
var shortTasksHelp = i18n.G(`Return a list of information associated with all change-ids.`)
35+
var longTasksHelp = i18n.G(`
36+
The tasks command is used to query the status of all change ids associated with
37+
snapctl commands running in asynchronous mode.
38+
39+
$ snapctl tasks [--json]
40+
0: successfully reported change information, regardless of state of change
41+
1: any error (invalid change ID, permissions error)
42+
stdout: table of tasks, mirroring "snap tasks <change-id>" output
43+
stderr: empty for exit code 0. Contains relevant errors for exit code 1.
44+
`)
45+
46+
func init() {
47+
addCommand("tasks", shortTasksHelp, longTasksHelp, func() command {
48+
return &tasksCommand{}
49+
})
50+
}
51+
52+
func (c *tasksCommand) Execute(args []string) error {
53+
ctx, err := c.ensureContext()
54+
if err != nil {
55+
return err
56+
}
57+
58+
if len(args) != 1 {
59+
return fmt.Errorf("invalid number of arguments: expected 1, got %d", len(args))
60+
}
61+
62+
c.json = c.flagSet.Lookup("json").Value.String() == "true"
63+
64+
ready, err := isReady(ctx, c.changeID)
65+
66+
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}
73+
}
74+
75+
if ready != state.DoneStatus {
76+
return &UnsuccessfulError{ExitCode: changeUnsuccessfulExitCode}
77+
}
78+
79+
return nil
80+
}

0 commit comments

Comments
 (0)