diff --git a/internal/dirnotifier/dirnotifier.go b/internal/dirnotifier/dirnotifier.go deleted file mode 100644 index c86df8d84fa..00000000000 --- a/internal/dirnotifier/dirnotifier.go +++ /dev/null @@ -1,61 +0,0 @@ -package dirnotifier - -import ( - "sync" - - "github.com/fsnotify/fsnotify" - "github.com/pkg/errors" -) - -type DirectoryNotifier struct { - watcher *fsnotify.Watcher - filesToNotifiers sync.Map - opsToWatch []fsnotify.Op - dir string -} - -func New(dir string, opsToWatch ...fsnotify.Op) (*DirectoryNotifier, error) { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return nil, err - } - - dn := &DirectoryNotifier{ - watcher: watcher, - opsToWatch: opsToWatch, - dir: dir, - } - - go dn.initializeWatcher() - - if err := watcher.Add(dir); err != nil { - return nil, err - } - return dn, nil -} - -func (dn *DirectoryNotifier) initializeWatcher() { - defer dn.watcher.Close() - for event := range dn.watcher.Events { - for op := range dn.opsToWatch { - if event.Op&fsnotify.Op(op) == fsnotify.Op(op) { - if notifyChan, ok := dn.filesToNotifiers.LoadAndDelete(event.Name); ok { - close(notifyChan.(chan struct{})) - break - } - } - } - } -} - -func (dn *DirectoryNotifier) NotifierForFile(file string) (chan struct{}, error) { - c := make(chan struct{}, 1) - if _, ok := dn.filesToNotifiers.LoadOrStore(file, c); ok { - return nil, errors.Errorf("exec watcher already watching file %s", file) - } - return c, nil -} - -func (dn *DirectoryNotifier) Directory() string { - return dn.dir -} diff --git a/internal/dirnotifier/dirnotifier_test.go b/internal/dirnotifier/dirnotifier_test.go deleted file mode 100644 index f65bfb44fc3..00000000000 --- a/internal/dirnotifier/dirnotifier_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package dirnotifier_test - -import ( - "os" - "path/filepath" - - "github.com/cri-o/cri-o/internal/dirnotifier" - "github.com/fsnotify/fsnotify" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -// The actual test suite -var _ = t.Describe("DirectoryNotifier", func() { - var testDir, testFile string - BeforeEach(func() { - testDir = t.MustTempDir("dirnotifier") - testFile = filepath.Join(testDir, "file") - }) - t.Describe("New", func() { - It("should fail with invalid directory", func() { - // When - _, err := dirnotifier.New("doesnotexist", fsnotify.Create) - // Then - Expect(err).NotTo(BeNil()) - }) - }) - t.Describe("Directory", func() { - It("should be configured", func() { - // When - dn, err := dirnotifier.New(testDir, fsnotify.Create) - Expect(err).To(BeNil()) - // Then - Expect(dn.Directory()).To(Equal(testDir)) - }) - }) - t.Describe("NotifierForFile", func() { - It("should fail on duplicate", func() { - // Given - dn, err := dirnotifier.New(testDir, fsnotify.Create) - Expect(err).To(BeNil()) - // When - _, err = dn.NotifierForFile(testFile) - Expect(err).To(BeNil()) - // Then - _, err = dn.NotifierForFile(testFile) - Expect(err).NotTo(BeNil()) - }) - It("should notify on configured operation", func() { - // Given - dn, err := dirnotifier.New(testDir, fsnotify.Create) - Expect(err).To(BeNil()) - // When - notifierChan, err := dn.NotifierForFile(testFile) - Expect(err).To(BeNil()) - f, err := os.Create(testFile) - Expect(err).To(BeNil()) - f.Close() - - // Then - <-notifierChan - }) - }) -}) diff --git a/internal/dirnotifier/suite_test.go b/internal/dirnotifier/suite_test.go deleted file mode 100644 index 1438cf15119..00000000000 --- a/internal/dirnotifier/suite_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package dirnotifier_test - -import ( - "testing" - - . "github.com/cri-o/cri-o/test/framework" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -// TestDirNotifier runs the created specs -func TestDirNotifier(t *testing.T) { - RegisterFailHandler(Fail) - RunFrameworkSpecs(t, "DirectoryNotifier") -} - -var t *TestFramework - -var _ = BeforeSuite(func() { - t = NewTestFramework(NilFunc, NilFunc) - t.Setup() -}) - -var _ = AfterSuite(func() { - t.Teardown() -}) diff --git a/internal/oci/oci.go b/internal/oci/oci.go index c50e1c4e7f1..b59811eed1b 100644 --- a/internal/oci/oci.go +++ b/internal/oci/oci.go @@ -1,6 +1,7 @@ package oci import ( + "bytes" "fmt" "io" "os" @@ -10,10 +11,8 @@ import ( "syscall" "time" - "github.com/cri-o/cri-o/internal/dirnotifier" "github.com/cri-o/cri-o/pkg/config" "github.com/cri-o/cri-o/server/cri/types" - "github.com/fsnotify/fsnotify" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "golang.org/x/net/context" @@ -44,7 +43,6 @@ type Runtime struct { config *config.Config runtimeImplMap map[string]RuntimeImpl runtimeImplMapMutex sync.RWMutex - execNotifier *dirnotifier.DirectoryNotifier } // RuntimeImpl is an interface used by the caller to interact with the @@ -83,14 +81,9 @@ func New(c *config.Config) (*Runtime, error) { return nil, errors.Wrapf(err, "create oci runtime pid dir") } - notifier, err := dirnotifier.New(execNotifyDir, fsnotify.Write, fsnotify.Rename) - if err != nil { - return nil, err - } return &Runtime{ config: c, runtimeImplMap: make(map[string]RuntimeImpl), - execNotifier: notifier, }, nil } @@ -433,6 +426,18 @@ func (r *Runtime) ReopenContainerLog(ctx context.Context, c *Container) error { return impl.ReopenContainerLog(ctx, c) } +// ExecSyncError wraps command's streams, exit code and error on ExecSync error. +type ExecSyncError struct { + Stdout bytes.Buffer + Stderr bytes.Buffer + ExitCode int32 + Err error +} + +func (e *ExecSyncError) Error() string { + return fmt.Sprintf("command error: %+v, stdout: %s, stderr: %s, exit code %d", e.Err, e.Stdout.Bytes(), e.Stderr.Bytes(), e.ExitCode) +} + // BuildContainerdBinaryName() is responsible for ensuring the binary passed will // be properly converted to the containerd binary naming pattern. // diff --git a/internal/oci/runtime_oci.go b/internal/oci/runtime_oci.go index 6f433607de5..11122587c7c 100644 --- a/internal/oci/runtime_oci.go +++ b/internal/oci/runtime_oci.go @@ -22,7 +22,6 @@ import ( "github.com/cri-o/cri-o/server/metrics" "github.com/cri-o/cri-o/utils" "github.com/fsnotify/fsnotify" - "github.com/google/uuid" json "github.com/json-iterator/go" rspec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" @@ -72,6 +71,12 @@ type syncInfo struct { Message string `json:"message,omitempty"` } +// exitCodeInfo is used to return the monitored process exit code to the daemon +type exitCodeInfo struct { + ExitCode int32 `json:"exit_code"` + Message string `json:"message,omitempty"` +} + // CreateContainer creates a container. func (r *runtimeOCI) CreateContainer(ctx context.Context, c *Container, cgroupParent string) (retErr error) { if c.Spoofed() { @@ -255,6 +260,69 @@ func (r *runtimeOCI) StartContainer(ctx context.Context, c *Container) error { return nil } +func prepareExec() (pidFileName string, parentPipe, childPipe *os.File, _ error) { + var err error + parentPipe, childPipe, err = os.Pipe() + if err != nil { + return "", nil, nil, err + } + + pidFile, err := ioutil.TempFile("", "pidfile") + if err != nil { + parentPipe.Close() + childPipe.Close() + return "", nil, nil, err + } + pidFile.Close() + pidFileName = pidFile.Name() + + return pidFileName, parentPipe, childPipe, nil +} + +func parseLog(ctx context.Context, l []byte) (stdout, stderr []byte) { + // Split the log on newlines, which is what separates entries. + lines := bytes.SplitAfter(l, []byte{'\n'}) + for _, line := range lines { + // Ignore empty lines. + if len(line) == 0 { + continue + } + + // The format of log lines is "DATE pipe LogTag REST". + parts := bytes.SplitN(line, []byte{' '}, 4) + if len(parts) < 4 { + // Ignore the line if it's formatted incorrectly, but complain + // about it so it can be debugged. + log.Warnf(ctx, "Hit invalid log format: %q", string(line)) + continue + } + + pipe := string(parts[1]) + content := parts[3] + + linetype := string(parts[2]) + if linetype == "P" { + contentLen := len(content) + if contentLen > 0 && content[contentLen-1] == '\n' { + content = content[:contentLen-1] + } + } + + switch pipe { + case "stdout": + stdout = append(stdout, content...) + case "stderr": + stderr = append(stderr, content...) + default: + // Complain about unknown pipes. + log.Warnf(ctx, "Hit invalid log format [unknown pipe %s]: %q", pipe, string(line)) + continue + } + } + + return stdout, stderr +} + // ExecContainer prepares a streaming endpoint to execute a command in the container. func (r *runtimeOCI) ExecContainer(ctx context.Context, c *Container, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error { if c.Spoofed() { @@ -266,8 +334,59 @@ func (r *runtimeOCI) ExecContainer(ctx context.Context, c *Container, cmd []stri return err } defer os.RemoveAll(processFile) - execCmd := r.constructExecCommand(ctx, c, processFile, "") - cmdErr := r.executeExec(execCmd, stdin, stdout, stderr, tty, resize) + + args := []string{rootFlag, r.root, "exec"} + args = append(args, "--process", processFile, c.ID()) + execCmd := exec.Command(r.path, args...) // nolint: gosec + if v, found := os.LookupEnv("XDG_RUNTIME_DIR"); found { + execCmd.Env = append(execCmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", v)) + } + var cmdErr, copyError error + if tty { + cmdErr = ttyCmd(execCmd, stdin, stdout, resize) + } else { + var r, w *os.File + if stdin != nil { + // Use an os.Pipe here as it returns true *os.File objects. + // This way, if you run 'kubectl exec -i bash' (no tty) and type 'exit', + // the call below to execCmd.Run() can unblock because its Stdin is the read half + // of the pipe. + r, w, err = os.Pipe() + if err != nil { + return err + } + execCmd.Stdin = r + go func() { + _, copyError = pools.Copy(w, stdin) + w.Close() + }() + } + + if stdout != nil { + execCmd.Stdout = stdout + } + + if stderr != nil { + execCmd.Stderr = stderr + } + + if err := execCmd.Start(); err != nil { + return err + } + + // The read side of the pipe should be closed after the container process has been started. + if r != nil { + if err := r.Close(); err != nil { + return err + } + } + + cmdErr = execCmd.Wait() + } + + if copyError != nil { + return copyError + } if exitErr, ok := cmdErr.(*exec.ExitError); ok { return &utilexec.ExitErrorWrapper{ExitError: exitErr} } @@ -277,199 +396,174 @@ func (r *runtimeOCI) ExecContainer(ctx context.Context, c *Container, cmd []stri // ExecSyncContainer execs a command in a container and returns it's stdout, stderr and return code. func (r *runtimeOCI) ExecSyncContainer(ctx context.Context, c *Container, command []string, timeout int64) (*types.ExecSyncResponse, error) { if c.Spoofed() { - return &types.ExecSyncResponse{}, nil + return nil, nil } - processFile, err := prepareProcessExec(c, command, c.terminal) + pidFile, parentPipe, childPipe, err := prepareExec() if err != nil { - return nil, err + return nil, &ExecSyncError{ + ExitCode: -1, + Err: err, + } } - defer os.RemoveAll(processFile) - - pidFile := filepath.Join(r.execNotifier.Directory(), c.id+uuid.New().String()) - - cmd := r.constructExecCommand(ctx, c, processFile, pidFile) - cmd.SysProcAttr = sysProcAttrPlatform() - - stdoutBuf := nopWriteCloser{&bytes.Buffer{}} - stderrBuf := nopWriteCloser{&bytes.Buffer{}} - resize := make(chan remotecommand.TerminalSize) + defer parentPipe.Close() + defer func() { + if e := os.Remove(pidFile); e != nil { + log.Warnf(ctx, "Could not remove temporary PID file %s", pidFile) + } + }() - pidFileCreatedCh, err := r.execNotifier.NotifierForFile(pidFile) + logFile, err := ioutil.TempFile("", "crio-log-"+c.id) if err != nil { - return nil, errors.Wrapf(err, "failed to watch %s", pidFile) + return nil, &ExecSyncError{ + ExitCode: -1, + Err: err, + } } + logFile.Close() - // wait till the command is done - done := make(chan error, 1) - go func() { - done <- r.executeExec(cmd, nil, stdoutBuf, stderrBuf, c.terminal, resize) - close(done) + logPath := logFile.Name() + defer func() { + os.RemoveAll(logPath) }() - // First, wait for the pid file to be created. - // When it is, the timer begins for the exec process. - // If the command fails before that happens, however, - // that needs to be caught. - var doneErr error - select { - case <-pidFileCreatedCh: - case doneErr = <-done: + args := []string{ + "-c", c.id, + "-n", c.name, + "-r", r.path, + "-p", pidFile, + "-e", + "-l", logPath, + "--socket-dir-path", r.config.ContainerAttachSocketDir, + "--log-level", logrus.GetLevel().String(), } - switch { - case doneErr != nil: - // If we've already gotten an error from done - // the runtime finished before writing the pid file - // (probably because the command didn't exist). - case timeout > 0: - // If there's a timeout, wait for that timeout duration. - select { - case <-time.After(time.Second * time.Duration(timeout)): - // Ensure the process is not left behind - killContainerExecProcess(ctx, pidFile, cmd) - - // Make sure the runtime process has been cleaned up - <-done - - // If the command timed out, we should return an ExecSyncResponse with a non-zero exit code because - // the prober code in the kubelet checks for it. If we return a custom error, - // then the probes transition into Unknown status and the container isn't restarted as expected. - return &types.ExecSyncResponse{ - Stderr: []byte(conmonconfig.TimedOutMessage), - ExitCode: -1, - }, nil - case doneErr = <-done: - break - } - default: - // If no timeout, just wait until the command finishes. - doneErr = <-done + if r.config.ConmonSupportsSync() { + args = append(args, "--sync") } - - // gather exit code from err - exitCode := int32(0) - if doneErr != nil { - if exitError, ok := doneErr.(*exec.ExitError); ok { - exitCode = int32(exitError.ExitCode()) - } + if c.terminal { + args = append(args, "-t") } - - return &types.ExecSyncResponse{ - Stdout: stdoutBuf.Bytes(), - Stderr: stderrBuf.Bytes(), - ExitCode: exitCode, - }, nil -} - -func (r *runtimeOCI) executeExec(execCmd *exec.Cmd, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error { - var cmdErr, copyError error - if tty { - return ttyCmd(execCmd, stdin, stdout, resize) + if timeout > 0 { + args = append(args, "-T", fmt.Sprintf("%d", timeout)) } - var rd, w *os.File - var err error - if stdin != nil { - // Use an os.Pipe here as it returns true *os.File objects. - // This way, if you run 'kubectl exec -i bash' (no tty) and type 'exit', - // the call below to execCmd.Run() can unblock because its Stdin is the read half - // of the pipe. - rd, w, err = os.Pipe() - if err != nil { - return err + + processFile, err := prepareProcessExec(c, command, c.terminal) + if err != nil { + return nil, &ExecSyncError{ + ExitCode: -1, + Err: err, } - execCmd.Stdin = rd - go func() { - _, copyError = pools.Copy(w, stdin) - w.Close() - }() } + defer os.RemoveAll(processFile) - if stdout != nil { - execCmd.Stdout = stdout - } + args = append(args, + "--exec-process-spec", processFile, + "--runtime-arg", fmt.Sprintf("%s=%s", rootFlag, r.root)) - if stderr != nil { - execCmd.Stderr = stderr - } + cmd := exec.Command(r.config.Conmon, args...) // nolint: gosec - if err := execCmd.Start(); err != nil { - return err + var stdoutBuf, stderrBuf bytes.Buffer + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + cmd.ExtraFiles = append(cmd.ExtraFiles, childPipe) + // 0, 1 and 2 are stdin, stdout and stderr + cmd.Env = r.config.ConmonEnv + cmd.Env = append(cmd.Env, fmt.Sprintf("_OCI_SYNCPIPE=%d", 3)) + if v, found := os.LookupEnv("XDG_RUNTIME_DIR"); found { + cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", v)) } - // The read side of the pipe should be closed after the container process has been started. - if rd != nil { - if err := rd.Close(); err != nil { - return err + err = cmd.Start() + if err != nil { + childPipe.Close() + return nil, &ExecSyncError{ + Stdout: stdoutBuf, + Stderr: stderrBuf, + ExitCode: -1, + Err: err, } } - cmdErr = execCmd.Wait() + // We don't need childPipe on the parent side + childPipe.Close() - if copyError != nil { - return copyError + // first, wait till the command is done + waitErr := cmd.Wait() + + // regardless of what is in waitErr + // we should attempt to decode the output of the parent pipe + // this allows us to catch TimedOutMessage, which will cause waitErr to not be nil + var ec *exitCodeInfo + decodeErr := json.NewDecoder(parentPipe).Decode(&ec) + if decodeErr == nil { + log.Debugf(ctx, "Received container exit code: %v, message: %s", ec.ExitCode, ec.Message) + + // When we timeout the command in conmon then we should return + // an ExecSyncResponse with a non-zero exit code because + // the prober code in the kubelet checks for it. If we return + // a custom error, then the probes transition into Unknown status + // and the container isn't restarted as expected. + if ec.ExitCode == -1 && ec.Message == conmonconfig.TimedOutMessage { + return &types.ExecSyncResponse{ + Stderr: []byte(conmonconfig.TimedOutMessage), + ExitCode: -1, + }, nil + } } - return cmdErr -} - -// Needed because https://github.com/golang/go/issues/22823 was denied -type nopWriteCloser struct { - *bytes.Buffer -} - -func (nopWriteCloser) Close() error { - return nil -} -func (r *runtimeOCI) constructExecCommand(ctx context.Context, c *Container, processFile, pidFile string) *exec.Cmd { - args := []string{rootFlag, r.root, "exec"} - if pidFile != "" { - args = append(args, "--pid-file", pidFile) - } - args = append(args, "--process", processFile, c.ID()) - execCmd := exec.CommandContext(ctx, r.path, args...) // nolint: gosec - if v, found := os.LookupEnv("XDG_RUNTIME_DIR"); found { - execCmd.Env = append(execCmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", v)) + if waitErr != nil { + // if we aren't a ExitError, some I/O problems probably occurred + if _, ok := waitErr.(*exec.ExitError); !ok { + return nil, &ExecSyncError{ + Stdout: stdoutBuf, + Stderr: stderrBuf, + ExitCode: -1, + Err: waitErr, + } + } } - return execCmd -} -func killContainerExecProcess(ctx context.Context, pidFile string, cmd *exec.Cmd) { - // Attempt to get the container PID and PGID from the file the runtime should have written. - ctrPid, ctrPgid, err := pidAndpgidFromFile(pidFile) - if err != nil && ctrPid <= 0 { - // only kill the runtime process if we failed to find a ctrPid - // as this means the runtime exec hasn't successfully written the pid file - if killErr := cmd.Process.Kill(); killErr != nil { - log.Errorf(ctx, "Error killing runtime exec process(%v) after error finding runtime pid: (%v)", killErr, err) + if decodeErr != nil { + return nil, &ExecSyncError{ + Stdout: stdoutBuf, + Stderr: stderrBuf, + ExitCode: -1, + Err: decodeErr, } } - if ctrPgid > 1 { - // First attempt to kill the container group - if err := syscall.Kill(-ctrPgid, syscall.SIGKILL); err != nil { - log.Errorf(ctx, "Failed to kill process group after timeout: %v", err) - } - } else if ctrPid > 0 { - // If that fails, kill the container PID itself - if err := syscall.Kill(ctrPid, syscall.SIGKILL); err != nil { - log.Errorf(ctx, "Failed to kill process after timeout: %v", err) + if ec.ExitCode == -1 { + return nil, &ExecSyncError{ + Stdout: stdoutBuf, + Stderr: stderrBuf, + ExitCode: -1, + Err: fmt.Errorf(ec.Message), } } -} -func pidAndpgidFromFile(pidFile string) (pid, pgid int, _ error) { - // find the pid of the parent process - pidStr, err := ioutil.ReadFile(pidFile) + // The actual logged output is not the same as stdoutBuf and stderrBuf, + // which are used for getting error information. For the actual + // ExecSyncResponse we have to read the logfile. + // XXX: Currently runC dups the same console over both stdout and stderr, + // so we can't differentiate between the two. + logBytes, err := ioutil.ReadFile(logPath) if err != nil { - return -1, -1, err - } - pid, err = strconv.Atoi(string(pidStr)) - if err != nil { - return -1, -1, err + return nil, &ExecSyncError{ + Stdout: stdoutBuf, + Stderr: stderrBuf, + ExitCode: -1, + Err: err, + } } - pgid, err = syscall.Getpgid(pid) - return pid, pgid, err + + // We have to parse the log output into {stdout, stderr} buffers. + stdoutBytes, stderrBytes := parseLog(ctx, logBytes) + return &types.ExecSyncResponse{ + Stdout: stdoutBytes, + Stderr: stderrBytes, + ExitCode: ec.ExitCode, + }, nil } // UpdateContainer updates container resources @@ -1030,24 +1124,57 @@ func (r *runtimeOCI) ReopenContainerLog(ctx context.Context, c *Container) error } defer controlFile.Close() - done := make(chan struct{}, 1) - defer close(done) - ch, err := WatchForFile(c.LogPath(), done, fsnotify.Create, fsnotify.Write) + watcher, err := fsnotify.NewWatcher() if err != nil { - return errors.Wrapf(err, "failed to create watch for %s", c.LogPath()) + return fmt.Errorf("failed to create new watch: %v", err) + } + defer watcher.Close() + + done := make(chan struct{}) + doneClosed := false + errorCh := make(chan error) + go func() { + for { + select { + case event := <-watcher.Events: + log.Debugf(ctx, "Event: %v", event) + if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write { + log.Debugf(ctx, "File created %s", event.Name) + if event.Name == c.LogPath() { + log.Debugf(ctx, "Expected log file created") + done <- struct{}{} + return + } + } + case err := <-watcher.Errors: + errorCh <- fmt.Errorf("watch error for container log reopen %v: %v", c.ID(), err) + close(errorCh) + return + } + } + }() + cLogDir := filepath.Dir(c.LogPath()) + if err := watcher.Add(cLogDir); err != nil { + log.Errorf(ctx, "Watcher.Add(%q) failed: %s", cLogDir, err) + close(done) + doneClosed = true } if _, err = fmt.Fprintf(controlFile, "%d %d %d\n", 2, 0, 0); err != nil { log.Debugf(ctx, "Failed to write to control file to reopen log file: %v", err) } + select { - case err := <-ch: - if err != nil { - return errors.Wrapf(err, "failed to watch for %s", c.LogPath()) + case err := <-errorCh: + if !doneClosed { + close(done) } - case <-time.After(time.Minute * 3): - // Give up after 3 minutes, as something wrong probably happened - log.Errorf(ctx, "Failed to reopen log file for container %s: timed out", c.ID()) + return err + case <-done: + if !doneClosed { + close(done) + } + break } return nil @@ -1092,45 +1219,3 @@ func prepareProcessExec(c *Container, cmd []string, tty bool) (processFile strin func (c *Container) conmonPidFilePath() string { return filepath.Join(c.bundlePath, "conmon-pidfile") } - -// WatchForFile creates a watch on the parent directory of path, looking for events opsToWatch. -// It returns immediately with a channel to find when path had one of those events. -// done can be used to stop the watch. -// WatchForFile is responsible for closing all internal channels and the returned channel, but not for closing done. -func WatchForFile(path string, done chan struct{}, opsToWatch ...fsnotify.Op) (chan error, error) { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return nil, err - } - - ch := make(chan error) - - dir := filepath.Dir(path) - go func() { - defer watcher.Close() - defer close(ch) - for { - select { - case event := <-watcher.Events: - if event.Name != path { - continue - } - for op := range opsToWatch { - if event.Op&fsnotify.Op(op) == fsnotify.Op(op) { - ch <- nil - return - } - } - case err := <-watcher.Errors: - ch <- err - return - case <-done: - return - } - } - }() - if err := watcher.Add(dir); err != nil { - return nil, err - } - return ch, nil -} diff --git a/internal/oci/runtime_oci_test.go b/internal/oci/runtime_oci_test.go index 7f990d4121e..3385e305027 100644 --- a/internal/oci/runtime_oci_test.go +++ b/internal/oci/runtime_oci_test.go @@ -3,13 +3,10 @@ package oci_test import ( "context" "math/rand" - "os" "os/exec" - "path/filepath" "time" "github.com/cri-o/cri-o/internal/oci" - "github.com/fsnotify/fsnotify" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/pkg/errors" @@ -145,54 +142,6 @@ var _ = t.Describe("Oci", func() { }) } }) - t.Describe("WatchForFile", func() { - var notifyFile string - var done chan struct{} - BeforeEach(func() { - notifyFile = filepath.Join(t.MustTempDir("watch"), "file") - done = make(chan struct{}, 1) - }) - It("should catch file creation", func() { - // Given - ch, err := oci.WatchForFile(notifyFile, done, fsnotify.Create, fsnotify.Write) - Expect(err).To(BeNil()) - - // When - f, err := os.Create(notifyFile) - Expect(err).To(BeNil()) - f.Close() - - <-ch - }) - It("should not catch file create if doesn't exist", func() { - // Given - ch, err := oci.WatchForFile(notifyFile, done, fsnotify.Create, fsnotify.Write) - Expect(err).To(BeNil()) - - // When - f, err := os.Create(notifyFile + "-backup") - Expect(err).To(BeNil()) - f.Close() - checkChannelEmpty(ch) - - // Then - f, err = os.Create(notifyFile) - Expect(err).To(BeNil()) - f.Close() - - <-ch - }) - It("should give up after sending on done", func() { - // Given - ch, err := oci.WatchForFile(notifyFile, done, fsnotify.Write) - Expect(err).To(BeNil()) - - // When - checkChannelEmpty(ch) - done <- struct{}{} - <-ch - }) - }) }) func waitContainerStopAndFailAfterTimeout(ctx context.Context, @@ -234,12 +183,3 @@ func verifyContainerNotStopped(sut *oci.Container, _ *exec.Cmd, waitError error) func inSeconds(d int64) time.Duration { return time.Duration(d) * time.Second } - -func checkChannelEmpty(ch chan error) { - select { - case <-ch: - // We don't expect to get anything here - Expect(true).To(Equal(false)) - case <-time.After(time.Second * 3): - } -}