/
refresh.go
231 lines (213 loc) · 8.87 KB
/
refresh.go
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
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2019 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 snapstate
import (
"fmt"
"path/filepath"
"sort"
"strings"
"github.com/snapcore/snapd/cmd/snaplock/runinhibit"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/snapstate/backend"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/sandbox/cgroup"
"github.com/snapcore/snapd/snap"
userclient "github.com/snapcore/snapd/usersession/client"
)
// pidsOfSnap is a mockable version of PidsOfSnap
var pidsOfSnap = cgroup.PidsOfSnap
var genericRefreshCheck = func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error {
knownPids, err := pidsOfSnap(info.InstanceName())
if err != nil {
return err
}
// Due to specific of the interaction with locking, all locking is performed by the caller.
var busyAppNames []string
var busyHookNames []string
var busyPIDs []int
// Currently there are no situations when hooks might be allowed to run
// during the refresh process. The function exists to make the next two
// chunks of code symmetric.
canHookRunDuringRefresh := func(hook *snap.HookInfo) bool {
return false
}
for name, app := range info.Apps {
if canAppRunDuringRefresh(app) {
continue
}
if PIDs := knownPids[app.SecurityTag()]; len(PIDs) > 0 {
busyAppNames = append(busyAppNames, name)
busyPIDs = append(busyPIDs, PIDs...)
}
}
for name, hook := range info.Hooks {
if canHookRunDuringRefresh(hook) {
continue
}
if PIDs := knownPids[hook.SecurityTag()]; len(PIDs) > 0 {
busyHookNames = append(busyHookNames, name)
busyPIDs = append(busyPIDs, PIDs...)
}
}
if len(busyAppNames) == 0 && len(busyHookNames) == 0 {
return nil
}
sort.Strings(busyAppNames)
sort.Strings(busyHookNames)
sort.Ints(busyPIDs)
return &BusySnapError{
SnapInfo: info,
busyAppNames: busyAppNames,
busyHookNames: busyHookNames,
pids: busyPIDs,
}
}
// SoftNothingRunningRefreshCheck looks if there are at most only service processes alive.
//
// The check is designed to run early in the refresh pipeline. Before
// downloading or stopping services for the update, we can check that only
// services are running, that is, that no non-service apps or hooks are
// currently running.
//
// Since services are stopped during the update this provides a good early
// precondition check. The check is also deliberately racy as existing snap
// commands can fork new processes or existing processes can die. After the
// soft check passes the user is free to start snap applications and block the
// hard check.
func SoftNothingRunningRefreshCheck(info *snap.Info) error {
return genericRefreshCheck(info, func(app *snap.AppInfo) bool {
return app.IsService()
})
}
// HardNothingRunningRefreshCheck looks if there are any undesired processes alive.
//
// The check is designed to run late in the refresh pipeline, after stopping
// snap services. At this point non-enduring services should be stopped, hooks
// should no longer run, and applications should be barred from running
// externally (e.g. by using a new inhibition mechanism for snap run).
//
// The check fails if any process belonging to the snap, apart from services
// that are enduring refresh, is still alive. If a snap is busy it cannot be
// refreshed and the refresh process is aborted.
func HardNothingRunningRefreshCheck(info *snap.Info) error {
return genericRefreshCheck(info, func(app *snap.AppInfo) bool {
// TODO: use a constant instead of "endure"
return app.IsService() && app.RefreshMode == "endure"
})
}
// BusySnapError indicates that snap has apps or hooks running and cannot refresh.
type BusySnapError struct {
SnapInfo *snap.Info
pids []int
busyAppNames []string
busyHookNames []string
}
// PendingSnapRefreshInfo computes information necessary to perform user notification
// of postponed refresh of a snap, based on the information about snap "business".
//
// The returned value contains the instance name of the snap as well as, if possible,
// information relevant for desktop notification services, such as application name
// and the snapd-generated desktop file name.
func (err *BusySnapError) PendingSnapRefreshInfo() *userclient.PendingSnapRefreshInfo {
refreshInfo := &userclient.PendingSnapRefreshInfo{
InstanceName: err.SnapInfo.InstanceName(),
}
for _, appName := range err.busyAppNames {
if app, ok := err.SnapInfo.Apps[appName]; ok {
path := app.DesktopFile()
if osutil.FileExists(path) {
refreshInfo.BusyAppName = appName
refreshInfo.BusyAppDesktopEntry = strings.SplitN(filepath.Base(path), ".", 2)[0]
break
}
}
}
return refreshInfo
}
// Error formats an error string describing what is running.
func (err *BusySnapError) Error() string {
switch {
case len(err.busyAppNames) > 0 && len(err.busyHookNames) > 0:
return fmt.Sprintf("snap %q has running apps (%s) and hooks (%s)",
err.SnapInfo.InstanceName(), strings.Join(err.busyAppNames, ", "), strings.Join(err.busyHookNames, ", "))
case len(err.busyAppNames) > 0:
return fmt.Sprintf("snap %q has running apps (%s)",
err.SnapInfo.InstanceName(), strings.Join(err.busyAppNames, ", "))
case len(err.busyHookNames) > 0:
return fmt.Sprintf("snap %q has running hooks (%s)",
err.SnapInfo.InstanceName(), strings.Join(err.busyHookNames, ", "))
default:
return fmt.Sprintf("snap %q has running apps or hooks", err.SnapInfo.InstanceName())
}
}
// Pids returns the set of process identifiers that are running.
//
// Since this list is a snapshot it should be only acted upon if there is an
// external synchronization system applied (e.g. all processes are frozen) at
// the time the snapshot was taken.
//
// The list is intended for snapd to forcefully kill all processes for a forced
// refresh scenario.
func (err BusySnapError) Pids() []int {
return err.pids
}
// hardEnsureNothingRunningDuringRefresh performs the complete hard refresh interaction.
//
// This check uses HardNothingRunningRefreshCheck along with interaction with
// two locks - the snap lock, shared by snap-confine and snapd and the snap run
// inhibition lock, shared by snapd and snap run.
//
// On success this function returns a locked snap lock, allowing the caller to
// atomically, with regards to "snap-confine", finish any action that required
// the apps and hooks not to be running. In addition, the persistent run
// inhibition lock is established, forcing snap-run to pause and postpone
// startup of applications from the given snap.
//
// In practice, we either inhibit app startup and refresh the snap _or_ inhibit
// the refresh change and continue running existing app processes.
func hardEnsureNothingRunningDuringRefresh(backend managerBackend, st *state.State, snapst *SnapState, info *snap.Info) (*osutil.FileLock, error) {
return backend.RunInhibitSnapForUnlink(info, runinhibit.HintInhibitedForRefresh, func() error {
// In case of successful refresh inhibition the snap state is modified
// to indicate when the refresh was first inhibited. If the first
// refresh inhibition is outside of a grace period then refresh
// proceeds regardless of the existing processes.
return inhibitRefresh(st, snapst, info, HardNothingRunningRefreshCheck)
})
}
// softCheckNothingRunningForRefresh checks if non-service apps are off for a snap refresh.
//
// The details of the check are explained by SoftNothingRunningRefreshCheck.
// The check is performed while holding the snap lock, which ensures that we
// are not racing with snap-confine, which is starting a new process in the
// context of the given snap.
//
// In the case that the check fails, the state is modified to reflect when the
// refresh was first postponed. Eventually the check does not fail, even if
// non-service apps are running, because this mechanism only allows postponing
// refreshes for a bounded amount of time.
func softCheckNothingRunningForRefresh(st *state.State, snapst *SnapState, info *snap.Info) error {
// Grab per-snap lock to prevent new processes from starting. This is
// sufficient to perform the check, even though individual processes may
// fork or exit, we will have per-security-tag information about what is
// running.
return backend.WithSnapLock(info, func() error {
// Perform the soft refresh viability check, possibly writing to the state
// on failure.
return inhibitRefresh(st, snapst, info, SoftNothingRunningRefreshCheck)
})
}