github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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 "bufio" 24 "fmt" 25 "io" 26 "os" 27 "path/filepath" 28 "sort" 29 "strconv" 30 "strings" 31 32 "github.com/snapcore/snapd/cmd/snaplock" 33 "github.com/snapcore/snapd/dirs" 34 "github.com/snapcore/snapd/snap" 35 ) 36 37 func genericRefreshCheck(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error { 38 // Grab per-snap lock to prevent new processes from starting. This is 39 // sufficient to perform the check, even though individual processes 40 // may fork or exit, we will have per-security-tag information about 41 // what is running. 42 lock, err := snaplock.OpenLock(info.SnapName()) 43 if err != nil { 44 return err 45 } 46 // Closing the lock also unlocks it, if locked. 47 defer lock.Close() 48 if err := lock.Lock(); err != nil { 49 return err 50 } 51 52 var busyAppNames []string 53 var busyHookNames []string 54 var busyPIDs []int 55 56 // Currently there are no situations when hooks might be allowed to run 57 // during the refresh process. The function exists to make the next two 58 // chunks of code symmetric. 59 canHookRunDuringRefresh := func(hook *snap.HookInfo) bool { 60 return false 61 } 62 63 for name, app := range info.Apps { 64 if canAppRunDuringRefresh(app) { 65 continue 66 } 67 PIDs, err := pidsOfSecurityTag(app.SecurityTag()) 68 if err != nil { 69 return err 70 } 71 if len(PIDs) > 0 { 72 busyAppNames = append(busyAppNames, name) 73 busyPIDs = append(busyPIDs, PIDs...) 74 } 75 } 76 77 for name, hook := range info.Hooks { 78 if canHookRunDuringRefresh(hook) { 79 continue 80 } 81 PIDs, err := pidsOfSecurityTag(hook.SecurityTag()) 82 if err != nil { 83 return err 84 } 85 if len(PIDs) > 0 { 86 busyHookNames = append(busyHookNames, name) 87 busyPIDs = append(busyPIDs, PIDs...) 88 } 89 } 90 if len(busyAppNames) == 0 && len(busyHookNames) == 0 { 91 return nil 92 } 93 sort.Strings(busyAppNames) 94 sort.Strings(busyHookNames) 95 sort.Ints(busyPIDs) 96 return &BusySnapError{ 97 snapName: info.SnapName(), 98 busyAppNames: busyAppNames, 99 busyHookNames: busyHookNames, 100 pids: busyPIDs, 101 } 102 } 103 104 // SoftNothingRunningRefreshCheck looks if there are at most only service processes alive. 105 // 106 // The check is designed to run early in the refresh pipeline. Before 107 // downloading or stopping services for the update, we can check that only 108 // services are running, that is, that no non-service apps or hooks are 109 // currently running. 110 // 111 // Since services are stopped during the update this provides a good early 112 // precondition check. The check is also deliberately racy as existing snap 113 // commands can fork new processes or existing processes can die. After the 114 // soft check passes the user is free to start snap applications and block the 115 // hard check. 116 func SoftNothingRunningRefreshCheck(info *snap.Info) error { 117 return genericRefreshCheck(info, func(app *snap.AppInfo) bool { 118 return app.IsService() 119 }) 120 } 121 122 // HardNothingRunningRefreshCheck looks if there are any undesired processes alive. 123 // 124 // The check is designed to run late in the refresh pipeline, after stopping 125 // snap services. At this point non-enduring services should be stopped, hooks 126 // should no longer run, and applications should be barred from running 127 // externally (e.g. by using a new inhibition mechanism for snap run). 128 // 129 // The check fails if any process belonging to the snap, apart from services 130 // that are enduring refresh, is still alive. If a snap is busy it cannot be 131 // refreshed and the refresh process is aborted. 132 func HardNothingRunningRefreshCheck(info *snap.Info) error { 133 return genericRefreshCheck(info, func(app *snap.AppInfo) bool { 134 // TODO: use a constant instead of "endure" 135 return app.IsService() && app.RefreshMode == "endure" 136 }) 137 } 138 139 // BusySnapError indicates that snap has apps or hooks running and cannot refresh. 140 type BusySnapError struct { 141 snapName string 142 pids []int 143 busyAppNames []string 144 busyHookNames []string 145 } 146 147 // Error formats an error string describing what is running. 148 func (err *BusySnapError) Error() string { 149 switch { 150 case len(err.busyAppNames) > 0 && len(err.busyHookNames) > 0: 151 return fmt.Sprintf("snap %q has running apps (%s) and hooks (%s)", 152 err.snapName, strings.Join(err.busyAppNames, ", "), strings.Join(err.busyHookNames, ", ")) 153 case len(err.busyAppNames) > 0: 154 return fmt.Sprintf("snap %q has running apps (%s)", 155 err.snapName, strings.Join(err.busyAppNames, ", ")) 156 case len(err.busyHookNames) > 0: 157 return fmt.Sprintf("snap %q has running hooks (%s)", 158 err.snapName, strings.Join(err.busyHookNames, ", ")) 159 default: 160 return fmt.Sprintf("snap %q has running apps or hooks", err.snapName) 161 } 162 } 163 164 // Pids returns the set of process identifiers that are running. 165 // 166 // Since this list is a snapshot it should be only acted upon if there is an 167 // external synchronization system applied (e.g. all processes are frozen) at 168 // the time the snapshot was taken. 169 // 170 // The list is intended for snapd to forcefully kill all processes for a forced 171 // refresh scenario. 172 func (err BusySnapError) Pids() []int { 173 return err.pids 174 } 175 176 // parsePid parses a string as a process identifier. 177 func parsePid(text string) (int, error) { 178 pid, err := strconv.Atoi(text) 179 if err == nil && pid <= 0 { 180 return 0, fmt.Errorf("cannot parse pid %q", text) 181 } 182 return pid, err 183 } 184 185 // parsePids parses a list of pids, one per line, from a reader. 186 func parsePids(reader io.Reader) ([]int, error) { 187 scanner := bufio.NewScanner(reader) 188 var pids []int 189 for scanner.Scan() { 190 s := scanner.Text() 191 pid, err := parsePid(s) 192 if err != nil { 193 return nil, err 194 } 195 pids = append(pids, pid) 196 } 197 if err := scanner.Err(); err != nil { 198 return nil, err 199 } 200 return pids, nil 201 } 202 203 // pidsOfSecurityTag returns a list of PIDs belonging to a given security tag. 204 // 205 // The list is obtained from a pids cgroup. 206 func pidsOfSecurityTag(securityTag string) ([]int, error) { 207 fname := filepath.Join(dirs.PidsCgroupDir, securityTag, "cgroup.procs") 208 file, err := os.Open(fname) 209 if os.IsNotExist(err) { 210 return nil, nil 211 } 212 if err != nil { 213 return nil, err 214 } 215 defer file.Close() 216 return parsePids(bufio.NewReader(file)) 217 }