File: wait.go

package info (click to toggle)
snapd 2.73-3
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 81,460 kB
  • sloc: sh: 16,736; ansic: 16,652; python: 11,215; makefile: 1,966; exp: 190; awk: 58; xml: 22
file content (237 lines) | stat: -rw-r--r-- 6,442 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2016-2017 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package main

import (
	"errors"
	"fmt"
	"os"
	"os/signal"
	"time"

	"github.com/snapcore/snapd/client"
	"github.com/snapcore/snapd/i18n"
	"github.com/snapcore/snapd/progress"
)

var (
	maxGoneTime = 5 * time.Second
	pollTime    = 100 * time.Millisecond
)

// mustWaitMixin mixin which exposes a helper method to wait for a state change.
type mustWaitMixin struct {
	clientMixin
	// skipAbort when set, the change we wait for will not be aborted
	skipAbort bool
	// noProgress when set, no progress will be shown to the user
	noProgress bool

	// Wait also for tasks in the "wait" state.
	waitForTasksInWaitStatus bool
}

// wait waits for a given change to complete. By default, unless skipAbort is
// set, it will abort the change when SIGINT is received. Change progress is
// reported to standard output, unless noProgress is true.
func (wmx mustWaitMixin) wait(id string) (*client.Change, error) {
	cli := wmx.client
	// Intercept sigint
	c := make(chan os.Signal, 2)
	signal.Notify(c, os.Interrupt)
	go func() {
		sig := <-c
		// sig is nil if c was closed
		if sig == nil || wmx.skipAbort {
			return
		}
		_, err := wmx.client.Abort(id)
		if err != nil {
			fmt.Fprint(Stderr, err.Error()+"\n")
		}
	}()

	var pb progress.Meter
	if wmx.noProgress {
		pb = &progress.Null
	} else {
		pb = progress.MakeProgressBar(Stdout)
	}
	defer func() {
		pb.Finished()
		// next two not strictly needed for CLI, but without
		// them the tests will leak goroutines.
		signal.Stop(c)
		close(c)
	}()

	tMax := time.Time{}

	var lastID string
	lastLog := map[string]string{}
	for {
		var rebootingErr error
		chg, err := cli.Change(id)
		if err != nil {
			// a client.Error means we were able to communicate with
			// the server (got an answer)
			if e, ok := err.(*client.Error); ok {
				return nil, e
			}

			// A non-client error here means the server most likely went away.
			// First thing we should check is whether this is a part of a system restart,
			// as in that case we want to to report this to user instead of looping here until
			// the restart does happen. (Or in the case of spread tests, blocks forever).
			if e, ok := cli.Maintenance().(*client.Error); ok && e.Kind == client.ErrorKindSystemRestart {
				return nil, e
			}

			// Otherwise it's most likely a daemon restart, assume it might come up again.
			// XXX: it actually can be a bunch of other things; fix client to expose it better
			now := time.Now()
			if tMax.IsZero() {
				tMax = now.Add(maxGoneTime)
			}
			if now.After(tMax) {
				return nil, err
			}
			pb.Spin(i18n.G("Waiting for server to restart"))
			time.Sleep(pollTime)
			continue
		}
		if maintErr, ok := cli.Maintenance().(*client.Error); ok && maintErr.Kind == client.ErrorKindSystemRestart {
			rebootingErr = maintErr
		}
		if !tMax.IsZero() {
			pb.Finished()
			tMax = time.Time{}
		}

		maybeShowLog := func(t *client.Task) {
			nowLog := lastLogStr(t.Log)
			if lastLog[t.ID] != nowLog {
				pb.Notify(nowLog)
				lastLog[t.ID] = nowLog
			}
		}

		// Tasks in "wait" state communicate the wait reason
		// via the log mechanism. So make sure the log is
		// visible even if the normal progress reporting
		// has tasks in "Doing" state (like "check-refresh")
		// that would suppress displaying the log. This will
		// ensure on a classic+modes system the user sees
		// the messages: "Task set to wait until a manual system restart allows to continue"
		for _, t := range chg.Tasks {
			if t.Status == "Wait" {
				maybeShowLog(t)
			}
		}

		// progress reporting
		inDoing := map[string]*client.Task{}
		for _, t := range chg.Tasks {
			if t.Status == "Doing" {
				inDoing[t.ID] = t
			}
		}

		// Show the last 'not yet done' task, which hopefully is a good
		// representation of how the operation is progressing. In case of single
		// snap operations this should nicely pick snap's own 'active' tasks or
		// one from its dependencies.
		for i := len(chg.Tasks) - 1; i >= 0; i-- {
			t := chg.Tasks[i]
			switch {
			case t.Status != "Doing" && t.Status != "Wait":
				continue
			case t.Kind == "check-rerefresh" && len(inDoing) > 1:
				// when doing a refresh, check-rerefresh task is perpetually in
				// Doing state as it not blocked by other tasks, but rather
				// monitors them for completion so that additional refresh check
				// can be performend. Purposefully skip unless it's really the
				// only running task.
				continue
			case t.Progress.Total == 1:
				pb.Spin(t.Summary)
				maybeShowLog(t)
			case t.ID == lastID:
				pb.Set(float64(t.Progress.Done))
			default:
				pb.Start(t.Summary, float64(t.Progress.Total))
				lastID = t.ID
			}
			break
		}

		if !wmx.waitForTasksInWaitStatus && chg.Status == "Wait" {
			return chg, nil
		}

		if chg.Ready {
			if chg.Status == "Done" {
				return chg, nil
			}

			if chg.Err != "" {
				return chg, errors.New(chg.Err)
			}

			return nil, fmt.Errorf(i18n.G("change finished in status %q with no error message"), chg.Status)
		}

		if rebootingErr != nil {
			return nil, rebootingErr
		}

		// note this very purposely is not a ticker; we want
		// to sleep 100ms between calls, not call once every
		// 100ms.
		time.Sleep(pollTime)
	}
}

type waitMixin struct {
	mustWaitMixin
	NoWait bool `long:"no-wait"`
}

var waitDescs = mixinDescs{
	// TRANSLATORS: This should not start with a lowercase letter.
	"no-wait": i18n.G("Do not wait for the operation to finish but just print the change id."),
}

var noWait = errors.New("no wait for op")

func (wmx waitMixin) wait(id string) (*client.Change, error) {
	if wmx.NoWait {
		fmt.Fprintf(Stdout, "%s\n", id)
		return nil, noWait
	}
	return wmx.mustWaitMixin.wait(id)
}

func lastLogStr(logs []string) string {
	if len(logs) == 0 {
		return ""
	}
	return logs[len(logs)-1]
}