mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-05 17:00:58 +01:00
first draft
This commit is contained in:
parent
37de453104
commit
13338c0ff4
3 changed files with 166 additions and 50 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Add table
Reference in a new issue