diff options
author | Spencer Low <CompareAndSwap@gmail.com> | 2015-11-17 19:35:22 -0800 |
---|---|---|
committer | Spencer Low <CompareAndSwap@gmail.com> | 2015-11-21 20:39:56 -0800 |
commit | 82aa7da5a65d96ec991ec767f98bbd7ff5ae0770 (patch) | |
tree | 131e563535ccea1f1c4ffd3c760bb62ea6689b8e /python-packages | |
parent | 95e02602521720cbe539a7ea57e6a6fbfc88787a (diff) | |
download | android_development-82aa7da5a65d96ec991ec767f98bbd7ff5ae0770.tar.gz android_development-82aa7da5a65d96ec991ec767f98bbd7ff5ae0770.tar.bz2 android_development-82aa7da5a65d96ec991ec767f98bbd7ff5ae0770.zip |
win32: for Unicode cmdline args, switch from cmd.exe to powershell.exe
Previously, to pass a subprocess cmdline args with Unicode characters,
we'd write the command line to a batch file and run it with cmd.exe. The
problem was that the batch file used chcp to get cmd.exe to be able to
read UTF-8 from the batch file, but this had the side-effect of changing
the codepage of the user's console window.
Instead of saving & restoring the codepage (which would always have the
risk of somehow leaving the console in the wrong codepage if a Ctrl-C
handler somehow wasn't called, or if Task Manager was used manually,
etc.), we use Windows PowerShell as the helper instead of cmd.exe.
PowerShell is installed on every version of Windows since Windows 7 and
is a separate free download for Windows XP and Windows Vista, but
realistically Windows XP is no longer supported by Microsoft and
probably no one chooses to run Windows Vista over Windows 7. Plus, this
change only uses PowerShell if Unicode args are passed.
Switching to PowerShell allows us to get rid of the temp file, context
manager, and messy __del__ that caused a variety of problems in the
past.
Change-Id: Ia3df533f7747f7b9b28a093f8ca24117371e5e59
Signed-off-by: Spencer Low <CompareAndSwap@gmail.com>
Diffstat (limited to 'python-packages')
-rw-r--r-- | python-packages/adb/device.py | 116 |
1 files changed, 46 insertions, 70 deletions
diff --git a/python-packages/adb/device.py b/python-packages/adb/device.py index 436593bb7..29347c3f6 100644 --- a/python-packages/adb/device.py +++ b/python-packages/adb/device.py @@ -14,12 +14,11 @@ # limitations under the License. # import atexit -import contextlib +import base64 import logging import os import re import subprocess -import tempfile class FindDeviceError(RuntimeError): @@ -152,82 +151,59 @@ def get_emulator_device(adb_path='adb'): return _get_device_by_type('-e', adb_path=adb_path) -@contextlib.contextmanager -def _file_deleter(f): - yield - if f: - f.close() - os.remove(f.name) - - -# Internal helper that may return a temporary file (containing a command line -# in UTF-8) that should be executed with the help of _get_subprocess_args(). -def _get_windows_unicode_helper(args): - # Only do this slow work-around if Unicode is in the cmd line on Windows. - if (os.name != 'nt' or all(not isinstance(arg, unicode) for arg in args)): - return None - - # cmd.exe requires a suffix to know that it is running a batch file. - # We can't use delete=True because that causes File Share Mode Delete to be - # used which prevents the file from being opened by other processes that - # don't use that File Share Mode. The caller must manually delete the file. - tf = tempfile.NamedTemporaryFile('wb', suffix='.cmd', delete=False) - # @ in batch suppresses echo of the current line. - # Change the codepage to 65001, the UTF-8 codepage. - tf.write('@chcp 65001 > nul\r\n') - tf.write('@') - # Properly quote all the arguments and encode in UTF-8. - tf.write(subprocess.list2cmdline(args).encode('utf-8')) - tf.close() - return tf - - -# Let the caller know how to run the batch file. Takes subprocess.check_output() -# or subprocess.Popen() args and returns a new tuple that should be passed -# instead, or the original args if there is no file -def _get_subprocess_args(args, helper_file): - if helper_file: - # Concatenate our new command line args with any other function args. - return (['cmd.exe', '/c', helper_file.name],) + args[1:] - else: +# If necessary, modifies subprocess.check_output() or subprocess.Popen() args to run the subprocess +# via Windows PowerShell to work-around an issue in Python 2's subprocess class on Windows where it +# doesn't support Unicode. +def _get_subprocess_args(args): + # Only do this slow work-around if Unicode is in the cmd line on Windows. PowerShell takes + # 600-700ms to startup on a 2013-2014 machine, which is very slow. + if (os.name != 'nt' or all(not isinstance(arg, unicode) for arg in args[0])): return args + def escape_arg(arg): + # Escape for the parsing that the C Runtime does in Windows apps. In particular, this will + # take care of double-quotes. + arg = subprocess.list2cmdline([arg]) + # Escape single-quote with another single-quote because we're about to... + arg = arg.replace(u"'", u"''") + # ...put the arg in a single-quoted string for PowerShell to parse. + arg = u"'" + arg + u"'" + return arg + + # Escape command line args. + argv = map(escape_arg, args[0]) + # Cause script errors (such as adb not found) to stop script immediately with an error. + ps_code = u'$ErrorActionPreference = "Stop"\r\n'; + # Add current directory to the PATH var, to match cmd.exe/CreateProcess() behavior. + ps_code += u'$env:Path = ".;" + $env:Path\r\n'; + # Precede by &, the PowerShell call operator, and separate args by space. + ps_code += u'& ' + u' '.join(argv) + # Make the PowerShell exit code the exit code of the subprocess. + ps_code += u'\r\nExit $LastExitCode' + # Encode as UTF-16LE (without Byte-Order-Mark) which Windows natively understands. + ps_code = ps_code.encode('utf-16le') + + # Encode the PowerShell command as base64 and use the special -EncodedCommand option that base64 + # decodes. Base64 is just plain ASCII, so it should have no problem passing through Win32 + # CreateProcessA() (which python erroneously calls instead of CreateProcessW()). + return (['powershell.exe', '-NoProfile', '-NonInteractive', '-EncodedCommand', + base64.b64encode(ps_code)],) + args[1:] + # Call this instead of subprocess.check_output() to work-around issue in Python -# 2's subprocess class on Windows where it doesn't support Unicode. This -# writes the command line to a UTF-8 batch file that is properly interpreted -# by cmd.exe. +# 2's subprocess class on Windows where it doesn't support Unicode. def _subprocess_check_output(*args, **kwargs): - helper = _get_windows_unicode_helper(args[0]) - with _file_deleter(helper): - try: - return subprocess.check_output( - *_get_subprocess_args(args, helper), **kwargs) - except subprocess.CalledProcessError as e: - # Show real command line instead of the cmd.exe command line. - raise subprocess.CalledProcessError(e.returncode, args[0], - output=e.output) + try: + return subprocess.check_output(*_get_subprocess_args(args), **kwargs) + except subprocess.CalledProcessError as e: + # Show real command line instead of the powershell.exe command line. + raise subprocess.CalledProcessError(e.returncode, args[0], + output=e.output) # Call this instead of subprocess.Popen(). Like _subprocess_check_output(). -class _subprocess_Popen(subprocess.Popen): - def __init__(self, *args, **kwargs): - # __del__() can be called after global teardown has started, meaning - # the global references to _subprocess_Popen and the os module may - # no longer exist. We need to save local references to all global names - # used in __del__() to avoid this. - self.saved_class = _subprocess_Popen - self.saved_os = os - # Save reference to helper so that it can be deleted once it is no - # longer used. - self.helper = _get_windows_unicode_helper(args[0]) - super(_subprocess_Popen, self).__init__( - *_get_subprocess_args(args, self.helper), **kwargs) - - def __del__(self, *args, **kwargs): - super(self.saved_class, self).__del__(*args, **kwargs) - if self.helper: - self.saved_os.remove(self.helper.name) +def _subprocess_Popen(*args, **kwargs): + return subprocess.Popen(*_get_subprocess_args(args), **kwargs) class AndroidDevice(object): |