Skip to content

Commit 684b4a0

Browse files
committed
o/h/ctlcmd: (wip) start work on snapctl tasks
1 parent 04b719d commit 684b4a0

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
@@ -764,6 +764,68 @@ func setChangeAccessedAt(st *state.State, accessed time.Time, changeID string) {
764764
st.Cache(key, accessed.UnixNano())
765765
}
766766

767+
// changeStatus checks if the change is ready, if it is, it returns the status, otherwise st.Doing.
768+
func changeStatus(hctx *hookstate.Context, changeID string) (state.Change, error) {
769+
callerSnapName := hctx.InstanceName()
770+
771+
st := hctx.State()
772+
st.Lock()
773+
defer st.Unlock()
774+
775+
chg := st.Change(changeID)
776+
777+
if chg == nil {
778+
return state.Change{}, fmt.Errorf("change %q not found", changeID)
779+
}
780+
781+
var initiatorSnapName string
782+
err := chg.Get("initiated-by-snap", &initiatorSnapName)
783+
if err != nil {
784+
return state.Change{}, fmt.Errorf("could not find initiator attribute for change %q", changeID)
785+
}
786+
787+
if initiatorSnapName != callerSnapName {
788+
return state.Change{}, fmt.Errorf("change %q was initiated by another snap", changeID)
789+
}
790+
791+
lastAccess := st.Cached(fmt.Sprintf("snapctl-%s-last-accessed", callerSnapName))
792+
st.Cache(fmt.Sprintf("snapctl-%s-last-accessed", hctx.InstanceName()), time.Now().UnixNano())
793+
794+
// Compute how long to wait before checking the change status.
795+
var toWait time.Duration
796+
if lastAccess != nil {
797+
lastAccessNano, ok := lastAccess.(int64)
798+
if !ok {
799+
return state.Change{}, fmt.Errorf("invalid last accessed time format for change %q", changeID)
800+
}
801+
toWait = 200*time.Millisecond - time.Since(time.Unix(0, lastAccessNano))
802+
}
803+
804+
st.Unlock()
805+
806+
ready := chg.Ready()
807+
808+
if toWait <= 0 {
809+
select {
810+
case <-ready:
811+
st.Lock()
812+
return *chg, nil
813+
default:
814+
st.Lock()
815+
return state.Change{}, nil
816+
}
817+
}
818+
819+
select {
820+
case <-ready:
821+
st.Lock()
822+
return *chg, nil
823+
case <-timeAfter(toWait):
824+
st.Lock()
825+
return state.Change{}, nil
826+
}
827+
}
828+
767829
// getAttribute unmarshals into result the value of the provided key from attributes map.
768830
// If the key does not exist, an error of type *NoAttributeError is returned.
769831
// 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)