【问题标题】:How do I get the parent shell's path on windows in pure python?如何在纯 python 中的 windows 上获取父 shell 的路径?
【发布时间】:2021-01-26 14:11:21
【问题描述】:

以下将返回启动当前进程的 shell 和 shell 的完整路径。但是,它使用了一个充满 c 扩展的 python 库。有时 shell 会启动 shell 等。我只是在寻找作为 shell 的“最近的祖先”进程。

我如何只用纯 python 做到这一点? (即没有c扩展,使用windll.kernel32等就可以了——当然在某些时候要获取进程信息代码必须访问特定于平台的本机代码,它只需要是已经埋在python标准库中的东西,不需要编译 c)

Shellingham at the moment can't do this.

from typing import Tuple, List

import os

import psutil

SHELL_NAMES = {
    'sh', 'bash', 'dash', 'ash',    # Bourne.
    'csh', 'tcsh',                  # C.
    'ksh', 'zsh', 'fish',           # Common alternatives.
    'cmd', 'powershell', 'pwsh',    # Microsoft.
    'elvish', 'xonsh',              # More exotic.
}

def find_shell_for_windows() -> Tuple[str,str]:
    names_paths:List[Tuple[str,str]]=[]
    current_process = psutil.Process(os.getppid())
    process_name, process_path = current_process.name(), current_process.exe()
    names_paths.append((process_name, process_path))
    for parent in current_process.parents():
        names_paths.append((parent.name(), parent.exe()))
    for n,p in names_paths:
        if n.lower() in SHELL_NAMES or n.lower().replace(".exe","") in SHELL_NAMES:
            return n,p
    return ["",""]

if __name__ == '__main__':
    print(find_shell_for_windows())

【问题讨论】:

  • 什么是外壳?
  • 例如 cmd.ex 或 bash.exe - 从 shell 启动的应用程序(进程)将有一个带有名称和路径的父进程,例如bash.exe,c:\program files\etc 的路径...
  • 那么,如果我复制一份 Bash 并将其命名为 shab.exe,它就不是 shell?
  • 正确。 shell 的名称列在代码 sn-p、sh、bash 等中。如果进程名为 shab.exe 或其他任何名称,则可以安全地假设它不是 shell。我们不会尝试处理用户试图隐藏他们正在使用的 shell 的情况。
  • import os 用于获取进程 ID 的 os.getppid()。如果您已经有路径,看起来relative_to 用于路径操作。诀窍是首先获取启动当前进程的 shell 的路径。

标签: python windows shell


【解决方案1】:

这个问题是关于寻找一种方法来遍历祖先进程列表并找到第一个可执行文件与硬编码名称列表中的项目匹配的,希望它是用户的“shell”。在我回答之前,以下是为什么这是一件绝对愚蠢和无用的事情的一些原因:

  • 您要查找的可执行文件的名称可能与您期望的不同。在对该问题的评论中,我举了一个将 Bash 可执行文件命名为 shab.exe 的示例,当用户试图“隐藏”他们的外壳。但即使用户没有任何“恶意”意图“隐藏”某些内容,这种情况也可能发生。例如,用户可能安装了具有不同可执行文件名称(bash-4.00.exegitbash.exe 等)的多个版本的 Bash,以测试他们的脚本是否与所有版本兼容。您要枚举代码中的每一个可能的名称吗?或者您要匹配名称中带有sh 的所有可执行文件,然后交叉手指确认没有任何误报?
  • 相反,just because an executable has the name you expect, it doesn’t mean it will have the behaviour you expect。在 Windows 上更是如此,没有真正的标准化可执行文件名称集,应该始终具有特定的、规定的行为。如果您不加选择地将cmdbashfishxonsh 之类的程序放在“shell”的名称下,这尤其是正确的:这些程序自己接受不同的语法命令行及其各自的脚本语言。除非您只想启动 shell 以供用户交互,否则您将寻找 shell 的特定 种类 - 无论是兼容 POSIX 的 shell,还是 DOS-派生的 shell 像 cmd 或其他完全一样的东西——为了利用它的特殊行为。仅仅知道它是一个“外壳”实际上并不能告诉你任何有用的东西。我们不要忘记,并非所有的 shell 都是命令行——毕竟 Windows 资源管理器是一个 shell。
  • 即使可执行文件的名称排列完美,硬编码列表也不可能详尽无遗。提问者的列表已经省略了tcmdtclsh。我听说一些疯狂的人将 Python 本身用作外壳——你是谁来阻止他们?如果出现新的外壳,则必须将其添加为列表中的另一个条目;让我们希望还有人记得它当时在哪里。
  • 尽管为了论证,我们只对命令行 shell 感兴趣,而忽略其他所有内容。如果脚本是从本身是从 Bash 启动的 Explorer 进程启动的呢?该脚本将忽略 Explorer 进程并选择 Bash 作为“shell”,即使它显然是 Explorer 而不是 Bash 启动了该脚本。这是正确的还是期望的?我认为答案远非显而易见。

但是,如果上述内容并不能阻止您完成这项徒劳的任务(或者您可能出于 更明智的目的而想做类似的事情),那么无论如何,您可以通过以下方式完成这个无意义的事情:

import ctypes, ctypes.wintypes, contextlib

k32 = ctypes.windll.kernel32

INVALID_HANDLE_VALUE = ctypes.wintypes.HANDLE(-1).value
ERROR_NO_MORE_FILES = 18
ERROR_INSUFFICIENT_BUFFER = 122
TH32CS_SNAPPROCESS = 2
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000

def _check_handle(error_val=0):
    def check(ret, func, args):
        if ret == error_val:
            raise ctypes.WinError()
        return ret
    return check

def _check_expected(expected):
    def check(ret, func, args):
        if ret:
            return True
        code = ctypes.GetLastError()
        if code == expected:
            return False
        raise ctypes.WinError(code)
    return check

class ProcessEntry32(ctypes.Structure):
    _fields_ = (
        ('dwSize'             , ctypes.wintypes.DWORD),
        ('cntUsage'           , ctypes.wintypes.DWORD),
        ('th32ProcessID'      , ctypes.wintypes.DWORD),
        ('th32DefaultHeapID'  , ctypes.POINTER(ctypes.wintypes.ULONG)),
        ('th32ModuleID'       , ctypes.wintypes.DWORD),
        ('cntThreads'         , ctypes.wintypes.DWORD),
        ('th32ParentProcessID', ctypes.wintypes.DWORD),
        ('pcPriClassBase'     , ctypes.wintypes.LONG),
        ('dwFlags'            , ctypes.wintypes.DWORD),
        ('szExeFile'          , ctypes.wintypes.CHAR * ctypes.wintypes.MAX_PATH),
    )

k32.CloseHandle.argtypes = \
    (ctypes.wintypes.HANDLE,)
k32.CloseHandle.restype = ctypes.wintypes.BOOL

k32.CreateToolhelp32Snapshot.argtypes = \
    (ctypes.wintypes.DWORD, ctypes.wintypes.DWORD)
k32.CreateToolhelp32Snapshot.restype = ctypes.wintypes.HANDLE
k32.CreateToolhelp32Snapshot.errcheck = _check_handle(INVALID_HANDLE_VALUE)

k32.Process32First.argtypes = \
    (ctypes.wintypes.HANDLE, ctypes.POINTER(ProcessEntry32))
k32.Process32First.restype = ctypes.wintypes.BOOL
k32.Process32First.errcheck = _check_expected(ERROR_NO_MORE_FILES)

k32.Process32Next.argtypes = \
    (ctypes.wintypes.HANDLE, ctypes.POINTER(ProcessEntry32))
k32.Process32Next.restype = ctypes.wintypes.BOOL
k32.Process32Next.errcheck = _check_expected(ERROR_NO_MORE_FILES)

k32.GetCurrentProcessId.argtypes = ()
k32.GetCurrentProcessId.restype = ctypes.wintypes.DWORD

k32.OpenProcess.argtypes = \
    (ctypes.wintypes.DWORD, ctypes.wintypes.BOOL, ctypes.wintypes.DWORD)
k32.OpenProcess.restype = ctypes.wintypes.HANDLE
k32.OpenProcess.errcheck = _check_handle(INVALID_HANDLE_VALUE)

k32.QueryFullProcessImageNameW.argtypes = \
    (ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD, ctypes.wintypes.LPWSTR, ctypes.wintypes.PDWORD)
k32.QueryFullProcessImageNameW.restype = ctypes.wintypes.BOOL
k32.QueryFullProcessImageNameW.errcheck = _check_expected(ERROR_INSUFFICIENT_BUFFER)

@contextlib.contextmanager
def Win32Handle(handle):
    try:
        yield handle
    finally:
        k32.CloseHandle(handle)

def enum_processes():
    with Win32Handle(k32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)) as snap:
        entry = ProcessEntry32()
        entry.dwSize = ctypes.sizeof(entry)
        ret = k32.Process32First(snap, entry)
        while ret:
            yield entry
            ret = k32.Process32Next(snap, entry)

def get_full_path(proch):
    size = ctypes.wintypes.DWORD(ctypes.wintypes.MAX_PATH)
    while True:
        path_buff = ctypes.create_unicode_buffer('', size.value)
        if k32.QueryFullProcessImageNameW(proch, 0, path_buff, size):
            return path_buff.value
        size.value *= 2

SHELLS = frozenset((
    b'sh.exe', b'bash.exe', b'dash.exe', b'ash.exe',
    b'csh.exe', b'tcsh.exe',
    b'ksh.exe', b'zsh.exe', b'fish.exe',
    b'cmd.exe', b'powershell.exe', b'pwsh.exe',
    b'elvish.exe', b'xonsh.exe',
))

def find_shell_for_windows():
    proc_map = {
        proc.th32ProcessID: (proc.th32ParentProcessID, proc.szExeFile)
        for proc in enum_processes()
    }

    pid = proc_map[k32.GetCurrentProcessId()][0]
    proc = proc_map[pid]
    while proc:
        ppid, name = proc
        if name in SHELLS:
            with Win32Handle(k32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)) as proch:
                return get_full_path(proch)
        pid, proc = ppid, proc_map.get(ppid)

if __name__ = '__main__':
    print(find_shell_for_windows())

上面的内容完全符合要求,仅使用ctypes,并且应该可以在 Windows Vista 及更高版本上运行(在 Windows 7 上使用 Python 3.8 进行测试)。对于较旧的 Windows 版本,可能需要更改一些 Win32 调用(最重要的是,QueryFullProcessImageNameW)。

【讨论】:

  • 哇,这正是我一直在寻找的东西,我会在结束所有这些会议后立即尝试一下 :-)
  • 哦,我不同意这是愚蠢的,但poetry 依赖于这种逻辑,这在 Windows 上是错误的。这似乎是让维护人员修复它的更简单方法。
猜你喜欢
  • 1970-01-01
  • 2020-03-05
  • 1970-01-01
  • 2020-12-06
  • 2017-05-20
  • 1970-01-01
  • 1970-01-01
  • 2011-07-15
  • 2012-07-10
相关资源
最近更新 更多