cgroup_test.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. package worker
  2. import (
  3. "errors"
  4. "os"
  5. "os/exec"
  6. "path/filepath"
  7. "strconv"
  8. "strings"
  9. "syscall"
  10. "testing"
  11. "time"
  12. cgv1 "github.com/containerd/cgroups/v3/cgroup1"
  13. cgv2 "github.com/containerd/cgroups/v3/cgroup2"
  14. units "github.com/docker/go-units"
  15. "github.com/moby/moby/pkg/reexec"
  16. . "github.com/smartystreets/goconvey/convey"
  17. )
  18. func init() {
  19. _, testReexec := os.LookupEnv("TESTREEXEC")
  20. if !testReexec {
  21. reexec.Init()
  22. }
  23. }
  24. func TestReexec(t *testing.T) {
  25. testCase, testReexec := os.LookupEnv("TESTREEXEC")
  26. if !testReexec {
  27. return
  28. }
  29. for len(os.Args) > 1 {
  30. thisArg := os.Args[1]
  31. os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
  32. if thisArg == "--" {
  33. break
  34. }
  35. }
  36. switch testCase {
  37. case "1":
  38. Convey("Reexec should panic when command not found", t, func(ctx C) {
  39. So(func() {
  40. reexec.Init()
  41. }, ShouldPanicWith, exec.ErrNotFound)
  42. })
  43. case "2":
  44. Convey("Reexec should run when fd 3 is not open", t, func(ctx C) {
  45. So((func() error {
  46. pipe := os.NewFile(3, "pipe")
  47. if pipe == nil {
  48. return errors.New("pipe is nil")
  49. } else {
  50. _, err := pipe.Stat()
  51. return err
  52. }
  53. })(), ShouldNotBeNil)
  54. So(func() {
  55. reexec.Init()
  56. }, ShouldPanicWith, syscall.ENOEXEC)
  57. })
  58. case "3":
  59. Convey("Reexec should fail when fd 3 is sent with abrt cmd", t, func(ctx C) {
  60. So(func() {
  61. reexec.Init()
  62. }, ShouldPanicWith, "Exited on request")
  63. })
  64. case "4":
  65. Convey("Reexec should run when fd 3 is sent with cont cmd", t, func(ctx C) {
  66. So(func() {
  67. reexec.Init()
  68. }, ShouldPanicWith, syscall.ENOEXEC)
  69. })
  70. case "5":
  71. Convey("Reexec should not be triggered when argv[0] is not reexec", t, func(ctx C) {
  72. So(func() {
  73. reexec.Init()
  74. }, ShouldNotPanic)
  75. })
  76. }
  77. }
  78. func TestCgroup(t *testing.T) {
  79. var cgcf *cgroupConfig
  80. Convey("init cgroup", t, func(ctx C) {
  81. _, useCurrentCgroup := os.LookupEnv("USECURCGROUP")
  82. cgcf = &cgroupConfig{BasePath: "/sys/fs/cgroup", Group: "tunasync", Subsystem: "cpu"}
  83. if useCurrentCgroup {
  84. cgcf.Group = ""
  85. }
  86. err := initCgroup(cgcf)
  87. So(err, ShouldBeNil)
  88. if cgcf.isUnified {
  89. So(cgcf.cgMgrV2, ShouldNotBeNil)
  90. } else {
  91. So(cgcf.cgMgrV1, ShouldNotBeNil)
  92. }
  93. Convey("Cgroup Should Work", func(ctx C) {
  94. tmpDir, err := os.MkdirTemp("", "tunasync")
  95. defer os.RemoveAll(tmpDir)
  96. So(err, ShouldBeNil)
  97. cmdScript := filepath.Join(tmpDir, "cmd.sh")
  98. daemonScript := filepath.Join(tmpDir, "daemon.sh")
  99. tmpFile := filepath.Join(tmpDir, "log_file")
  100. bgPidfile := filepath.Join(tmpDir, "bg.pid")
  101. c := cmdConfig{
  102. name: "tuna-cgroup",
  103. upstreamURL: "http://mirrors.tuna.moe/",
  104. command: cmdScript + " " + daemonScript,
  105. workingDir: tmpDir,
  106. logDir: tmpDir,
  107. logFile: tmpFile,
  108. interval: 600 * time.Second,
  109. env: map[string]string{
  110. "BG_PIDFILE": bgPidfile,
  111. },
  112. }
  113. cmdScriptContent := `#!/bin/bash
  114. redirect-std() {
  115. [[ -t 0 ]] && exec </dev/null
  116. [[ -t 1 ]] && exec >/dev/null
  117. [[ -t 2 ]] && exec 2>/dev/null
  118. }
  119. # close all non-std* fds
  120. close-fds() {
  121. eval exec {3..255}\>\&-
  122. }
  123. # full daemonization of external command with setsid
  124. daemonize() {
  125. (
  126. redirect-std
  127. cd /
  128. close-fds
  129. exec setsid "$@"
  130. ) &
  131. }
  132. echo $$
  133. daemonize $@
  134. sleep 5
  135. `
  136. daemonScriptContent := `#!/bin/bash
  137. echo $$ > $BG_PIDFILE
  138. sleep 30
  139. `
  140. err = os.WriteFile(cmdScript, []byte(cmdScriptContent), 0755)
  141. So(err, ShouldBeNil)
  142. err = os.WriteFile(daemonScript, []byte(daemonScriptContent), 0755)
  143. So(err, ShouldBeNil)
  144. provider, err := newCmdProvider(c)
  145. So(err, ShouldBeNil)
  146. cg := newCgroupHook(provider, *cgcf, 0)
  147. provider.AddHook(cg)
  148. err = cg.preExec()
  149. So(err, ShouldBeNil)
  150. go func() {
  151. err := provider.Run(make(chan empty, 1))
  152. ctx.So(err, ShouldNotBeNil)
  153. }()
  154. time.Sleep(1 * time.Second)
  155. // Deamon should be started
  156. daemonPidBytes, err := os.ReadFile(bgPidfile)
  157. So(err, ShouldBeNil)
  158. daemonPid := strings.Trim(string(daemonPidBytes), " \n")
  159. logger.Debug("daemon pid: %s", daemonPid)
  160. procDir := filepath.Join("/proc", daemonPid)
  161. _, err = os.Stat(procDir)
  162. So(err, ShouldBeNil)
  163. err = provider.Terminate()
  164. So(err, ShouldBeNil)
  165. // Deamon won't be killed
  166. _, err = os.Stat(procDir)
  167. So(err, ShouldBeNil)
  168. // Deamon can be killed by cgroup killer
  169. cg.postExec()
  170. _, err = os.Stat(procDir)
  171. So(os.IsNotExist(err), ShouldBeTrue)
  172. })
  173. Convey("Rsync Memory Should Be Limited", func() {
  174. tmpDir, err := os.MkdirTemp("", "tunasync")
  175. defer os.RemoveAll(tmpDir)
  176. So(err, ShouldBeNil)
  177. scriptFile := filepath.Join(tmpDir, "myrsync")
  178. tmpFile := filepath.Join(tmpDir, "log_file")
  179. c := rsyncConfig{
  180. name: "tuna-cgroup",
  181. upstreamURL: "rsync://rsync.tuna.moe/tuna/",
  182. rsyncCmd: scriptFile,
  183. workingDir: tmpDir,
  184. logDir: tmpDir,
  185. logFile: tmpFile,
  186. useIPv6: true,
  187. interval: 600 * time.Second,
  188. }
  189. provider, err := newRsyncProvider(c)
  190. So(err, ShouldBeNil)
  191. cg := newCgroupHook(provider, *cgcf, 512*units.MiB)
  192. provider.AddHook(cg)
  193. err = cg.preExec()
  194. So(err, ShouldBeNil)
  195. if cgcf.isUnified {
  196. cgpath := filepath.Join(cgcf.BasePath, cgcf.Group, provider.Name())
  197. if useCurrentCgroup {
  198. group, err := cgv2.NestedGroupPath(filepath.Join("..", provider.Name()))
  199. So(err, ShouldBeNil)
  200. cgpath = filepath.Join(cgcf.BasePath, group)
  201. }
  202. memoLimit, err := os.ReadFile(filepath.Join(cgpath, "memory.max"))
  203. So(err, ShouldBeNil)
  204. So(strings.Trim(string(memoLimit), "\n"), ShouldEqual, strconv.Itoa(512*1024*1024))
  205. } else {
  206. for _, subsys := range cg.cgMgrV1.Subsystems() {
  207. if subsys.Name() == cgv1.Memory {
  208. cgpath := filepath.Join(cgcf.Group, provider.Name())
  209. if useCurrentCgroup {
  210. p, err := cgv1.NestedPath(filepath.Join("..", provider.Name()))(cgv1.Memory)
  211. So(err, ShouldBeNil)
  212. cgpath = p
  213. }
  214. memoLimit, err := os.ReadFile(filepath.Join(cgcf.BasePath, "memory", cgpath, "memory.limit_in_bytes"))
  215. So(err, ShouldBeNil)
  216. So(strings.Trim(string(memoLimit), "\n"), ShouldEqual, strconv.Itoa(512*1024*1024))
  217. }
  218. }
  219. }
  220. cg.postExec()
  221. So(cg.cgMgrV1, ShouldBeNil)
  222. })
  223. Reset(func() {
  224. if cgcf.isUnified {
  225. if cgcf.Group == "" {
  226. wkrg, err := cgv2.NestedGroupPath("")
  227. So(err, ShouldBeNil)
  228. wkrMgr, err := cgv2.Load(wkrg)
  229. allCtrls, err := wkrMgr.Controllers()
  230. So(err, ShouldBeNil)
  231. err = wkrMgr.ToggleControllers(allCtrls, cgv2.Disable)
  232. So(err, ShouldBeNil)
  233. origMgr := cgcf.cgMgrV2
  234. for {
  235. logger.Debugf("Restoring pids")
  236. procs, err := wkrMgr.Procs(false)
  237. So(err, ShouldBeNil)
  238. if len(procs) == 0 {
  239. break
  240. }
  241. for _, p := range procs {
  242. if err := origMgr.AddProc(p); err != nil {
  243. if errors.Is(err, syscall.ESRCH) {
  244. logger.Debugf("Write pid %d to sub group failed: process vanished, ignoring")
  245. } else {
  246. So(err, ShouldBeNil)
  247. }
  248. }
  249. }
  250. }
  251. err = wkrMgr.Delete()
  252. So(err, ShouldBeNil)
  253. }
  254. } else {
  255. if cgcf.Group == "" {
  256. pather := (func(p cgv1.Path) cgv1.Path {
  257. return func(subsys cgv1.Name) (string, error) {
  258. path, err := p(subsys)
  259. if err != nil {
  260. return "", err
  261. }
  262. if path == "/" {
  263. return "", cgv1.ErrControllerNotActive
  264. }
  265. return path, err
  266. }
  267. })(cgv1.NestedPath(""))
  268. wkrMgr, err := cgv1.Load(pather, func(cfg *cgv1.InitConfig) error {
  269. cfg.InitCheck = cgv1.AllowAny
  270. return nil
  271. })
  272. So(err, ShouldBeNil)
  273. origMgr := cgcf.cgMgrV1
  274. for _, subsys := range wkrMgr.Subsystems() {
  275. for {
  276. procs, err := wkrMgr.Processes(subsys.Name(), false)
  277. So(err, ShouldBeNil)
  278. if len(procs) == 0 {
  279. break
  280. }
  281. for _, proc := range procs {
  282. if err := origMgr.Add(proc); err != nil {
  283. if errors.Is(err, syscall.ESRCH) {
  284. logger.Debugf("Write pid %d to sub group failed: process vanished, ignoring")
  285. } else {
  286. So(err, ShouldBeNil)
  287. }
  288. }
  289. }
  290. }
  291. }
  292. err = wkrMgr.Delete()
  293. So(err, ShouldBeNil)
  294. }
  295. }
  296. })
  297. })
  298. }