gitee.com/mysnapcore/mysnapd@v0.1.0/sandbox/cgroup/freezer.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 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 cgroup
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  	"time"
    30  
    31  	"gitee.com/mysnapcore/mysnapd/dirs"
    32  	"gitee.com/mysnapcore/mysnapd/logger"
    33  	"gitee.com/mysnapcore/mysnapd/osutil"
    34  )
    35  
    36  const defaultFreezerCgroupV1Dir = "/sys/fs/cgroup/freezer"
    37  
    38  var freezerCgroupV1Dir = defaultFreezerCgroupV1Dir
    39  
    40  func init() {
    41  	dirs.AddRootDirCallback(func(root string) {
    42  		freezerCgroupV1Dir = filepath.Join(root, defaultFreezerCgroupV1Dir)
    43  	})
    44  }
    45  
    46  func pickFreezerV1Impl() {
    47  	FreezeSnapProcesses = freezeSnapProcessesImplV1
    48  	ThawSnapProcesses = thawSnapProcessesImplV1
    49  }
    50  
    51  func pickFreezerV2Impl() {
    52  	FreezeSnapProcesses = freezeSnapProcessesImplV2
    53  	ThawSnapProcesses = thawSnapProcessesImplV2
    54  }
    55  
    56  // FreezeSnapProcesses suspends execution of all the processes belonging to
    57  // a given snap. Processes remain frozen until ThawSnapProcesses is called,
    58  // care must be taken not to freezer processes indefinitely.
    59  //
    60  // The freeze operation is not instant. Once commenced it proceeds
    61  // asynchronously. Internally the function waits for the freezing to complete
    62  // in at most 3000ms. If this time is insufficient then the processes are
    63  // thawed and an error is returned.
    64  //
    65  // A correct implementation is picked depending on cgroup v1 or v2 use in the
    66  // system. When cgroup v1 is detected, the call will directly act on the freezer
    67  // group created when a snap process was started, while with v2 the call will
    68  // act on all tracking groups of a snap.
    69  //
    70  // This operation can be mocked with MockFreezing
    71  var FreezeSnapProcesses = freezeSnapProcessesImplV1
    72  
    73  // ThawSnapProcesses resumes execution of all processes belonging to a given snap.
    74  //
    75  // A correct implementation is picked depending on cgroup v1 or v2 use in the
    76  // system. When cgroup v1 is detected, the call will directly act on the freezer
    77  // group created when a snap process was started, while with v2 the call will
    78  // act on all tracking groups of a snap.
    79  //
    80  // This operation can be mocked with MockFreezing
    81  var ThawSnapProcesses = thawSnapProcessesImplV1
    82  
    83  // freezeSnapProcessesImplV1 freezes all the processes originating from the given snap.
    84  // Processes are frozen regardless of which particular snap application they
    85  // originate from.
    86  func freezeSnapProcessesImplV1(snapName string) error {
    87  	fname := filepath.Join(freezerCgroupV1Dir, fmt.Sprintf("snap.%s", snapName), "freezer.state")
    88  	if err := ioutil.WriteFile(fname, []byte("FROZEN"), 0644); err != nil && os.IsNotExist(err) {
    89  		// When there's no freezer cgroup we don't have to freeze anything.
    90  		// This can happen when no process belonging to a given snap has been
    91  		// started yet.
    92  		return nil
    93  	} else if err != nil {
    94  		return fmt.Errorf("cannot freeze processes of snap %q, %v", snapName, err)
    95  	}
    96  	for i := 0; i < 30; i++ {
    97  		data, err := ioutil.ReadFile(fname)
    98  		if err != nil {
    99  			return fmt.Errorf("cannot determine the freeze state of processes of snap %q, %v", snapName, err)
   100  		}
   101  		// If the cgroup is still freezing then wait a moment and try again.
   102  		if bytes.Equal(data, []byte("FREEZING")) {
   103  			time.Sleep(100 * time.Millisecond)
   104  			continue
   105  		}
   106  		return nil
   107  	}
   108  	// If we got here then we timed out after seeing FREEZING for too long.
   109  	ThawSnapProcesses(snapName) // ignore the error, this is best-effort.
   110  	return fmt.Errorf("cannot finish freezing processes of snap %q", snapName)
   111  }
   112  
   113  func thawSnapProcessesImplV1(snapName string) error {
   114  	fname := filepath.Join(freezerCgroupV1Dir, fmt.Sprintf("snap.%s", snapName), "freezer.state")
   115  	if err := ioutil.WriteFile(fname, []byte("THAWED"), 0644); err != nil && os.IsNotExist(err) {
   116  		// When there's no freezer cgroup we don't have to thaw anything.
   117  		// This can happen when no process belonging to a given snap has been
   118  		// started yet.
   119  		return nil
   120  	} else if err != nil {
   121  		return fmt.Errorf("cannot thaw processes of snap %q", snapName)
   122  	}
   123  	return nil
   124  }
   125  
   126  func applyToSnap(snapName string, action func(groupName string) error, skipError func(err error) bool) error {
   127  	if action == nil {
   128  		return fmt.Errorf("internal error: action is nil")
   129  	}
   130  	if skipError == nil {
   131  		return fmt.Errorf("internal error: skip error is nil")
   132  	}
   133  	canary := fmt.Sprintf("snap.%s.", snapName)
   134  	cgroupRoot := filepath.Join(rootPath, cgroupMountPoint)
   135  	if _, dir, _ := osutil.DirExists(cgroupRoot); !dir {
   136  		return nil
   137  	}
   138  	return filepath.Walk(filepath.Join(rootPath, cgroupMountPoint), func(name string, info os.FileInfo, err error) error {
   139  		if err != nil {
   140  			if skipError(err) {
   141  				// we don't know whether it's a file or
   142  				// directory, so just return nil instead
   143  				return nil
   144  			}
   145  			return err
   146  		}
   147  		if !info.IsDir() {
   148  			return nil
   149  		}
   150  		if !strings.HasPrefix(info.Name(), canary) {
   151  			return nil
   152  		}
   153  		// snap applications end up inside a cgroup related to a
   154  		// service, or when ran standalone, a scope
   155  		if ext := filepath.Ext(info.Name()); ext != ".scope" && ext != ".service" {
   156  			return nil
   157  		}
   158  		// found a group
   159  		if err := action(name); err != nil && !skipError(err) {
   160  			return err
   161  		}
   162  		return filepath.SkipDir
   163  	})
   164  }
   165  
   166  // writeExistingFile can be used as a drop-in replacement for ioutil.WriteFile,
   167  // but does not create a file when it does not exist
   168  func writeExistingFile(where string, data []byte, mode os.FileMode) error {
   169  	f, err := os.OpenFile(where, os.O_WRONLY|os.O_TRUNC, mode)
   170  	if err != nil {
   171  		return err
   172  	}
   173  	_, errW := f.Write(data)
   174  	errC := f.Close()
   175  	// pick the right error
   176  	if errW != nil {
   177  		return errW
   178  	}
   179  	return errC
   180  }
   181  
   182  // freezeSnapProcessesImplV2 freezes all the processes originating from the
   183  // given snap. Processes are frozen regardless of which particular snap
   184  // application they originate from.
   185  func freezeSnapProcessesImplV2(snapName string) error {
   186  	// in case of v2, the process calling this code, (eg. snap-update-ns)
   187  	// may already be part of the trackign cgroup for particular snap, care
   188  	// must be taken to not freeze ourselves
   189  	ownGroup, err := cgroupProcessPathInTrackingCgroup(os.Getpid())
   190  	if err != nil {
   191  		return err
   192  	}
   193  	ownGroupDir := filepath.Join(rootPath, cgroupMountPoint, ownGroup)
   194  	freezeOne := func(dir string) error {
   195  		if dir == ownGroupDir {
   196  			// let's not freeze ourselves
   197  			logger.Debugf("freeze, skipping own group %v", dir)
   198  			return nil
   199  		}
   200  		fname := filepath.Join(dir, "cgroup.freeze")
   201  		if err := writeExistingFile(fname, []byte("1"), 0644); err != nil {
   202  			if os.IsNotExist(err) {
   203  				//  the group may be gone already
   204  				return nil
   205  			}
   206  			return fmt.Errorf("cannot freeze processes of snap %q, %v", snapName, err)
   207  		}
   208  		for i := 0; i < 30; i++ {
   209  			data, err := ioutil.ReadFile(fname)
   210  			if err != nil {
   211  				if os.IsNotExist(err) {
   212  					// group may be gone
   213  					return nil
   214  				}
   215  				return fmt.Errorf("cannot determine the freeze state of processes of snap %q, %v", snapName, err)
   216  			}
   217  			// If the cgroup is still freezing then wait a moment and try again.
   218  			if bytes.Equal(bytes.TrimSpace(data), []byte("1")) {
   219  				// we're done
   220  				return nil
   221  			}
   222  			// add a bit of delay
   223  			time.Sleep(100 * time.Millisecond)
   224  		}
   225  		return fmt.Errorf("cannot freeze processes of snap %q in group %v", snapName, filepath.Base(dir))
   226  	}
   227  	// freeze, skipping ENOENT errors
   228  	err = applyToSnap(snapName, freezeOne, os.IsNotExist)
   229  	if err == nil {
   230  		return nil
   231  	}
   232  	// we either got here because we hit a timeout freezing snap processes
   233  	// or some other error
   234  
   235  	// ignore errors when thawing processes, this is best-effort.
   236  	alwaysSkipError := func(_ error) bool { return true }
   237  	thawSnapProcessesV2(snapName, alwaysSkipError)
   238  	return fmt.Errorf("cannot finish freezing processes of snap %q: %v", snapName, err)
   239  }
   240  
   241  func thawSnapProcessesV2(snapName string, skipError func(error) bool) error {
   242  	if skipError == nil {
   243  		return fmt.Errorf("internal error: skip error is nil")
   244  	}
   245  	thawOne := func(dir string) error {
   246  		fname := filepath.Join(dir, "cgroup.freeze")
   247  		if err := writeExistingFile(fname, []byte("0"), 0644); err != nil && os.IsNotExist(err) {
   248  			//  the group may be gone already
   249  			return nil
   250  		} else if err != nil && !skipError(err) {
   251  			return fmt.Errorf("cannot thaw processes of snap %q, %v", snapName, err)
   252  		}
   253  		return nil
   254  	}
   255  	return applyToSnap(snapName, thawOne, skipError)
   256  }
   257  
   258  func thawSnapProcessesImplV2(snapName string) error {
   259  	// thaw skipping ENOENT errors
   260  	return thawSnapProcessesV2(snapName, os.IsNotExist)
   261  }
   262  
   263  // MockFreezing replaces the real implementation of freeze and thaw.
   264  func MockFreezing(freeze, thaw func(snapName string) error) (restore func()) {
   265  	oldFreeze := FreezeSnapProcesses
   266  	oldThaw := ThawSnapProcesses
   267  
   268  	FreezeSnapProcesses = freeze
   269  	ThawSnapProcesses = thaw
   270  
   271  	return func() {
   272  		FreezeSnapProcesses = oldFreeze
   273  		ThawSnapProcesses = oldThaw
   274  	}
   275  }