Your IP : 10.1.141.128


Current Path : /proc/self/root/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/
Upload File :
Current File : //proc/self/root/opt/imunify360/venv/lib/python3.11/site-packages/defence360agent/utils/_shutil.py

"""High-level file operations."""
import errno
import logging
import os
import shutil

logger = logging.getLogger(__name__)


def is_safe_subdir_name(name) -> bool:
    return (
        isinstance(name, str)
        and bool(name)
        and "\x00" not in name
        and name == os.path.basename(name)
        and name not in (".", "..")
    )


def _is_nfs_silly_rename(path):
    """NFS replaces an unlinked-but-still-open file with a sibling
    `.nfsXXXX` entry that lingers until the holder closes it. The
    name has the magic `.nfs` prefix; treating those as ignorable
    EBUSY/ENOTEMPTY sources is what every long-running process on an
    NFS-backed work dir ends up needing."""
    return os.path.basename(str(path)).startswith(".nfs")


def _only_nfs_silly_renames_inside(path):
    """Return True if the directory at `path` exists and contains
    nothing but `.nfsXXXX` survivors."""
    try:
        entries = os.listdir(path)
    except OSError:
        return False
    if not entries:
        return False
    return all(_is_nfs_silly_rename(name) for name in entries)


def _swallow_nfs(func, path, exc_info):
    """`onerror` callback for shutil.rmtree.

    NFS-backed work dirs (k8s shared volumes especially) routinely
    contain `.nfsXXXX` silly-rename files left behind by a syncer
    that still holds an open fd to a file we just unlinked. Two
    shapes appear:

      1. unlink(`<dir>/.nfsXXXX`) → EBUSY because the syncer still
         has the fd open. The file expires on its own.
      2. rmdir(`<dir>`) → ENOTEMPTY because the .nfsXXXX child is
         still there. Same root cause.

    Both are tolerable: leaving the dir + .nfs survivors in place is
    not a leak — the holder eventually closes its fd and the next
    cleanup pass succeeds.

    Re-raise everything else.
    """
    err = exc_info[1] if exc_info else None
    if err is None:
        return
    if not isinstance(err, OSError) or err.errno not in (
        errno.EBUSY,
        errno.ENOTEMPTY,
    ):
        raise err
    if _is_nfs_silly_rename(path) or _only_nfs_silly_renames_inside(path):
        logger.debug(
            "ignoring NFS silly-rename leftover at %s during rmtree"
            " (errno=%s)",
            path,
            err.errno,
        )
        return
    raise err


def rmtree(path, ignore_errors=False, onerror=None, *, max_tries=3):
    """More robust shutil.rmtree.

    Retry on "Directory not empty" race condition:
    https://github.com/ansible/ansible/issues/34335#issuecomment-362995700

    Also tolerate NFS silly-rename ``.nfsXXXX`` leftovers — they
    expire when the holder closes its fd and there is nothing for
    a synchronous cleanup to do that wouldn't race.
    """
    # Compose onerror so callers' custom handlers still see other
    # errors. We let our handler swallow only the NFS pathology.
    # Only cluster deployments put work dirs on NFS; standalone keeps
    # the caller's onerror untouched.
    # Env read (not utils.is_cluster) to avoid a circular import.
    in_cluster = os.environ.get("IS_IM_CLUSTER") == "1"
    if not in_cluster:
        effective_onerror = onerror
    elif onerror is None:
        effective_onerror = _swallow_nfs
    else:

        def _chained(func, path_, exc_info):
            try:
                _swallow_nfs(func, path_, exc_info)
            except Exception:
                onerror(func, path_, exc_info)

        effective_onerror = _chained

    retriable = [errno.EEXIST, errno.ENOTEMPTY]
    if in_cluster:
        # An EBUSY that got past _swallow_nfs is a busy path on a shared
        # volume, where the holder usually lets go within a retry.
        retriable.append(errno.EBUSY)

    for i in range(1, max_tries + 1):
        try:
            return shutil.rmtree(path, ignore_errors, effective_onerror)
        except OSError as e:
            if i == max_tries or e.errno not in retriable:
                raise

            logger.warning("Can't remove %s tree, reason: %s", path, e)