github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/snapstate/refresh.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019 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 snapstate 21 22 import ( 23 "fmt" 24 "path/filepath" 25 "sort" 26 "strings" 27 28 "github.com/snapcore/snapd/cmd/snaplock/runinhibit" 29 "github.com/snapcore/snapd/osutil" 30 "github.com/snapcore/snapd/overlord/snapstate/backend" 31 "github.com/snapcore/snapd/overlord/state" 32 "github.com/snapcore/snapd/sandbox/cgroup" 33 "github.com/snapcore/snapd/snap" 34 userclient "github.com/snapcore/snapd/usersession/client" 35 ) 36 37 // pidsOfSnap is a mockable version of PidsOfSnap 38 var pidsOfSnap = cgroup.PidsOfSnap 39 40 var genericRefreshCheck = func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error { 41 knownPids, err := pidsOfSnap(info.InstanceName()) 42 if err != nil { 43 return err 44 } 45 46 // Due to specific of the interaction with locking, all locking is performed by the caller. 47 var busyAppNames []string 48 var busyHookNames []string 49 var busyPIDs []int 50 51 // Currently there are no situations when hooks might be allowed to run 52 // during the refresh process. The function exists to make the next two 53 // chunks of code symmetric. 54 canHookRunDuringRefresh := func(hook *snap.HookInfo) bool { 55 return false 56 } 57 58 for name, app := range info.Apps { 59 if canAppRunDuringRefresh(app) { 60 continue 61 } 62 if PIDs := knownPids[app.SecurityTag()]; len(PIDs) > 0 { 63 busyAppNames = append(busyAppNames, name) 64 busyPIDs = append(busyPIDs, PIDs...) 65 } 66 } 67 68 for name, hook := range info.Hooks { 69 if canHookRunDuringRefresh(hook) { 70 continue 71 } 72 if PIDs := knownPids[hook.SecurityTag()]; len(PIDs) > 0 { 73 busyHookNames = append(busyHookNames, name) 74 busyPIDs = append(busyPIDs, PIDs...) 75 } 76 } 77 if len(busyAppNames) == 0 && len(busyHookNames) == 0 { 78 return nil 79 } 80 sort.Strings(busyAppNames) 81 sort.Strings(busyHookNames) 82 sort.Ints(busyPIDs) 83 return &BusySnapError{ 84 SnapInfo: info, 85 busyAppNames: busyAppNames, 86 busyHookNames: busyHookNames, 87 pids: busyPIDs, 88 } 89 } 90 91 // SoftNothingRunningRefreshCheck looks if there are at most only service processes alive. 92 // 93 // The check is designed to run early in the refresh pipeline. Before 94 // downloading or stopping services for the update, we can check that only 95 // services are running, that is, that no non-service apps or hooks are 96 // currently running. 97 // 98 // Since services are stopped during the update this provides a good early 99 // precondition check. The check is also deliberately racy as existing snap 100 // commands can fork new processes or existing processes can die. After the 101 // soft check passes the user is free to start snap applications and block the 102 // hard check. 103 func SoftNothingRunningRefreshCheck(info *snap.Info) error { 104 return genericRefreshCheck(info, func(app *snap.AppInfo) bool { 105 return app.IsService() 106 }) 107 } 108 109 // HardNothingRunningRefreshCheck looks if there are any undesired processes alive. 110 // 111 // The check is designed to run late in the refresh pipeline, after stopping 112 // snap services. At this point non-enduring services should be stopped, hooks 113 // should no longer run, and applications should be barred from running 114 // externally (e.g. by using a new inhibition mechanism for snap run). 115 // 116 // The check fails if any process belonging to the snap, apart from services 117 // that are enduring refresh, is still alive. If a snap is busy it cannot be 118 // refreshed and the refresh process is aborted. 119 func HardNothingRunningRefreshCheck(info *snap.Info) error { 120 return genericRefreshCheck(info, func(app *snap.AppInfo) bool { 121 // TODO: use a constant instead of "endure" 122 return app.IsService() && app.RefreshMode == "endure" 123 }) 124 } 125 126 // BusySnapError indicates that snap has apps or hooks running and cannot refresh. 127 type BusySnapError struct { 128 SnapInfo *snap.Info 129 pids []int 130 busyAppNames []string 131 busyHookNames []string 132 } 133 134 // PendingSnapRefreshInfo computes information necessary to perform user notification 135 // of postponed refresh of a snap, based on the information about snap "business". 136 // 137 // The returned value contains the instance name of the snap as well as, if possible, 138 // information relevant for desktop notification services, such as application name 139 // and the snapd-generated desktop file name. 140 func (err *BusySnapError) PendingSnapRefreshInfo() *userclient.PendingSnapRefreshInfo { 141 refreshInfo := &userclient.PendingSnapRefreshInfo{ 142 InstanceName: err.SnapInfo.InstanceName(), 143 } 144 for _, appName := range err.busyAppNames { 145 if app, ok := err.SnapInfo.Apps[appName]; ok { 146 path := app.DesktopFile() 147 if osutil.FileExists(path) { 148 refreshInfo.BusyAppName = appName 149 refreshInfo.BusyAppDesktopEntry = strings.SplitN(filepath.Base(path), ".", 2)[0] 150 break 151 } 152 } 153 } 154 return refreshInfo 155 } 156 157 // Error formats an error string describing what is running. 158 func (err *BusySnapError) Error() string { 159 switch { 160 case len(err.busyAppNames) > 0 && len(err.busyHookNames) > 0: 161 return fmt.Sprintf("snap %q has running apps (%s) and hooks (%s)", 162 err.SnapInfo.InstanceName(), strings.Join(err.busyAppNames, ", "), strings.Join(err.busyHookNames, ", ")) 163 case len(err.busyAppNames) > 0: 164 return fmt.Sprintf("snap %q has running apps (%s)", 165 err.SnapInfo.InstanceName(), strings.Join(err.busyAppNames, ", ")) 166 case len(err.busyHookNames) > 0: 167 return fmt.Sprintf("snap %q has running hooks (%s)", 168 err.SnapInfo.InstanceName(), strings.Join(err.busyHookNames, ", ")) 169 default: 170 return fmt.Sprintf("snap %q has running apps or hooks", err.SnapInfo.InstanceName()) 171 } 172 } 173 174 // Pids returns the set of process identifiers that are running. 175 // 176 // Since this list is a snapshot it should be only acted upon if there is an 177 // external synchronization system applied (e.g. all processes are frozen) at 178 // the time the snapshot was taken. 179 // 180 // The list is intended for snapd to forcefully kill all processes for a forced 181 // refresh scenario. 182 func (err BusySnapError) Pids() []int { 183 return err.pids 184 } 185 186 // hardEnsureNothingRunningDuringRefresh performs the complete hard refresh interaction. 187 // 188 // This check uses HardNothingRunningRefreshCheck along with interaction with 189 // two locks - the snap lock, shared by snap-confine and snapd and the snap run 190 // inhibition lock, shared by snapd and snap run. 191 // 192 // On success this function returns a locked snap lock, allowing the caller to 193 // atomically, with regards to "snap-confine", finish any action that required 194 // the apps and hooks not to be running. In addition, the persistent run 195 // inhibition lock is established, forcing snap-run to pause and postpone 196 // startup of applications from the given snap. 197 // 198 // In practice, we either inhibit app startup and refresh the snap _or_ inhibit 199 // the refresh change and continue running existing app processes. 200 func hardEnsureNothingRunningDuringRefresh(backend managerBackend, st *state.State, snapst *SnapState, info *snap.Info) (*osutil.FileLock, error) { 201 return backend.RunInhibitSnapForUnlink(info, runinhibit.HintInhibitedForRefresh, func() error { 202 // In case of successful refresh inhibition the snap state is modified 203 // to indicate when the refresh was first inhibited. If the first 204 // refresh inhibition is outside of a grace period then refresh 205 // proceeds regardless of the existing processes. 206 return inhibitRefresh(st, snapst, info, HardNothingRunningRefreshCheck) 207 }) 208 } 209 210 // softCheckNothingRunningForRefresh checks if non-service apps are off for a snap refresh. 211 // 212 // The details of the check are explained by SoftNothingRunningRefreshCheck. 213 // The check is performed while holding the snap lock, which ensures that we 214 // are not racing with snap-confine, which is starting a new process in the 215 // context of the given snap. 216 // 217 // In the case that the check fails, the state is modified to reflect when the 218 // refresh was first postponed. Eventually the check does not fail, even if 219 // non-service apps are running, because this mechanism only allows postponing 220 // refreshes for a bounded amount of time. 221 func softCheckNothingRunningForRefresh(st *state.State, snapst *SnapState, info *snap.Info) error { 222 // Grab per-snap lock to prevent new processes from starting. This is 223 // sufficient to perform the check, even though individual processes may 224 // fork or exit, we will have per-security-tag information about what is 225 // running. 226 return backend.WithSnapLock(info, func() error { 227 // Perform the soft refresh viability check, possibly writing to the state 228 // on failure. 229 return inhibitRefresh(st, snapst, info, SoftNothingRunningRefreshCheck) 230 }) 231 }