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  }