first draft

This commit is contained in:
Bob Hyman 2016-07-31 21:00:02 -04:00
parent 37de453104
commit 13338c0ff4
3 changed files with 166 additions and 50 deletions

View file

@ -4,12 +4,92 @@ import os
import glob
import argparse
import builtins
import subprocess
import winreg
from xonsh.lazyasd import lazyobject
from xonsh.tools import get_sep
from xonsh.platform import ON_WINDOWS
DIRSTACK = []
"""A list containing the currently remembered directories."""
_unc_tempDrives = {}
""" drive: sharePath for temp drive letters we create for UNC mapping"""
def _unc_check_enabled()->bool:
"""Check whether CMD.EXE is enforcing no-UNC-as-working-directory check.
Oh noes! can the registry entry be defined in HKCU too?
Returns:
True if `CMD.EXE` is enforcing the check (default Windows situation)
False if check is explicitly disabled by registry entry.
"""
ret_val = True
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'software\microsoft\command processor')
wval, wtype = winreg.QueryValueEx(key, 'DisableUNCCheck')
winreg.CloseKey(key)
if wtype == winreg.REG_DWORD and wval:
ret_val = False
except OSError as e:
pass
return ret_val
def _unc_map_temp_drive(unc_path)->str:
"""Map a new temporary drive letter for each distinct share, unless `CMD.EXE` is not insisting on non-UNC working directory.
Emulating behavior of `CMD.EXE` `pushd`, create a new mapped drive (starting from Z: towards A:, skipping existing
drive letters) for each new UNC path user selects.
Args:
unc_path: the path specified by user. Assumed to be a UNC path of form \\<server>\share...
Returns:
a replacement for `unc_path` to be used as the actual new working directory.
Note that the drive letter may be a the same as one already mapped if the server and share portion of `unc_path`
is the same as one still active on the stack.
"""
global _unc_tempDrives
assert unc_path[1] in (os.sep, os.altsep), "unc_path is UNC form of path"
if _unc_check_enabled():
return unc_path
else:
unc_share, rem_path = os.path.splitdrive( unc_path)
unc_share = unc_share.casefold()
for d, s in enumerate(_unc_tempDrives):
if s == unc_share:
return os.path.join( d, rem_path)
for dord in range(ord('z'), ord('a'), -1):
dpath = chr(dord) + ':'
if not os.path.isdir(dpath): # find unused drive letter starting from z:
subprocess.check_output(['NET', 'USE', dpath, unc_share], universal_newlines=True)
_unc_tempDrives[dpath] = unc_share
return os.path.join(dpath, rem_path)
pass
def _unc_unmap_temp_drive(drive):
"""Unmap a temporary drive letter if it is no longer on `DIRSTACK`.
So `DIRSTACK` must be popped prior to calling this function..."""
global _unc_tempDrives
if drive not in _unc_tempDrives: # if drive letter is not one we've mapped, don't unmap it
return
for p in DIRSTACK: # if drive letter still in use on dirstack, also don't aunmap it.
if p.startswith(drive):
return
_unc_tempDrives.pop(drive)
subprocess.check_output(['NET', 'USE', drive, '/delete'], universal_newlines=True)
def _get_cwd():
@ -128,6 +208,10 @@ def pushd(args, stdin=None):
Adds a directory to the top of the directory stack, or rotates the stack,
making the new top of the stack the current working directory.
On Windows, if the path is a UNC path (begins with `\\<server>\<share>`) and if the `DisableUNCCheck` registry
value is not enabled, creates a temporary mapped drive letter and sets the working directory there, emulating
behavior of `PUSHD` in `CMD.EXE`
"""
global DIRSTACK
@ -185,6 +269,8 @@ def pushd(args, stdin=None):
if new_pwd is not None:
if args.cd:
DIRSTACK.insert(0, os.path.expanduser(pwd))
if ON_WINDOWS and (new_pwd[0] == new_pwd[1]) and (new_pwd[0] in (os.sep, os.altsep)):
new_pwd = _unc_map_temp_drive(new_pwd)
_change_working_directory(new_pwd)
else:
DIRSTACK.insert(0, os.path.expanduser(new_pwd))
@ -277,8 +363,15 @@ def popd(args, stdin=None):
if new_pwd is not None:
e = None
if args.cd:
env = builtins.__xonsh_env__
pwd = env['PWD']
_change_working_directory(new_pwd)
if ON_WINDOWS:
drive, rem_path = os.path.splitdrive(pwd)
_unc_unmap_temp_drive(drive.casefold())
if not args.quiet and not env.get('PUSHD_SILENT'):
return dirs([], None)

View file

@ -1,4 +1,4 @@
'''Tests for xontrib.uncpushd'''
"""Tests for xontrib.uncpushd"""
import pytest
import os
@ -12,6 +12,7 @@ from xonsh.environ import Env
from xonsh.dirstack import DIRSTACK
from xonsh.platform import ON_WINDOWS
def _do_subprocess( *args, check:bool=True)->tuple:
"""
Invoke `args`, outputs and return code
@ -31,16 +32,18 @@ def _do_subprocess( *args, check:bool=True)->tuple:
, None
except subprocess.CalledProcessError as e:
if check:
print('Subproc error {}, text: {}'.format( e.returncode, e.output))
print('Subproc error {}, text: {}'.format(e.returncode, e.output))
assert e.output is None
return e.returncode \
, None \
, e.output
TEST_WORK_DIR='uncpushd'
TEST_WORK_DIR = 'uncpushd'
# seems like a lot of mocking needed to use xonsh functions in tests...
## seems like a lot of mocking needed to use xonsh functions in tests...
@pytest.yield_fixture(scope="module")
def xonsh_builtins(tmpdir_factory):
"""Mock out most of the builtins xonsh attributes."""
@ -48,18 +51,18 @@ def xonsh_builtins(tmpdir_factory):
builtins.__xonsh_env__ = Env(PWD=temp_dir.strpath)
builtins.__xonsh_ctx__ = {}
#builtins.__xonsh_shell__ = DummyShell()
# builtins.__xonsh_shell__ = DummyShell()
builtins.__xonsh_help__ = lambda x: x
#builtins.__xonsh_glob__ = glob.glob
# builtins.__xonsh_glob__ = glob.glob
builtins.__xonsh_exit__ = False
builtins.__xonsh_superhelp__ = lambda x: x
builtins.__xonsh_regexpath__ = lambda x: []
builtins.__xonsh_expand_path__ = lambda x: x
#builtins.__xonsh_subproc_captured__ = sp
#builtins.__xonsh_subproc_uncaptured__ = sp
#builtins.__xonsh_ensure_list_of_strs__ = ensure_list_of_strs
#builtins.XonshBlockError = XonshBlockError
#builtins.__xonsh_subproc_captured_hiddenobject__ = sp
# builtins.__xonsh_subproc_captured__ = sp
# builtins.__xonsh_subproc_uncaptured__ = sp
# builtins.__xonsh_ensure_list_of_strs__ = ensure_list_of_strs
# builtins.XonshBlockError = XonshBlockError
# builtins.__xonsh_subproc_captured_hiddenobject__ = sp
builtins.evalx = eval
builtins.execx = None
builtins.compilex = None
@ -67,22 +70,23 @@ def xonsh_builtins(tmpdir_factory):
yield builtins
del builtins.__xonsh_env__
del builtins.__xonsh_ctx__
#del builtins.__xonsh_shell__
# del builtins.__xonsh_shell__
del builtins.__xonsh_help__
#del builtins.__xonsh_glob__
# del builtins.__xonsh_glob__
del builtins.__xonsh_exit__
del builtins.__xonsh_superhelp__
del builtins.__xonsh_regexpath__
del builtins.__xonsh_expand_path__
#del builtins.__xonsh_subproc_captured__
#del builtins.__xonsh_subproc_uncaptured__
#del builtins.__xonsh_ensure_list_of_strs__
#del builtins.XonshBlockError
# del builtins.__xonsh_subproc_captured__
# del builtins.__xonsh_subproc_uncaptured__
# del builtins.__xonsh_ensure_list_of_strs__
# del builtins.XonshBlockError
del builtins.evalx
del builtins.execx
del builtins.compilex
del builtins.aliases
@pytest.fixture(scope="module")
def wd_setup( tmpdir_factory):
temp_dir = tmpdir_factory.getbasetemp().join(TEST_WORK_DIR)
@ -97,7 +101,6 @@ def wd_setup( tmpdir_factory):
return temp_dir
@pytest.yield_fixture(scope="module")
def shares_setup( tmpdir_factory):
"""create some shares to play with on current machine.
@ -109,10 +112,10 @@ def shares_setup( tmpdir_factory):
return []
temp_dir = tmpdir_factory.getbasetemp().join(TEST_WORK_DIR)
shares = [[r'uncpushd_test_cwd', 'y:', temp_dir.strpath] \
shares = [[r'uncpushd_test_cwd', 'y:', temp_dir.strpath]
, [r'uncpushd_test_cd1', 'w:', temp_dir.join('cdtest1').strpath]]
for s,d,l in shares: # set up some shares on local machine. dirs already exist (test case must invoke wd_setup)
for s,d,l in shares: # set up some shares on local machine. dirs already exist test case must invoke wd_setup.
_do_subprocess( ['net', 'share', s, '/delete'], check=False) # clean up from previous run after good, long wait.
_do_subprocess(['net', 'share', s + '=' + l])
@ -125,13 +128,14 @@ def shares_setup( tmpdir_factory):
os.chdir( temp_dir.strpath)
for dl in _unc_tempDrives:
_do_subprocess(['net', 'use', dl, '/delete'])
for s,d,l in shares:
for s, d, l in shares:
_do_subprocess(['net', 'use', d, '/delete'])
#_do_subprocess(['net', 'share', s, '/delete']) # fails with access denied, unless I wait > 10 sec
# see http://stackoverflow.com/questions/38448413/access-denied-in-net-share-delete
# _do_subprocess(['net', 'share', s, '/delete']) # fails with access denied, unless I wait > 10 sec
# see http://stackoverflow.com/questions/38448413/access-denied-in-net-share-delete
def test_unc_pushdpopd( xonsh_builtins, wd_setup):
"""verify extension doesn't break unix experience if someone where so benighted as to declare these aliases not on WINDOWS
"""verify extension doesn't break unix experience if someone were so benighted as to declare these aliases on unix
Also validates unc_pushd/popd work for non-unc cases
"""
assert os.getcwd() == wd_setup
@ -141,13 +145,15 @@ def test_unc_pushdpopd( xonsh_builtins, wd_setup):
unc_popd([])
assert wd_setup == os.getcwd(), "popd returned cwd to expected dir"
def push_and_check( unc_path, drive_letter):
o, e, c = unc_pushd( [unc_path])
assert c == 0
##is dirs assert o is None
# is dirs assert o is None
assert e is None or len(e) == 0
assert os.path.splitdrive( os.getcwd())[0].casefold() == drive_letter.casefold()
@pytest.mark.skipif( not ON_WINDOWS, reason="Windows-only UNC functionality")
def test_unc_cases( xonsh_builtins, wd_setup, shares_setup):
"""unc_pushd/popd handle """
@ -163,6 +169,7 @@ def test_unc_cases( xonsh_builtins, wd_setup, shares_setup):
unc_popd([])
assert os.getcwd() == old_cwd
@pytest.mark.skipif( not ON_WINDOWS, reason="Windows-only UNC functionality")
def test_unc_repush_to_temp_driveletter( wd_setup, shares_setup):
"""verify popd doesn't unmap temp drive letter until earliest reference to drive is popped from dirstack."""

View file

@ -1,22 +1,31 @@
"""Utilities for handling UNC (\\node\share\...) paths in PUSHD (on Windows)
""" Handle UNC (\\node\share\...) paths in PUSHD (on Windows)
Current viersion of windows CMD.EXE enforce a check for working directory set to a UNC path
and by default issue the error:
>>>
CMD.EXE was started with the above path as the current directory.
UNC paths are not supported. Defaulting to Windows directory.
This extension toggles a (Windows) registry entry `DisableUNCCheck` which, duh, disables the UNC check
in `CMD.EXE`. When you do so, `os.getwd` will report a natural looking path and most programs (including
`CMD.EXE` most of the time) will do the right thing.
Apparently, MS is still worried about child processes created by such a shell which continue running after the shell closes.
By default, `CMD.EXE` refuses to run with working directory set to a UNC path. It issues this warning:
This module contains 2 ways to deal with it, because neither is perfect.
> CMD.EXE was started with the above path as the current directory.
> UNC paths are not supported. Defaulting to Windows directory.
and proceeds to run with the working directory set to `%WINDIR%`. This is an arguably brain-dead
thing to do (%WINDIR% being either unwritable for the ordinary user or hazardous for the admin user to make
unexpected changes in), but it has been the behavior since 1994. Instead, they hacked `PUSHD` to create
a temporary mapped drive (which behavior is now available in `xonsh.dirstack` too).
MS professes to be worried about child processes created by such a shell which might continue
to run after the shell closes. I have never seen a problem, but have not tried to create it.
Background: see https://support.microsoft.com/en-us/kb/156276
"""
# (of all the brain-dead things to do! CMD.EXE doesn't fail, it warns and proceeds with a dangerous default, C:\windows.
# It would be much better to fail if they really mean it, and really can't fix the problem (which they don't describe, and may have been fixed long since....)
# It would be much better to fail if they really mean it, and really can't fix the problem
# (which they don't describe, and may have been fixed long since....)
# And, if you must proceed , %WINDIR% is probably the worst fallback to use!
# Either (ordinary) user will fail reading or trying and failing to write a file, or (privileged) user may succeed in writing, possibly clobbering something important.
# Either (ordinary) user will fail reading or trying and failing to write a file,
# or (privileged) user may succeed in writing, possibly clobbering something important.
# -- there. I feel much better now. To proceed...)
import argparse
@ -31,15 +40,17 @@ from xonsh.dirstack import pushd, popd, DIRSTACK
_uncpushd_choices = dict(enable=1, disable=0, show=None)
@lazyobject
def uncpushd_parser():
parser = argparse.ArgumentParser(prog="uncpushd", description='Enable or disable CMD.EXE check for UNC path.')
parser.add_argument('action', choices=_uncpushd_choices, default='show')
return parser
def uncpushd(args=None, stdin=None):
"""Fix alternative 1: configure CMD.EXE to bypass the chech for UNC path.
Set, Clear or display current value for DisableUNCCheck in registry, which controls
Set, Clear or display current value for DisableUNCCheck in registry, which controls
whether CMD.EXE complains when working directory set to a UNC path.
In new windows install, value is not set, so if we cannot query the current value, assume check is enabled
@ -69,12 +80,13 @@ def uncpushd(args=None, stdin=None):
else: # set to 1 or 0
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'software\microsoft\command processor')
winreg.SetValueEx( key, 'DisableUNCCheck', 0, winreg.REG_DWORD, 1 if _uncpushd_choices[args.action] else 0)
winreg.CloseKey( key)
winreg.SetValueEx(key, 'DisableUNCCheck', 0, winreg.REG_DWORD, 1 if _uncpushd_choices[args.action] else 0)
winreg.CloseKey(key)
return None, None, 0
except OSError as e:
return None, str(OSError), 1
def _do_subprocess( *args)->tuple:
"""
Invoke `args`, outputs and return code
@ -88,7 +100,7 @@ def _do_subprocess( *args)->tuple:
return_code:int - return code from subprocess
"""
try:
return subprocess.check_output( *args, universal_newlines=True, stderr=subprocess.STDOUT) \
return subprocess.check_output(*args, universal_newlines=True, stderr=subprocess.STDOUT) \
, None \
, 0
except subprocess.CalledProcessError as e:
@ -97,6 +109,7 @@ def _do_subprocess( *args)->tuple:
_unc_tempDrives = {} # drivePart: tempDriveLetter for temp drive letters we create
def unc_pushd( args, stdin=None):
"""Fix 2: Handle pushd when argument is a UNC path (\\<server>\<share>...) the same way CMD.EXE does.
Create a temporary drive letter mapping, then pushd (via built-in) to that path.
@ -107,7 +120,7 @@ def unc_pushd( args, stdin=None):
if not ON_WINDOWS or args is None or args[0] is None or args[0][0] not in (os.sep, os.altsep):
return pushd(args, stdin)
else:
share, relPath = os.path.splitdrive( args[0])
share, rel_path = os.path.splitdrive(args[0])
if share[0] not in (os.sep, os.altsep):
return pushd(args, stdin)
else: # path begins \\ or //...
@ -119,16 +132,19 @@ def unc_pushd( args, stdin=None):
return co
else:
_unc_tempDrives[dpath] = share
return pushd( [os.path.join( dpath, relPath )], stdin)
return pushd( [os.path.join( dpath, rel_path )], stdin)
def _coalesce( a1, a2):
""" return a1 + a2, treating None as ''. But return None if both a1 and a2 are None."""
retVal = ''
ret_val = ''
if a1 is not None:
retVal = a1
ret_val = a1
if a2 is not None:
retVal += a2
ret_val += a2
return ret_val if ret_val != '' else None
return retVal if retVal != '' else None
def unc_popd( args, stdin=None):
"""Handle popd from a temporary drive letter mapping established by `unc_pushd`
@ -143,9 +159,9 @@ def unc_popd( args, stdin=None):
else:
co = None, None, 0
env = builtins.__xonsh_env__
drive, relPath = os.path.splitdrive( env['PWD'].casefold()) ## os.getcwd() uppercases drive letters on Windows?!
drive, rel_path = os.path.splitdrive( env['PWD'].casefold()) ## os.getcwd() uppercases drive letters on Windows?!
pdResult = popd( args, stdin) # pop first
pd_result = popd( args, stdin) # pop first
if drive in _unc_tempDrives:
for p in [os.getcwd().casefold()] + DIRSTACK: #hard_won: what dirs command shows is wd + contents of DIRSTACK
@ -155,6 +171,6 @@ def unc_popd( args, stdin=None):
_unc_tempDrives.pop(drive)
co = _do_subprocess(['net', 'use', drive, '/delete'])
return _coalesce( pdResult[0], co[0])\
, _coalesce( pdResult[1], co[1])\
, pdResult[2]
return _coalesce(pd_result[0], co[0])\
, _coalesce(pd_result[1], co[1])\
, pd_result[2]