2020-01-21 10:01:55 -05:00
|
|
|
# Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
2019-06-08 07:46:57 -04:00
|
|
|
import collections
|
2018-07-08 02:24:29 -04:00
|
|
|
import os
|
2018-08-19 03:00:34 -04:00
|
|
|
import re
|
2018-07-25 04:17:44 -04:00
|
|
|
import shutil
|
2019-05-30 16:40:40 -04:00
|
|
|
import select
|
2018-07-25 04:17:44 -04:00
|
|
|
import stat
|
2018-07-23 20:50:11 -04:00
|
|
|
import sys
|
2018-07-08 02:24:29 -04:00
|
|
|
import subprocess
|
2019-02-01 18:29:00 -05:00
|
|
|
import tempfile
|
2019-05-30 16:40:40 -04:00
|
|
|
import time
|
2018-07-08 02:24:29 -04:00
|
|
|
|
2019-06-03 12:35:55 -04:00
|
|
|
if os.environ.get("NO_COLOR", None):
|
|
|
|
RESET = FG_READ = FG_GREEN = ""
|
|
|
|
else:
|
|
|
|
RESET = "\x1b[0m"
|
|
|
|
FG_RED = "\x1b[31m"
|
|
|
|
FG_GREEN = "\x1b[32m"
|
2018-11-04 14:52:31 -05:00
|
|
|
|
2018-07-21 19:08:24 -04:00
|
|
|
executable_suffix = ".exe" if os.name == "nt" else ""
|
2019-09-14 09:01:27 -04:00
|
|
|
|
2018-07-25 04:17:44 -04:00
|
|
|
root_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
2020-02-02 16:55:22 -05:00
|
|
|
tests_path = os.path.join(root_path, "cli/tests")
|
2019-09-14 09:01:27 -04:00
|
|
|
third_party_path = os.path.join(root_path, "third_party")
|
2018-07-21 19:08:24 -04:00
|
|
|
|
2018-07-08 02:24:29 -04:00
|
|
|
|
2018-11-30 03:27:41 -05:00
|
|
|
def make_env(merge_env=None, env=None):
|
2018-07-25 04:17:44 -04:00
|
|
|
if env is None:
|
|
|
|
env = os.environ
|
|
|
|
env = env.copy()
|
2018-11-30 03:27:41 -05:00
|
|
|
if merge_env is None:
|
|
|
|
merge_env = {}
|
2018-07-25 04:17:44 -04:00
|
|
|
for key in merge_env.keys():
|
|
|
|
env[key] = merge_env[key]
|
|
|
|
return env
|
|
|
|
|
|
|
|
|
2018-10-21 21:47:34 -04:00
|
|
|
def add_env_path(add, env, key="PATH", prepend=False):
|
|
|
|
dirs_left = env[key].split(os.pathsep) if key in env else []
|
2018-11-30 03:27:41 -05:00
|
|
|
dirs_right = add.split(os.pathsep) if isinstance(add, str) else add
|
2018-10-21 21:47:34 -04:00
|
|
|
|
|
|
|
if prepend:
|
|
|
|
dirs_left, dirs_right = dirs_right, dirs_left
|
|
|
|
|
2018-11-30 03:27:41 -05:00
|
|
|
for d in dirs_right:
|
|
|
|
if not d in dirs_left:
|
|
|
|
dirs_left += [d]
|
2018-10-21 21:47:34 -04:00
|
|
|
|
|
|
|
env[key] = os.pathsep.join(dirs_left)
|
|
|
|
|
|
|
|
|
2019-09-15 10:47:37 -04:00
|
|
|
def run(args, quiet=False, cwd=None, env=None, merge_env=None, shell=None):
|
2018-07-25 04:17:44 -04:00
|
|
|
args[0] = os.path.normpath(args[0])
|
|
|
|
env = make_env(env=env, merge_env=merge_env)
|
2019-09-15 10:47:37 -04:00
|
|
|
if shell is None:
|
|
|
|
# Use the default value for 'shell' parameter.
|
|
|
|
# - Posix: do not use shell.
|
|
|
|
# - Windows: use shell; this makes .bat/.cmd files work.
|
|
|
|
shell = os.name == "nt"
|
|
|
|
if not quiet:
|
|
|
|
print " ".join([shell_quote(arg) for arg in args])
|
2018-07-23 20:50:11 -04:00
|
|
|
rc = subprocess.call(args, cwd=cwd, env=env, shell=shell)
|
|
|
|
if rc != 0:
|
|
|
|
sys.exit(rc)
|
2018-07-08 02:24:29 -04:00
|
|
|
|
|
|
|
|
2019-06-08 07:46:57 -04:00
|
|
|
CmdResult = collections.namedtuple('CmdResult', ['out', 'err', 'code'])
|
|
|
|
|
|
|
|
|
|
|
|
def run_output(args,
|
|
|
|
quiet=False,
|
|
|
|
cwd=None,
|
|
|
|
env=None,
|
|
|
|
merge_env=None,
|
|
|
|
exit_on_fail=False):
|
2018-11-30 03:27:41 -05:00
|
|
|
if merge_env is None:
|
|
|
|
merge_env = {}
|
2018-07-26 20:15:55 -04:00
|
|
|
args[0] = os.path.normpath(args[0])
|
|
|
|
if not quiet:
|
|
|
|
print " ".join(args)
|
|
|
|
env = make_env(env=env, merge_env=merge_env)
|
|
|
|
shell = os.name == "nt" # Run through shell to make .bat/.cmd files work.
|
2019-06-08 07:46:57 -04:00
|
|
|
p = subprocess.Popen(
|
|
|
|
args,
|
|
|
|
cwd=cwd,
|
|
|
|
env=env,
|
|
|
|
shell=shell,
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.PIPE)
|
|
|
|
try:
|
|
|
|
out, err = p.communicate()
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
p.kill()
|
|
|
|
p.wait()
|
|
|
|
raise e
|
|
|
|
retcode = p.poll()
|
|
|
|
if retcode and exit_on_fail:
|
|
|
|
sys.exit(retcode)
|
|
|
|
# Ignore Windows CRLF (\r\n).
|
|
|
|
return CmdResult(
|
|
|
|
out.replace('\r\n', '\n'), err.replace('\r\n', '\n'), retcode)
|
2018-07-26 20:15:55 -04:00
|
|
|
|
|
|
|
|
2018-09-25 16:30:09 -04:00
|
|
|
def shell_quote_win(arg):
|
|
|
|
if re.search(r'[\x00-\x20"^%~!@&?*<>|()=]', arg):
|
|
|
|
# Double all " quote characters.
|
|
|
|
arg = arg.replace('"', '""')
|
|
|
|
# Wrap the entire string in " quotes.
|
|
|
|
arg = '"' + arg + '"'
|
|
|
|
# Double any N backslashes that are immediately followed by a " quote.
|
|
|
|
arg = re.sub(r'(\\+)(?=")', r'\1\1', arg)
|
|
|
|
return arg
|
|
|
|
|
|
|
|
|
|
|
|
def shell_quote(arg):
|
|
|
|
if os.name == "nt":
|
|
|
|
return shell_quote_win(arg)
|
|
|
|
else:
|
|
|
|
# Python 2 has posix shell quoting built in, albeit in a weird place.
|
|
|
|
from pipes import quote
|
|
|
|
return quote(arg)
|
|
|
|
|
|
|
|
|
2018-07-08 02:24:29 -04:00
|
|
|
def symlink(target, name, target_is_dir=False):
|
|
|
|
if os.name == "nt":
|
2018-07-31 05:19:53 -04:00
|
|
|
from ctypes import WinDLL, WinError, GetLastError
|
|
|
|
from ctypes.wintypes import BOOLEAN, DWORD, LPCWSTR
|
|
|
|
|
|
|
|
kernel32 = WinDLL('kernel32', use_last_error=False)
|
|
|
|
CreateSymbolicLinkW = kernel32.CreateSymbolicLinkW
|
|
|
|
CreateSymbolicLinkW.restype = BOOLEAN
|
|
|
|
CreateSymbolicLinkW.argtypes = (LPCWSTR, LPCWSTR, DWORD)
|
|
|
|
|
|
|
|
# File-type symlinks can only use backslashes as separators.
|
|
|
|
target = os.path.normpath(target)
|
|
|
|
|
|
|
|
# If the symlink points at a directory, it needs to have the appropriate
|
|
|
|
# flag set, otherwise the link will be created but it won't work.
|
|
|
|
if target_is_dir:
|
|
|
|
type_flag = 0x01 # SYMBOLIC_LINK_FLAG_DIRECTORY
|
|
|
|
else:
|
|
|
|
type_flag = 0
|
|
|
|
|
|
|
|
# Before Windows 10, creating symlinks requires admin privileges.
|
|
|
|
# As of Win 10, there is a flag that allows anyone to create them.
|
|
|
|
# Initially, try to use this flag.
|
|
|
|
unpriv_flag = 0x02 # SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
|
|
|
|
r = CreateSymbolicLinkW(name, target, type_flag | unpriv_flag)
|
|
|
|
|
|
|
|
# If it failed with ERROR_INVALID_PARAMETER, try again without the
|
|
|
|
# 'allow unprivileged create' flag.
|
|
|
|
if not r and GetLastError() == 87: # ERROR_INVALID_PARAMETER
|
|
|
|
r = CreateSymbolicLinkW(name, target, type_flag)
|
|
|
|
|
|
|
|
# Throw if unsuccessful even after the second attempt.
|
|
|
|
if not r:
|
|
|
|
raise WinError()
|
2018-07-08 02:24:29 -04:00
|
|
|
else:
|
|
|
|
os.symlink(target, name)
|
|
|
|
|
|
|
|
|
|
|
|
def touch(fname):
|
|
|
|
if os.path.exists(fname):
|
|
|
|
os.utime(fname, None)
|
|
|
|
else:
|
|
|
|
open(fname, 'a').close()
|
2018-07-23 12:44:27 -04:00
|
|
|
|
|
|
|
|
2019-09-14 10:25:49 -04:00
|
|
|
# Recursively list all files in (a subdirectory of) a git worktree.
|
|
|
|
# * Optionally, glob patterns may be specified to e.g. only list files with a
|
|
|
|
# certain extension.
|
|
|
|
# * Untracked files are included, unless they're listed in .gitignore.
|
|
|
|
# * Directory names themselves are not listed (but the files inside are).
|
|
|
|
# * Submodules and their contents are ignored entirely.
|
|
|
|
# * This function fails if the query matches no files.
|
|
|
|
def git_ls_files(base_dir, patterns=None):
|
|
|
|
base_dir = os.path.abspath(base_dir)
|
|
|
|
args = [
|
|
|
|
"git", "-C", base_dir, "ls-files", "-z", "--exclude-standard",
|
|
|
|
"--cached", "--modified", "--others"
|
|
|
|
]
|
|
|
|
if patterns:
|
|
|
|
args += ["--"] + patterns
|
|
|
|
output = subprocess.check_output(args)
|
|
|
|
files = [
|
|
|
|
os.path.normpath(os.path.join(base_dir, f)) for f in output.split("\0")
|
|
|
|
if f != ""
|
|
|
|
]
|
|
|
|
if not files:
|
|
|
|
raise RuntimeError("git_ls_files: no files in '%s'" % base_dir +
|
|
|
|
(" matching %s" % patterns if patterns else ""))
|
|
|
|
return files
|
|
|
|
|
|
|
|
|
2020-05-09 06:22:27 -04:00
|
|
|
# list all files staged for commit
|
|
|
|
def git_staged(base_dir, patterns=None):
|
|
|
|
base_dir = os.path.abspath(base_dir)
|
|
|
|
args = [
|
|
|
|
"git", "-C", base_dir, "diff", "--staged", "--diff-filter=ACMR",
|
|
|
|
"--name-only", "-z"
|
|
|
|
]
|
|
|
|
if patterns:
|
|
|
|
args += ["--"] + patterns
|
|
|
|
output = subprocess.check_output(args)
|
|
|
|
files = [
|
|
|
|
os.path.normpath(os.path.join(base_dir, f)) for f in output.split("\0")
|
|
|
|
if f != ""
|
|
|
|
]
|
|
|
|
return files
|
|
|
|
|
|
|
|
|
2018-07-25 04:17:44 -04:00
|
|
|
# The Python equivalent of `rm -rf`.
|
|
|
|
def rmtree(directory):
|
|
|
|
# On Windows, shutil.rmtree() won't delete files that have a readonly bit.
|
|
|
|
# Git creates some files that do. The 'onerror' callback deals with those.
|
|
|
|
def rm_readonly(func, path, _):
|
|
|
|
os.chmod(path, stat.S_IWRITE)
|
|
|
|
func(path)
|
|
|
|
|
|
|
|
shutil.rmtree(directory, onerror=rm_readonly)
|
2018-07-26 20:15:55 -04:00
|
|
|
|
|
|
|
|
2020-03-20 21:48:34 -04:00
|
|
|
def build_mode():
|
|
|
|
if "--release" in sys.argv:
|
2019-05-30 16:40:40 -04:00
|
|
|
return "release"
|
2018-07-26 20:15:55 -04:00
|
|
|
else:
|
2020-03-20 21:48:34 -04:00
|
|
|
return "debug"
|
2018-07-26 20:15:55 -04:00
|
|
|
|
|
|
|
|
2018-11-08 13:38:20 -05:00
|
|
|
# E.G. "target/debug"
|
2018-07-26 20:15:55 -04:00
|
|
|
def build_path():
|
2020-03-20 21:48:34 -04:00
|
|
|
return os.path.join(root_path, "target", build_mode())
|
2018-08-10 13:38:32 -04:00
|
|
|
|
|
|
|
|
2018-08-19 03:00:34 -04:00
|
|
|
def parse_exit_code(s):
|
|
|
|
codes = [int(d or 1) for d in re.findall(r'error(\d*)', s)]
|
|
|
|
if len(codes) > 1:
|
|
|
|
assert False, "doesn't support multiple error codes."
|
|
|
|
elif len(codes) == 1:
|
|
|
|
return codes[0]
|
|
|
|
else:
|
|
|
|
return 0
|
2018-09-02 17:37:14 -04:00
|
|
|
|
|
|
|
|
|
|
|
# Attempts to enable ANSI escape code support.
|
|
|
|
# Returns True if successful, False if not supported.
|
|
|
|
def enable_ansi_colors():
|
|
|
|
if os.name != 'nt':
|
|
|
|
return True # On non-windows platforms this just works.
|
|
|
|
return enable_ansi_colors_win10()
|
|
|
|
|
|
|
|
|
|
|
|
# The windows 10 implementation of enable_ansi_colors.
|
|
|
|
def enable_ansi_colors_win10():
|
|
|
|
import ctypes
|
|
|
|
|
|
|
|
# Function factory for errcheck callbacks that raise WinError on failure.
|
|
|
|
def raise_if(error_result):
|
2018-11-30 03:27:41 -05:00
|
|
|
def check(result, _func, args):
|
2018-09-02 17:37:14 -04:00
|
|
|
if result == error_result:
|
|
|
|
raise ctypes.WinError(ctypes.get_last_error())
|
|
|
|
return args
|
|
|
|
|
|
|
|
return check
|
|
|
|
|
|
|
|
# Windows API types.
|
|
|
|
from ctypes.wintypes import BOOL, DWORD, HANDLE, LPCWSTR, LPVOID
|
|
|
|
LPDWORD = ctypes.POINTER(DWORD)
|
|
|
|
|
|
|
|
# Generic constants.
|
|
|
|
NULL = ctypes.c_void_p(0).value
|
|
|
|
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
|
2018-09-30 14:58:42 -04:00
|
|
|
ERROR_INVALID_PARAMETER = 87
|
2018-09-02 17:37:14 -04:00
|
|
|
|
|
|
|
# CreateFile flags.
|
|
|
|
# yapf: disable
|
|
|
|
GENERIC_READ = 0x80000000
|
|
|
|
GENERIC_WRITE = 0x40000000
|
|
|
|
FILE_SHARE_READ = 0x01
|
|
|
|
FILE_SHARE_WRITE = 0x02
|
|
|
|
OPEN_EXISTING = 3
|
|
|
|
# yapf: enable
|
|
|
|
|
|
|
|
# Get/SetConsoleMode flags.
|
|
|
|
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
|
|
|
|
|
|
|
|
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
|
|
|
|
|
|
|
|
# HANDLE CreateFileW(...)
|
|
|
|
CreateFileW = kernel32.CreateFileW
|
|
|
|
CreateFileW.restype = HANDLE
|
|
|
|
CreateFileW.errcheck = raise_if(INVALID_HANDLE_VALUE)
|
|
|
|
# yapf: disable
|
|
|
|
CreateFileW.argtypes = (LPCWSTR, # lpFileName
|
|
|
|
DWORD, # dwDesiredAccess
|
|
|
|
DWORD, # dwShareMode
|
|
|
|
LPVOID, # lpSecurityAttributes
|
|
|
|
DWORD, # dwCreationDisposition
|
|
|
|
DWORD, # dwFlagsAndAttributes
|
|
|
|
HANDLE) # hTemplateFile
|
|
|
|
# yapf: enable
|
|
|
|
|
|
|
|
# BOOL CloseHandle(HANDLE hObject)
|
|
|
|
CloseHandle = kernel32.CloseHandle
|
|
|
|
CloseHandle.restype = BOOL
|
|
|
|
CloseHandle.errcheck = raise_if(False)
|
|
|
|
CloseHandle.argtypes = (HANDLE, )
|
|
|
|
|
|
|
|
# BOOL GetConsoleMode(HANDLE hConsoleHandle, LPDWORD lpMode)
|
|
|
|
GetConsoleMode = kernel32.GetConsoleMode
|
|
|
|
GetConsoleMode.restype = BOOL
|
|
|
|
GetConsoleMode.errcheck = raise_if(False)
|
|
|
|
GetConsoleMode.argtypes = (HANDLE, LPDWORD)
|
|
|
|
|
|
|
|
# BOOL SetConsoleMode(HANDLE hConsoleHandle, DWORD dwMode)
|
|
|
|
SetConsoleMode = kernel32.SetConsoleMode
|
|
|
|
SetConsoleMode.restype = BOOL
|
|
|
|
SetConsoleMode.errcheck = raise_if(False)
|
|
|
|
SetConsoleMode.argtypes = (HANDLE, DWORD)
|
|
|
|
|
|
|
|
# Open the console output device.
|
|
|
|
conout = CreateFileW("CONOUT$", GENERIC_READ | GENERIC_WRITE,
|
|
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
|
|
|
|
OPEN_EXISTING, 0, 0)
|
|
|
|
|
|
|
|
# Get the current mode.
|
|
|
|
mode = DWORD()
|
|
|
|
GetConsoleMode(conout, ctypes.byref(mode))
|
|
|
|
|
|
|
|
# Try to set the flag that controls ANSI escape code support.
|
|
|
|
try:
|
|
|
|
SetConsoleMode(conout, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
2018-11-30 03:27:41 -05:00
|
|
|
except WindowsError as e: # pylint:disable=undefined-variable
|
2018-09-02 17:37:14 -04:00
|
|
|
if e.winerror == ERROR_INVALID_PARAMETER:
|
|
|
|
return False # Not supported, likely an older version of Windows.
|
|
|
|
raise
|
|
|
|
finally:
|
|
|
|
CloseHandle(conout)
|
|
|
|
|
|
|
|
return True
|
2018-10-11 16:56:50 -04:00
|
|
|
|
|
|
|
|
|
|
|
def extract_number(pattern, string):
|
|
|
|
matches = re.findall(pattern, string)
|
|
|
|
if len(matches) != 1:
|
|
|
|
return None
|
|
|
|
return int(matches[0])
|
2018-10-15 16:44:35 -04:00
|
|
|
|
|
|
|
|
2019-03-24 23:36:27 -04:00
|
|
|
def extract_max_latency_in_milliseconds(pattern, string):
|
|
|
|
matches = re.findall(pattern, string)
|
|
|
|
if len(matches) != 1:
|
|
|
|
return None
|
|
|
|
num = float(matches[0][0])
|
|
|
|
unit = matches[0][1]
|
|
|
|
if (unit == 'ms'):
|
|
|
|
return num
|
|
|
|
elif (unit == 'us'):
|
|
|
|
return num / 1000
|
|
|
|
elif (unit == 's'):
|
|
|
|
return num * 1000
|
|
|
|
|
|
|
|
|
2018-10-15 16:44:35 -04:00
|
|
|
def parse_wrk_output(output):
|
2019-03-24 23:36:27 -04:00
|
|
|
stats = {}
|
|
|
|
stats['req_per_sec'] = None
|
|
|
|
stats['max_latency'] = None
|
2018-10-15 16:44:35 -04:00
|
|
|
for line in output.split("\n"):
|
2019-03-24 23:36:27 -04:00
|
|
|
if stats['req_per_sec'] is None:
|
|
|
|
stats['req_per_sec'] = extract_number(r'Requests/sec:\s+(\d+)',
|
|
|
|
line)
|
|
|
|
if stats['max_latency'] is None:
|
|
|
|
stats['max_latency'] = extract_max_latency_in_milliseconds(
|
2019-07-28 17:31:18 -04:00
|
|
|
r'\s+99%(?:\s+(\d+.\d+)([a-z]+))', line)
|
2019-03-24 23:36:27 -04:00
|
|
|
return stats
|
2018-10-19 19:15:14 -04:00
|
|
|
|
|
|
|
|
|
|
|
def platform():
|
|
|
|
return {"linux2": "linux", "darwin": "mac", "win32": "win"}[sys.platform]
|
2019-02-01 18:29:00 -05:00
|
|
|
|
2019-02-11 12:57:26 -05:00
|
|
|
|
2019-02-01 18:29:00 -05:00
|
|
|
def mkdtemp():
|
|
|
|
# On Windows, set the base directory that mkdtemp() uses explicitly. If not,
|
|
|
|
# it'll use the short (8.3) path to the temp dir, which triggers the error
|
|
|
|
# 'TS5009: Cannot find the common subdirectory path for the input files.'
|
|
|
|
temp_dir = os.environ["TEMP"] if os.name == 'nt' else None
|
|
|
|
return tempfile.mkdtemp(dir=temp_dir)
|
2019-05-30 16:40:40 -04:00
|
|
|
|
|
|
|
|
|
|
|
# This function is copied from:
|
|
|
|
# https://gist.github.com/hayd/4f46a68fc697ba8888a7b517a414583e
|
|
|
|
# https://stackoverflow.com/q/52954248/1240268
|
|
|
|
def tty_capture(cmd, bytes_input, timeout=5):
|
|
|
|
"""Capture the output of cmd with bytes_input to stdin,
|
|
|
|
with stdin, stdout and stderr as TTYs."""
|
|
|
|
# pty is not available on windows, so we import it within this function.
|
|
|
|
import pty
|
|
|
|
mo, so = pty.openpty() # provide tty to enable line-buffering
|
|
|
|
me, se = pty.openpty()
|
|
|
|
mi, si = pty.openpty()
|
|
|
|
fdmap = {mo: 'stdout', me: 'stderr', mi: 'stdin'}
|
|
|
|
|
|
|
|
timeout_exact = time.time() + timeout
|
|
|
|
p = subprocess.Popen(
|
|
|
|
cmd, bufsize=1, stdin=si, stdout=so, stderr=se, close_fds=True)
|
|
|
|
os.write(mi, bytes_input)
|
|
|
|
|
|
|
|
select_timeout = .04 #seconds
|
|
|
|
res = {'stdout': b'', 'stderr': b''}
|
|
|
|
while True:
|
|
|
|
ready, _, _ = select.select([mo, me], [], [], select_timeout)
|
|
|
|
if ready:
|
|
|
|
for fd in ready:
|
|
|
|
data = os.read(fd, 512)
|
|
|
|
if not data:
|
|
|
|
break
|
|
|
|
res[fdmap[fd]] += data
|
|
|
|
elif p.poll() is not None or time.time(
|
|
|
|
) > timeout_exact: # select timed-out
|
|
|
|
break # p exited
|
|
|
|
for fd in [si, so, se, mi, mo, me]:
|
|
|
|
os.close(fd) # can't do it sooner: it leads to errno.EIO error
|
|
|
|
p.wait()
|
|
|
|
return p.returncode, res['stdout'], res['stderr']
|
2020-05-09 06:22:27 -04:00
|
|
|
|
|
|
|
|
|
|
|
def print_command(cmd, files):
|
|
|
|
noun = "file" if len(files) == 1 else "files"
|
|
|
|
print "%s (%d %s)" % (cmd, len(files), noun)
|