2017-02-11 12 views
3

Права, это чрезвычайно неясная ...В Windows, что такое python launcher 'py', что позволяет control-C пересекать группы процессов?

Так на Windows, когда вы нажмете Ctrl + C, чтобы прервать программу консоли, это посылает CTRL_C_EVENT процесс. Вы также можете сделать это вручную через GenerateConsoleCtrlEvent. В Python, os.kill действует как оболочка вокруг C-уровня GenerateConsoleCtrlEvent, и позволяет нам послать CTRL_C_EVENT к текущему процессу, выполнив:

os.kill(os.getpid(), signal.CTRL_C_EVENT) 

Однако, это не просто идти на текущий процесс - это фактически относится ко всей «группе процессов», частью которой является этот процесс.

У меня есть набор тестов, который вызывает os.kill, как вы видите выше, как часть некоторых тестов, чтобы убедиться в правильности управления C-обработкой моей программы. Однако при запуске этого набора тестов на appveyor это создает проблему, потому что, видимо, какая-то инфраструктура appveyor находится в одной и той же «группе прогресса» и разбивается.

Решение состоит в том, что нам нужно установить набор тестов с установленным флагом CREATE_NEW_PROCESS_GROUP, так что его CTRL_C_EVENT с не «утечка» родительского элемента. Это легко сделать. BUT ...

Если я использую CREATE_NEW_PROCESS_GROUP и запускаю дочерний скрипт с использованием python whatever.py, то он работает так, как ожидалось: CTRL_C_EVENT ограничен ребенком.

Если я использую CREATE_NEW_PROCESS_GROUP и запустить скрипт ребенка с помощью py whatever.py (то есть, используя python launcher, который, как предполагается, эквивалентно запуску python непосредственно), то CREATE_NEW_PROCESS_GROUP, кажется, не имеют какой-либо эффект: CTRL_C_EVENT влияет на родителя, как Что ж!

Вот минимальный пример программы, которая использует только os.kill на себя, а затем проверяет, что он работал (незначительные морщины: CREATE_NEW_PROCESS_GROUP наборы CTRL_C_EVENT игнорируются в дочерних процессов, так что есть немного пуха здесь с помощью SetConsoleCtrlHandler ООН-игнорировать его) : https://github.com/njsmith/appveyor-ctrl-c-test/blob/master/a.py

Вот сценарий обертки я использую для запуска выше программы: https://github.com/njsmith/appveyor-ctrl-c-test/blob/master/run-a.py

Если сценарий обертки работает python a.py, то все работает. Если сценарий оболочки работает py a.py, тогда сценарий оболочки получает KeyboardInterrupt.

Так что мой вопрос: что, черт возьми, происходит здесь? Что делает пусковая установка py по-разному от python, что приводит к тому, что CTRL_C_EVENT «утечка» в родительский процесс, даже если он находится в другой группе процессов? И как это возможно?

(я первоначально обнаружил это потому, что работает pytest a.py действует как py a.py, т.е. нарушается, но python -m pytest a.py работы, предположительно, потому что точка входа pytestuses the py launcher.)

+0

В соответствии с документацией (см. Первую ссылку в своем вопросе) вы не можете указать группу процессов при отправке сигнала CTRL + C, т. Е. 'DwProcessGroupId' всегда должен быть равен нулю, который посылает сигнал на каждый процесс которые совместно используют консоль вызывающего процесса. Так что вызов 'python' ведет себя странно, а не вызов' py'. :-) –

+0

Да, правда. Однако это явно не то, что на самом деле происходит. Повторяя это снова, я понял, что это может объяснить это, если 'dwProcessGroupId = 0' означает отправить' CTRL_C_EVENT' все процессы на консоли, независимо от идентификатора группы процессов * и *, если вы установите 'dwProcessGroupId' в PID процесса, который * не является лидером группы, действует как установка его на 0. Тогда особая вещь о 'py' заключается в том, что мы делаем' py' лидером группы, но 'py' порождает' python' для запуска кода, поэтому 'python' не является лидером группы. –

+0

@eryksun: моя текущая догадка заключается в том, что ошибка заключается в том, что 'GenerateConsoleCtrlEvent' обрабатывает недопустимый идентификатор группы как бы 0 (что означает« все группы »). Я согласен с тем, что способ «os.kill» обернуть его довольно запутанным, но это не проблема. –

ответ

1

Каждый процесс в группе процессов. Он либо наследует группу своего родителя, либо создается как лидер новой группы с помощью флага создания CREATE_NEW_PROCESS_GROUP.Насколько мне известно, GenerateConsoleCtrlEvent - это единственный API, который использует группы процессов, и нет API для запроса идентификатора группы процессов. Вы можете получить его от ProcessParameters в PEB, но это связано с использованием недокументированных внутренних структур. Никто не должен этого делать, поэтому GenerateConsoleCtrlEvent должен отправлять только группе 0 для трансляции события или дочернего процесса, который, как вы знаете, был создан как новая группа.

Проблема, которую вы раскрыли здесь, заключается в том, что отправка события в процесс, прикрепленный к консоли, но не лидер группы, бесшумно обрабатывается, как если бы целью была группа 0. В частности, в вашем случае вы запуск py.exe в качестве лидера группы, а затем попытка отправить CTRL_C_EVENT на python.exe, то есть os.getpid(). В этом случае вам придется отправить os.getppid().

Эта проблема характерна для сценариев Python для Windows из-за запутанной реализации os.kill. Он объединяет идентификаторы процессов и идентификаторы групп процессов. Было бы менее запутанно, если бы GenerateConsoleCtrlEvent использовался для os.killpg (в настоящее время он не реализован в Windows) и TerminateProcess используется только для os.kill.

Опытный случайный зависает при звонке os.kill(os.getpid(), signal.CTRL_C_EVENT);time.sleep(10) может быть из-за состояния гонки. time.sleep реализован в pysleep в модулях/timemodule.c. В Windows, когда вызывается в основном потоке, он ждет события, которое устанавливается обработчиком сигнала для SIGINT (но не по умолчанию) SIGBREAK. Возможная гонка здесь состоит в том, что pysleep сбрасывает событие, прежде чем ждать его. Обработчик сигналов выполняется в новом потоке, и иногда он, возможно, уже установил событие до того, как pysleep сбрасывает его. Это возможно, поскольку выполнение байт-кода CPython относительно медленное. Тем не менее, я бы ожидать, что это будет близко гонка, потому что есть много этапов, чтобы создать нить обработчика управления, как показано в следующем обзоре показаны для ОС Windows 10.

1. Console Client -- Main Thread (python.exe) 
    kernelbase!GenerateConsoleCtrlEvent 
    kernelbase!ConsoleCallServer 
    ntdll!NtDeviceIoControlFile 

2. Device Driver (condrv.sys) 
    condrv!CdpDispatchDeviceControl 

    3a. Console Server (conhost.exe) 
     ntdll!NtDeviceIoControlFile 
     conhostv2!SrvGenerateConsoleCtrlEvent 
     conhostv2!ProcessCtrlEvents 
     user32!ConsoleControl 
     ntdll!CsrClientCallServer 
     ntdll!NtRequestWaitReplyPort (ALPC) 

    3b. Windows Server (csrss.exe) 
     ntdll!NtAlpcSendWaitReceivePort 
     winsrv!SrvEndTask 
     winsrv!CreateCtrlThread 
     ntdll!NtCreateThreadEx (Control Thread) 

    3c. Console Client -- Control Thread (python.exe) 
     kernelbase!CtrlRoutine 
     ucrtbase!ctrlevent_capture (Emulate SIGINT) 
     python36!signal_handler 
     kernelbase!SetEvent (SIGINT Event -- Race with step 4) 

4. Console Client -- Main Thread (python.exe) 
    python36!pysleep 
    kernelbase!ResetEvent (SIGINT Event -- Race with step 3c) 
    kernelbase!WaitForSingleObjectEx (SIGINT Event) 
    python36!PyErr_CheckSignals  
    python36!signal_default_int_handler 

Если состояние гонки является проблема, то давая time.sleep достаточно времени, чтобы сбросить событие до того, как GenerateConsoleCtrlEvent получает вызов, должен его решить. Попробуйте позвонить os.kill с помощью threading.Timer с небольшой задержкой.

+0

Спасибо, это отличный ответ!И хороший момент о состоянии гонки - я не уверен, что это то, что я ударил, но я [подал ошибку] ​​(https://bugs.python.org/issue30151) в любом случае :-). Я могу представить, что я мог бы быть, если бы он запускал некоторые эвристики в планировщике или что-то в этом роде ... Я могу надежно ударить [это состояние гонки] (https://bugs.python.org/issue30038) b/c по-видимому, записывая в сокет из обработчика сигнала, что другой поток 'select'ing детерминистически переключает управление на этот другой поток ... –