cancel_pull_transfers.py #1

  • //
  • guest/
  • russell_jackson/
  • sdp/
  • Server/
  • Unix/
  • p4/
  • common/
  • bin/
  • cancel_pull_transfers.py
  • View
  • Commits
  • Open Download .zip Download (5 KB)
#!/usr/bin/env python3
"""Cancel each archive transfer returned by p4 pull -l (Python 3.8+).

Uses the existing P4 environment/configuration and login ticket. No P4Python
dependency is needed. This cancels queued transfers; it does not retry them.
"""

import argparse
import re
import shlex
import subprocess
import sys

# The depot path can contain spaces. Identify its end from the archive revision
# and the following type/status/action/digest fields, not whitespace alone.
RECORD = re.compile(
    r"^(?P<file>//.+?)\s+(?P<revision>[0-9]+\.[0-9]+)"
    r"\s+\S+\s+\S+\s+\S+\s+[0-9a-fA-F]{32}(?:\s|$)"
)


def parse_listing(output):
    """Validate the entire snapshot before issuing any cancellation commands."""
    entries = []
    seen = set()
    for number, line in enumerate(output.splitlines(), 1):
        if not line.strip():
            continue
        match = RECORD.match(line)
        if not match:
            raise ValueError(
                f"Unrecognized p4 pull -l output on line {number}: {line[:160]!r}"
            )
        entry = (match.group("file"), match.group("revision"))
        if entry not in seen:
            seen.add(entry)
            entries.append(entry)
    return entries


def run_p4(command, timeout):
    return subprocess.run(
        command, capture_output=True, text=True, timeout=timeout, check=False
    )


def positive_timeout(value):
    value = int(value)
    if value < 1:
        raise argparse.ArgumentTypeError("timeout must be at least one second")
    return value


def main(argv=None):
    parser = argparse.ArgumentParser(
        description=(
            "Run p4 pull -l, then p4 pull -d -f FILE -r ARCHIVE_REVISION for "
            "each listed transfer. All listed statuses are included."
        ),
        epilog=(
            "Example: python3 cancel_pull_transfers.py --dry-run. "
            "Remove --dry-run to cancel the transfers. Uses your current "
            "P4PORT, P4USER, P4CONFIG and login ticket unless overridden below."
        ),
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="list commands without cancelling transfers",
    )
    parser.add_argument(
        "--p4", default="p4", help="p4 executable path (default: p4 on PATH)"
    )
    parser.add_argument(
        "-p", "--port", help="override P4PORT for both listing and cancellation"
    )
    parser.add_argument(
        "-u", "--user", help="override P4USER for both listing and cancellation"
    )
    parser.add_argument(
        "--timeout",
        type=positive_timeout,
        default=120,
        help="timeout per p4 command, in seconds (default: 120)",
    )
    args = parser.parse_args(argv)
    command = [args.p4]
    if args.port:
        command.extend(["-p", args.port])
    if args.user:
        command.extend(["-u", args.user])

    try:
        listing = run_p4(command + ["pull", "-l"], args.timeout)
        if listing.returncode:
            print("p4 pull -l failed; no transfers cancelled.", file=sys.stderr)
            print(listing.stderr or listing.stdout, file=sys.stderr, end="")
            return 1
        entries = parse_listing(listing.stdout)
    except (OSError, subprocess.TimeoutExpired, ValueError) as exc:
        print(
            f"Cannot read transfer list; no transfers cancelled: {exc}",
            file=sys.stderr,
        )
        return 1

    if listing.stderr:
        print(listing.stderr, file=sys.stderr, end="")
    if not entries:
        print("No transfers listed.")
        return 0

    failed = 0
    for filename, revision in entries:
        cancel = command + ["pull", "-d", "-f", filename, "-r", revision]
        print(shlex.join(cancel), flush=True)
        if args.dry_run:
            continue
        try:
            result = run_p4(cancel, args.timeout)
            if result.stdout:
                print(result.stdout, end="")
            if result.stderr:
                print(result.stderr, file=sys.stderr, end="")
            if result.returncode:
                failed += 1
                print(
                    f"Cancellation failed for {filename} ({revision})",
                    file=sys.stderr,
                )
        except (OSError, subprocess.TimeoutExpired) as exc:
            failed += 1
            print(
                f"Cancellation failed for {filename} ({revision}): {exc}",
                file=sys.stderr,
            )

    if args.dry_run:
        print(f"Dry run: {len(entries)} transfer(s); no transfers cancelled.")
    else:
        print(f"{len(entries) - failed} cancellation(s) succeeded; {failed} failed.")
    return 1 if failed else 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        print(
            "Interrupted; some preceding cancellations may have completed.",
            file=sys.stderr,
        )
        sys.exit(130)
# Change User Description Committed
#1 33881 Russell C. Jackson (Rusty) Add executable cancel_pull_transfers.py to cancel listed archive transfers with dry-run support.