【问题标题】:Detect if python program is executed via Windows GUI (double-click) vs command prompt检测python程序是否通过Windows GUI(双击)vs命令提示符执行
【发布时间】:2019-08-05 21:54:27
【问题描述】:

背景
我有一个 Python 3.5 控制台程序通过 pyinstaller 编译成 Windows 可执行文件。

问题

  • 当通过命令提示符执行时,我希望我的程序使用提供的任何参数(可能没有)运行。
  • 当通过操作系统的 GUI 执行时(即通过在 Windows 上的 Windows 资源管理器中双击 .exe 等),我希望我的程序提示用户输入。我还需要在退出之前暂停我的程序,以便用户可以阅读输出。

如何检测这些不同的场景?

约束

  1. 可执行文件必须能够在准系统(即全新安装)Windows/RedHat 机器上运行。
  2. 编译后的可执行文件必须是单个文件,并且不能依赖未打包在已编译可执行文件内的其他文件(pyinstaller 允许将文件打包在已编译可执行文件内)。
  3. 该程序可能依赖于第 3 方 python 包。

我尝试过的事情

【问题讨论】:

  • 我假设你的可执行文件是一个控制台应用程序,因为sys.stdin.isatty() 是真的。默认情况下,如果父级有控制台,它将继承父级的控制台。如果没有命令行参数并且父级 not 连接到您的控制台,您可以提示用户。调用 GetConsoleProcessList 以获取附加到控制台的进程的 PID。我们可以使用 ctypes 或者 PyWin32 来调用这个函数。
  • @eryksun 感谢您的回复。是的,我的可执行文件是一个控制台应用程序。我不熟悉 Windows API 调用;也许您可以通过示例发布完整的答案?

标签: python python-3.x windows pyinstaller


【解决方案1】:

计算附加到控制台的进程

Windows API documentation for GetConsoleProcessList

import ctypes

# Load kernel32.dll
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
# Create an array to store the processes in.  This doesn't actually need to
# be large enough to store the whole process list since GetConsoleProcessList()
# just returns the number of processes if the array is too small.
process_array = (ctypes.c_uint * 1)()
num_processes = kernel32.GetConsoleProcessList(process_array, 1)
# num_processes may be 1 if your compiled program doesn't have a launcher/wrapper.
if num_processes == 2:
    input('Press ENTER to continue...')

【讨论】:

    【解决方案2】:

    原来有一种简单而简洁的方法可以在 Windows 上确定这一点。 https://stackoverflow.com/a/14394730/3508142

    PROMPT 环境变量定义命令提示符中的提示文本。 https://ss64.com/nt/prompt.html

    # If the program was started via the GUI (i.e. by double-clicking the executable),
    # then prevent the console window from closing automatically.
    if os.name == 'nt' and 'PROMPT' not in os.environ:
        input('Press ENTER to continue...')
    

    【讨论】:

    • 这不可靠。在用户或系统环境变量中可能定义了一个自定义的PROMPT 变量。为此,我们没有可以依赖的环境变量。使用控制台 API 确保您的是附加到控制台的唯一进程。 (或者如果它解压缩到临时目录,也可能是冻结的可执行文件。)获取进程数是直截了当的,例如kernel32 = ctypes.WinDLL('kernel32', use_last_error=True);unused = (ctypes.c_uint * 1)();num_proceses = kernel32.GetConsoleProcessList(unused, 1).
    • @eryksun,我同意自定义 PROMPT 变量定义可能会导致误报。但是,这样做也会改变用户的prompt text,这似乎不太可能,所以我可以接受这种风险。就使用 ctypes 而言,它似乎只是简单地计算祖先进程的数量。如果我的程序被另一个程序执行,num_processes 的值将被夸大。因此,您的方法似乎在所有情况下都无法区分 GUI 和命令提示符执行。我错过了什么吗?也许一个完整的答案可以澄清事情?
    • 我给你的代码sn-p告诉你控制台附加了多少进程。它与祖先进程无关。如果您知道您的脚本应该将 1 个进程(或者如果有启动器,则可能是 2 个)附加到控制台,那么启动时的附加进程意味着控制台已被继承,因此只需使用提供的任何命令行选项(如果有),并且不要担心保持控制台活着。否则提示输入,并在退出时执行subprocess.Popen('cmd.exe') 之类的操作以保持控制台处于活动状态。
    • 您是正确的,GetConsoleProcessList 返回附加到控制台的进程数,而不是祖先进程。感谢您的澄清。此外,为澄清起见,我编译的程序似乎确实有一个单独的启动器进程。我做了更多调查并意识到如果num_processes == 2然后我的程序是从GUI执行的,否则更高的数字意味着它要么从命令提示符运行要么被脚本调用(我同意在这种情况下它应该不要暂停)。感谢@eryksun 的帮助,我将修改我的答案以包含您的建议。
    • 我刚刚对此进行了测试,虽然从cmd.exe 开始的python 进程有os.environ['PROMPT'],但从Powershell 5 开始的python 进程没有。
    猜你喜欢
    • 2022-01-10
    • 2014-01-21
    • 1970-01-01
    • 2014-07-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多