github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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 "sort" 25 "strings" 26 27 "github.com/snapcore/snapd/cmd/snaplock" 28 "github.com/snapcore/snapd/sandbox/cgroup" 29 "github.com/snapcore/snapd/snap" 30 ) 31 32 // pidsOfSnap is a mockable version of PidsOfSnap 33 var pidsOfSnap = cgroup.PidsOfSnap 34 35 func genericRefreshCheck(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error { 36 // Grab per-snap lock to prevent new processes from starting. This is 37 // sufficient to perform the check, even though individual processes 38 // may fork or exit, we will have per-security-tag information about 39 // what is running. 40 lock, err := snaplock.OpenLock(info.SnapName()) 41 if err != nil { 42 return err 43 } 44 // Closing the lock also unlocks it, if locked. 45 defer lock.Close() 46 if err := lock.Lock(); err != nil { 47 return err 48 } 49 knownPids, err := pidsOfSnap(info.InstanceName()) 50 if err != nil { 51 return err 52 } 53 // As soon as the lock is released the guarantee promised by pidsOfSnap is 54 // no longer true. This is an existing limitation. To cite the 55 // documentation of pidsOfSnap: 56 // 57 // > If the per-snap lock is held while computing the set, then the following 58 // > guarantee is true: If a security tag is not among the result then no such 59 // > tag can come into existence while the lock is held. 60 // 61 // This lock will be wrapped by another lock, the snap-inhibition-lock, 62 // which stalls startup of new apps and hooks. Unlike the snap-lock it can 63 // be held for many minutes or longer, enough to complete arbitrary data 64 // copy and download operations. The idea is that this refresh check will 65 // be performed while holding the snap lock (externally, the locking code 66 // will move to the call site), and if successful (the check indicated that 67 // refresh is possible) an inhibition lock will be grabbed before releasing 68 // the snap lock. This will remove the race condition and give the caller a 69 // chance to perform time-consuming operations. 70 lock.Unlock() 71 72 var busyAppNames []string 73 var busyHookNames []string 74 var busyPIDs []int 75 76 // Currently there are no situations when hooks might be allowed to run 77 // during the refresh process. The function exists to make the next two 78 // chunks of code symmetric. 79 canHookRunDuringRefresh := func(hook *snap.HookInfo) bool { 80 return false 81 } 82 83 for name, app := range info.Apps { 84 if canAppRunDuringRefresh(app) { 85 continue 86 } 87 if PIDs := knownPids[app.SecurityTag()]; len(PIDs) > 0 { 88 busyAppNames = append(busyAppNames, name) 89 busyPIDs = append(busyPIDs, PIDs...) 90 } 91 } 92 93 for name, hook := range info.Hooks { 94 if canHookRunDuringRefresh(hook) { 95 continue 96 } 97 if PIDs := knownPids[hook.SecurityTag()]; len(PIDs) > 0 { 98 busyHookNames = append(busyHookNames, name) 99 busyPIDs = append(busyPIDs, PIDs...) 100 } 101 } 102 if len(busyAppNames) == 0 && len(busyHookNames) == 0 { 103 return nil 104 } 105 sort.Strings(busyAppNames) 106 sort.Strings(busyHookNames) 107 sort.Ints(busyPIDs) 108 return &BusySnapError{ 109 SnapName: info.SnapName(), 110 busyAppNames: busyAppNames, 111 busyHookNames: busyHookNames, 112 pids: busyPIDs, 113 } 114 } 115 116 // SoftNothingRunningRefreshCheck looks if there are at most only service processes alive. 117 // 118 // The check is designed to run early in the refresh pipeline. Before 119 // downloading or stopping services for the update, we can check that only 120 // services are running, that is, that no non-service apps or hooks are 121 // currently running. 122 // 123 // Since services are stopped during the update this provides a good early 124 // precondition check. The check is also deliberately racy as existing snap 125 // commands can fork new processes or existing processes can die. After the 126 // soft check passes the user is free to start snap applications and block the 127 // hard check. 128 func SoftNothingRunningRefreshCheck(info *snap.Info) error { 129 return genericRefreshCheck(info, func(app *snap.AppInfo) bool { 130 return app.IsService() 131 }) 132 } 133 134 // HardNothingRunningRefreshCheck looks if there are any undesired processes alive. 135 // 136 // The check is designed to run late in the refresh pipeline, after stopping 137 // snap services. At this point non-enduring services should be stopped, hooks 138 // should no longer run, and applications should be barred from running 139 // externally (e.g. by using a new inhibition mechanism for snap run). 140 // 141 // The check fails if any process belonging to the snap, apart from services 142 // that are enduring refresh, is still alive. If a snap is busy it cannot be 143 // refreshed and the refresh process is aborted. 144 func HardNothingRunningRefreshCheck(info *snap.Info) error { 145 return genericRefreshCheck(info, func(app *snap.AppInfo) bool { 146 // TODO: use a constant instead of "endure" 147 return app.IsService() && app.RefreshMode == "endure" 148 }) 149 } 150 151 // BusySnapError indicates that snap has apps or hooks running and cannot refresh. 152 type BusySnapError struct { 153 SnapName string 154 pids []int 155 busyAppNames []string 156 busyHookNames []string 157 } 158 159 // Error formats an error string describing what is running. 160 func (err *BusySnapError) Error() string { 161 switch { 162 case len(err.busyAppNames) > 0 && len(err.busyHookNames) > 0: 163 return fmt.Sprintf("snap %q has running apps (%s) and hooks (%s)", 164 err.SnapName, strings.Join(err.busyAppNames, ", "), strings.Join(err.busyHookNames, ", ")) 165 case len(err.busyAppNames) > 0: 166 return fmt.Sprintf("snap %q has running apps (%s)", 167 err.SnapName, strings.Join(err.busyAppNames, ", ")) 168 case len(err.busyHookNames) > 0: 169 return fmt.Sprintf("snap %q has running hooks (%s)", 170 err.SnapName, strings.Join(err.busyHookNames, ", ")) 171 default: 172 return fmt.Sprintf("snap %q has running apps or hooks", err.SnapName) 173 } 174 } 175 176 // Pids returns the set of process identifiers that are running. 177 // 178 // Since this list is a snapshot it should be only acted upon if there is an 179 // external synchronization system applied (e.g. all processes are frozen) at 180 // the time the snapshot was taken. 181 // 182 // The list is intended for snapd to forcefully kill all processes for a forced 183 // refresh scenario. 184 func (err BusySnapError) Pids() []int { 185 return err.pids 186 }