Source code

Revision control

Other Tools

#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Usage: extract_and_run_command.py [-j N] [command to run] -- [files and/or directories]
-j is the number of workers to start, defaulting to 1.
[command to run] must be a command that can accept one or many files
to process as arguments.
WARNING: This script does NOT respond to SIGINT. You must use SIGQUIT or SIGKILL to
terminate it early.
"""
### The canonical location for this file is
###
### Please update the copy in puppet to deploy new changes to
### stage.mozilla.org, see
from __future__ import absolute_import
import logging
import os
import shutil
import subprocess
import sys
import tempfile
import time
from os import path
from threading import Thread
try:
from Queue import Queue
except ImportError:
from queue import Queue
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(message)s")
log = logging.getLogger(__name__)
try:
# the future - https://github.com/mozilla/build-mar via a venv
from mardor.marfile import BZ2MarFile
except:
sys.path.append(
path.join(path.dirname(path.realpath(__file__)), "../buildfarm/utils")
)
from mar import BZ2MarFile
SEVENZIP = "7za"
def extractMar(filename, tempdir):
m = BZ2MarFile(filename)
m.extractall(path=tempdir)
def extractExe(filename, tempdir):
try:
# We don't actually care about output, put we redirect to a tempfile
# to avoid deadlocking in wait() when stdout=PIPE
fd = tempfile.TemporaryFile()
proc = subprocess.Popen(
[SEVENZIP, "x", "-o%s" % tempdir, filename],
stdout=fd,
stderr=subprocess.STDOUT,
)
proc.wait()
except subprocess.CalledProcessError:
# Not all EXEs are 7-zip files, so we have to ignore extraction errors
pass
# The keys here are matched against the last 3 characters of filenames.
# The values are callables that accept two string arguments.
EXTRACTORS = {
".mar": extractMar,
".exe": extractExe,
}
def find_files(d):
"""yields all of the files in `d'"""
for root, _, files in os.walk(d):
for f in files:
yield path.abspath(path.join(root, f))
def rchmod(d, mode=0o755):
"""chmods everything in `d' to `mode', including `d' itself"""
os.chmod(d, mode)
for root, dirs, files in os.walk(d):
for item in dirs:
os.chmod(path.join(root, item), mode)
for item in files:
os.chmod(path.join(root, item), mode)
def maybe_extract(filename):
"""If an extractor is found for `filename', extracts it to a temporary
directory and chmods it. The consumer is responsible for removing
the extracted files, if desired."""
ext = path.splitext(filename)[1]
if ext not in EXTRACTORS:
return None
# Append the full filepath to the tempdir
tempdir_root = tempfile.mkdtemp()
tempdir = path.join(tempdir_root, filename.lstrip("/"))
os.makedirs(tempdir)
EXTRACTORS[ext](filename, tempdir)
rchmod(tempdir_root)
return tempdir_root
def process(item, command):
def format_time(t):
return time.strftime("%H:%M:%S", time.localtime(t))
# Buffer output to avoid interleaving of multiple workers'
logs = []
args = [item]
proc = None
start = time.time()
logs.append("START %s: %s" % (format_time(start), item))
# If the file was extracted, we need to process all of its files, too.
tempdir = maybe_extract(item)
if tempdir:
for f in find_files(tempdir):
args.append(f)
try:
fd = tempfile.TemporaryFile()
proc = subprocess.Popen(command + args, stdout=fd)
proc.wait()
if proc.returncode != 0:
raise Exception("returned %s" % proc.returncode)
finally:
if tempdir:
shutil.rmtree(tempdir)
fd.seek(0)
# rstrip() here to avoid an unnecessary newline, if it exists.
logs.append(fd.read().rstrip())
end = time.time()
elapsed = end - start
logs.append(
"END %s (%d seconds elapsed): %s\n" % (format_time(end), elapsed, item)
)
# Now that we've got all of our output, print it. It's important that
# the logging module is used for this, because "print" is not
# thread-safe.
log.info("\n".join(logs))
def worker(command, errors):
item = q.get()
while item != None:
try:
process(item, command)
except:
errors.put(item)
item = q.get()
if __name__ == "__main__":
# getopt is used in favour of optparse to enable "--" as a separator
# between the command and list of files. optparse doesn't allow that.
from getopt import getopt
options, args = getopt(sys.argv[1:], "j:h", ["help"])
concurrency = 1
for o, a in options:
if o == "-j":
concurrency = int(a)
elif o in ("-h", "--help"):
log.info(__doc__)
sys.exit(0)
if len(args) < 3 or "--" not in args:
log.error(__doc__)
sys.exit(1)
command = []
while args[0] != "--":
command.append(args.pop(0))
args.pop(0)
q = Queue()
errors = Queue()
threads = []
for i in range(concurrency):
t = Thread(target=worker, args=(command, errors))
t.start()
threads.append(t)
# find_files is a generator, so work will begin prior to it finding
# all of the files
for arg in args:
if path.isfile(arg):
q.put(arg)
else:
for f in find_files(arg):
q.put(f)
# Because the workers are started before we start populating the q
# they can't use .empty() to determine whether or not their done.
# We also can't use q.join() or j.task_done(), because we need to
# support Python 2.4. We know that find_files won't yield None,
# so we can detect doneness by having workers die when they get None
# as an item.
for i in range(concurrency):
q.put(None)
for t in threads:
t.join()
if not errors.empty():
log.error("Command failed for the following files:")
while not errors.empty():
log.error(" %s" % errors.get())
sys.exit(1)