aboutsummaryrefslogtreecommitdiff
path: root/vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs2/freezer.go
blob: 8917a6411d68ef5b36877c233f939f095656f912 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package fs2

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"strings"
	"time"

	"golang.org/x/sys/unix"

	"github.com/opencontainers/runc/libcontainer/cgroups"
	"github.com/opencontainers/runc/libcontainer/configs"
)

func setFreezer(dirPath string, state configs.FreezerState) error {
	var stateStr string
	switch state {
	case configs.Undefined:
		return nil
	case configs.Frozen:
		stateStr = "1"
	case configs.Thawed:
		stateStr = "0"
	default:
		return fmt.Errorf("invalid freezer state %q requested", state)
	}

	fd, err := cgroups.OpenFile(dirPath, "cgroup.freeze", unix.O_RDWR)
	if err != nil {
		// We can ignore this request as long as the user didn't ask us to
		// freeze the container (since without the freezer cgroup, that's a
		// no-op).
		if state != configs.Frozen {
			return nil
		}
		return fmt.Errorf("freezer not supported: %w", err)
	}
	defer fd.Close()

	if _, err := fd.WriteString(stateStr); err != nil {
		return err
	}
	// Confirm that the cgroup did actually change states.
	if actualState, err := readFreezer(dirPath, fd); err != nil {
		return err
	} else if actualState != state {
		return fmt.Errorf(`expected "cgroup.freeze" to be in state %q but was in %q`, state, actualState)
	}
	return nil
}

func getFreezer(dirPath string) (configs.FreezerState, error) {
	fd, err := cgroups.OpenFile(dirPath, "cgroup.freeze", unix.O_RDONLY)
	if err != nil {
		// If the kernel is too old, then we just treat the freezer as being in
		// an "undefined" state.
		if os.IsNotExist(err) || errors.Is(err, unix.ENODEV) {
			err = nil
		}
		return configs.Undefined, err
	}
	defer fd.Close()

	return readFreezer(dirPath, fd)
}

func readFreezer(dirPath string, fd *os.File) (configs.FreezerState, error) {
	if _, err := fd.Seek(0, 0); err != nil {
		return configs.Undefined, err
	}
	state := make([]byte, 2)
	if _, err := fd.Read(state); err != nil {
		return configs.Undefined, err
	}
	switch string(state) {
	case "0\n":
		return configs.Thawed, nil
	case "1\n":
		return waitFrozen(dirPath)
	default:
		return configs.Undefined, fmt.Errorf(`unknown "cgroup.freeze" state: %q`, state)
	}
}

// waitFrozen polls cgroup.events until it sees "frozen 1" in it.
func waitFrozen(dirPath string) (configs.FreezerState, error) {
	fd, err := cgroups.OpenFile(dirPath, "cgroup.events", unix.O_RDONLY)
	if err != nil {
		return configs.Undefined, err
	}
	defer fd.Close()

	// XXX: Simple wait/read/retry is used here. An implementation
	// based on poll(2) or inotify(7) is possible, but it makes the code
	// much more complicated. Maybe address this later.
	const (
		// Perform maxIter with waitTime in between iterations.
		waitTime = 10 * time.Millisecond
		maxIter  = 1000
	)
	scanner := bufio.NewScanner(fd)
	for i := 0; scanner.Scan(); {
		if i == maxIter {
			return configs.Undefined, fmt.Errorf("timeout of %s reached waiting for the cgroup to freeze", waitTime*maxIter)
		}
		line := scanner.Text()
		val := strings.TrimPrefix(line, "frozen ")
		if val != line { // got prefix
			if val[0] == '1' {
				return configs.Frozen, nil
			}

			i++
			// wait, then re-read
			time.Sleep(waitTime)
			_, err := fd.Seek(0, 0)
			if err != nil {
				return configs.Undefined, err
			}
		}
	}
	// Should only reach here either on read error,
	// or if the file does not contain "frozen " line.
	return configs.Undefined, scanner.Err()
}