mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-04 08:24:40 +01:00
New: Add sudo
functionality on Windows
This commit is contained in:
parent
ec3b0fb0cc
commit
48dac6e9cd
6 changed files with 178 additions and 9 deletions
|
@ -8,6 +8,7 @@ Current Developments
|
|||
|
||||
* Added a new shell type ``'none'``, used to avoid importing ``readline`` or
|
||||
``prompt_toolkit`` when running scripts or running a single command.
|
||||
* New: `sudo` functionality on Windows through an alias
|
||||
|
||||
**Changed:**
|
||||
|
||||
|
|
|
@ -178,6 +178,14 @@ The following aliases on Windows are expanded to ``['cmd', '/c', alias]``:
|
|||
On Windows, ``which`` is aliased to ``['where']``.
|
||||
|
||||
|
||||
``sudo`` on Windows
|
||||
====================
|
||||
On Windows, if no executables named ``sudo`` are found, Xonsh adds a ``sudo`` alias
|
||||
that poly fills the "run as Admin" behavior with the help of ``ShellExecuteEx`` and
|
||||
``ctypes``. It doesn't support any actual ``sudo`` parameters and just takes the
|
||||
command to run.
|
||||
|
||||
|
||||
``ls``
|
||||
====================
|
||||
The ``ls`` command is aliased to ``['ls', '--color=auto', '-v']`` normally. On Mac OSX
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Aliases for the xonsh shell."""
|
||||
import os
|
||||
import shlex
|
||||
|
||||
import builtins
|
||||
import subprocess
|
||||
from warnings import warn
|
||||
import os
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from xonsh.dirstack import cd, pushd, popd, dirs
|
||||
from xonsh.dirstack import cd, pushd, popd, dirs, _get_cwd
|
||||
from xonsh.jobs import jobs, fg, bg, kill_all_jobs
|
||||
from xonsh.proc import foreground
|
||||
from xonsh.timings import timeit_alias
|
||||
|
@ -253,6 +251,25 @@ if ON_WINDOWS:
|
|||
|
||||
DEFAULT_ALIASES['which'] = ['where']
|
||||
|
||||
if not locate_binary('sudo'):
|
||||
import xonsh.winutils as winutils
|
||||
|
||||
def sudo(args, sdin=None):
|
||||
if len(args) < 1:
|
||||
print('You need to provide an executable to run as Administrator.')
|
||||
return
|
||||
|
||||
cmd = args[0]
|
||||
|
||||
if locate_binary(cmd):
|
||||
return winutils.sudo(cmd, args[1:])
|
||||
elif cmd.lower() in WINDOWS_CMD_ALIASES:
|
||||
return winutils.sudo('cmd', ['/D', '/C', 'CD', _get_cwd(), '&&'] + args)
|
||||
else:
|
||||
print('Cannot find the path for executable "{0}".'.format(cmd))
|
||||
|
||||
DEFAULT_ALIASES['sudo'] = sudo
|
||||
|
||||
elif ON_MAC:
|
||||
DEFAULT_ALIASES['ls'] = ['ls', '-G']
|
||||
else:
|
||||
|
|
|
@ -25,7 +25,6 @@ from xonsh.tools import (
|
|||
)
|
||||
from xonsh.inspectors import Inspector
|
||||
from xonsh.environ import Env, default_env, locate_binary
|
||||
from xonsh.aliases import DEFAULT_ALIASES
|
||||
from xonsh.jobs import add_job, wait_for_active_job
|
||||
from xonsh.proc import (ProcProxy, SimpleProcProxy, ForegroundProcProxy,
|
||||
SimpleForegroundProcProxy, TeePTYProc,
|
||||
|
@ -772,6 +771,9 @@ def load_builtins(execer=None, config=None, login=False):
|
|||
builtins.evalx = None if execer is None else execer.eval
|
||||
builtins.execx = None if execer is None else execer.exec
|
||||
builtins.compilex = None if execer is None else execer.compile
|
||||
|
||||
# Need this inline/lazy import here since we use locate_binary that relies on __xonsh_env__ in default aliases
|
||||
from xonsh.aliases import DEFAULT_ALIASES
|
||||
builtins.default_aliases = builtins.aliases = Aliases(DEFAULT_ALIASES)
|
||||
if login:
|
||||
builtins.aliases.update(load_foreign_aliases(issue_warning=False))
|
||||
|
|
|
@ -687,10 +687,15 @@ def _is_executable_file(path):
|
|||
|
||||
|
||||
def yield_executables_windows(directory, name):
|
||||
normalized_name = os.path.normcase(name)
|
||||
extensions = builtins.__xonsh_env__.get('PATHEXT')
|
||||
for a_file in os.listdir(directory):
|
||||
base_name, ext = os.path.splitext(a_file)
|
||||
if name == base_name and ext.upper() in extensions:
|
||||
normalized_file_name = os.path.normcase(a_file)
|
||||
base_name, ext = os.path.splitext(normalized_file_name)
|
||||
|
||||
if (
|
||||
normalized_name == base_name or normalized_name == normalized_file_name
|
||||
) and ext.upper() in extensions:
|
||||
yield os.path.join(directory, a_file)
|
||||
|
||||
|
||||
|
@ -708,9 +713,15 @@ def locate_binary(name):
|
|||
if os.path.isfile(name) and name != os.path.basename(name):
|
||||
return name
|
||||
|
||||
directories = builtins.__xonsh_env__.get('PATH')
|
||||
|
||||
# Windows users expect t obe able to execute files in the same directory without `./`
|
||||
if ON_WINDOWS:
|
||||
directories = [_get_cwd()] + directories
|
||||
|
||||
try:
|
||||
return next(chain.from_iterable(yield_executables(directory, name) for
|
||||
directory in builtins.__xonsh_env__.get('PATH') if os.path.isdir(directory)))
|
||||
directory in directories if os.path.isdir(directory)))
|
||||
except StopIteration:
|
||||
return None
|
||||
|
||||
|
|
130
xonsh/winutils.py
Normal file
130
xonsh/winutils.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
"""
|
||||
This file is based on the code from https://github.com/JustAMan/pyWinClobber/blob/master/win32elevate.py
|
||||
|
||||
Copyright (c) 2013 by JustAMan at GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
import ctypes
|
||||
from ctypes.wintypes import HANDLE, BOOL, DWORD, HWND, HINSTANCE, HKEY
|
||||
from ctypes import c_ulong, c_char_p, c_int, c_void_p
|
||||
|
||||
P_HANDLE = ctypes.POINTER(HANDLE)
|
||||
P_WORD = ctypes.POINTER(DWORD)
|
||||
|
||||
CloseHandle = ctypes.windll.kernel32.CloseHandle
|
||||
CloseHandle.argtypes = (HANDLE, )
|
||||
CloseHandle.restype = BOOL
|
||||
|
||||
GetActiveWindow = ctypes.windll.user32.GetActiveWindow
|
||||
GetActiveWindow.argtypes = ()
|
||||
GetActiveWindow.restype = HANDLE
|
||||
|
||||
TOKEN_READ = 0x20008
|
||||
|
||||
|
||||
class ShellExecuteInfo(ctypes.Structure):
|
||||
_fields_ = [
|
||||
('cbSize', DWORD),
|
||||
('fMask', c_ulong),
|
||||
('hwnd', HWND),
|
||||
('lpVerb', c_char_p),
|
||||
('lpFile', c_char_p),
|
||||
('lpParameters', c_char_p),
|
||||
('lpDirectory', c_char_p),
|
||||
('nShow', c_int),
|
||||
('hInstApp', HINSTANCE),
|
||||
('lpIDList', c_void_p),
|
||||
('lpClass', c_char_p),
|
||||
('hKeyClass', HKEY),
|
||||
('dwHotKey', DWORD),
|
||||
('hIcon', HANDLE),
|
||||
('hProcess', HANDLE)
|
||||
]
|
||||
|
||||
def __init__(self, **kw):
|
||||
ctypes.Structure.__init__(self)
|
||||
self.cbSize = ctypes.sizeof(self)
|
||||
for field_name, field_value in kw.items():
|
||||
setattr(self, field_name, field_value)
|
||||
|
||||
PShellExecuteInfo = ctypes.POINTER(ShellExecuteInfo)
|
||||
|
||||
ShellExecuteEx = ctypes.windll.Shell32.ShellExecuteExA
|
||||
ShellExecuteEx.argtypes = (PShellExecuteInfo, )
|
||||
ShellExecuteEx.restype = BOOL
|
||||
|
||||
WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
|
||||
WaitForSingleObject.argtypes = (HANDLE, DWORD)
|
||||
WaitForSingleObject.restype = DWORD
|
||||
|
||||
# SW_HIDE = 0
|
||||
SW_SHOW = 5
|
||||
SEE_MASK_NOCLOSEPROCESS = 0x00000040
|
||||
SEE_MASK_NO_CONSOLE = 0x00008000
|
||||
INFINITE = -1
|
||||
|
||||
|
||||
def wait_and_close_handle(process_handle):
|
||||
"""
|
||||
Waits till spawned process finishes and closes the handle for it
|
||||
|
||||
:param process_handle: The Windows handle for the process
|
||||
:type process_handle: HANDLE
|
||||
"""
|
||||
WaitForSingleObject(process_handle, INFINITE)
|
||||
CloseHandle(process_handle)
|
||||
|
||||
|
||||
def sudo(executable, args=None):
|
||||
"""
|
||||
This will re-run current Python script requesting to elevate administrative rights.
|
||||
|
||||
:param executable: The path/name of the executable
|
||||
:type executable: str
|
||||
:param args: The arguments to be passed to the executable
|
||||
:type args: list
|
||||
"""
|
||||
if not args:
|
||||
args = []
|
||||
|
||||
execute_info = ShellExecuteInfo(
|
||||
fMask=SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE,
|
||||
hwnd=GetActiveWindow(),
|
||||
lpVerb=b'runas',
|
||||
lpFile=executable.encode('utf-8'),
|
||||
lpParameters=subprocess.list2cmdline(args).encode('utf-8'),
|
||||
lpDirectory=None,
|
||||
nShow=SW_SHOW
|
||||
)
|
||||
|
||||
if not all(stream.isatty() for stream in (sys.stdin, sys.stdout, sys.stderr)):
|
||||
# TODO: Some streams were redirected, we need to manually work them
|
||||
raise NotImplementedError("Redirection is not supported")
|
||||
|
||||
if not ShellExecuteEx(ctypes.byref(execute_info)):
|
||||
raise ctypes.WinError()
|
||||
|
||||
wait_and_close_handle(execute_info.hProcess)
|
||||
|
||||
|
||||
__all__ = ('sudo', )
|
Loading…
Add table
Reference in a new issue