Создание небуферизованного STDIN под Windows в Perl

windows #perl #io #nonblocking

#Windows #perl #io #неблокирующий

Вопрос:

Я пытаюсь выполнить обработку ввода (с консоли) в Perl асинхронно. Мой первый подход заключался в использовании IO::Select , но это не работает под Windows.

Затем я наткнулся на post-небуферизованный процессор в Perl, который примерно предполагает это:

 binmode STDIN;
binmode STDOUT;
STDIN->blocking(0) or warn $!;
STDOUT->autoflush(1);

while (1) {
    my $buffer;
    my $read_count = sysread(STDIN, $buffer, 4096);

    if (not defined($read_count)) {
        next;
    } elsif (0 == $read_count) {
        exit 0;
    }
}
 

Это работает, как и ожидалось, для обычных систем Unix, но не для Windows, где на самом sysread деле блокируется. Я протестировал это на Windows 10 с 64-разрядным Strawberry Perl 5.32.1.

Когда вы проверяете возвращаемое значение blocking() (как это сделано в приведенном выше коде), оказывается, что вызов завершается ошибкой со смешным сообщением об ошибке «Была предпринята попытка выполнения операции над чем-то, что не является сокетом».

Редактировать: мое приложение представляет собой шахматный движок, который теоретически может быть запущен в интерактивном режиме в терминале, но обычно взаимодействует через каналы с графическим интерфейсом. Следовательно, Win32::Console не помогает.

Что-то изменилось с момента публикации сообщения в блоге? Автор явно утверждает, что этот подход будет работать для Windows. Любой другой вариант, который я могу использовать, может быть, какой-нибудь модуль из Win32:: пространства имен?

Комментарии:

1. Кстати, perldoc perlport не упоминает blocking() .

2. Win32::Console, см. $CONSOLE->InputChar(1) . Не уверен, блокируется ли он. Если это произойдет, также используйте $CONSOLE->GetEvents()

3. И есть быстрый и грязный подход: используйте поток. Поток может взаимодействовать с основной программой, используя сокет, действующий как канал (например, Win32::SocketPair), что позволяет выполнять неблокирующие чтения в основной программе.

4. Я отредактировал вопрос, чтобы упомянуть, что STDIN обычно это не tty, а канал. Поэтому Win32::Console это не вариант. Кроме того, когда вы выбираете что-то с помощью мыши с помощью консоли, тогда даже GetEvent блоки.

Ответ №1:

Решение, которое я сейчас реализовал в https://github.com/gflohr/Chess-Plisco/blob/main/lib/Chess/Plisco/Engine.pm (поиск метода __msDosSocket() ) может быть изложен следующим образом:

  1. Если Windows обнаружена как операционная система, создайте временный файл в виде сокета домена Unix с IO::Socket::Unix возможностью записи.
  2. Сделайте a fork() , который фактически создает поток в Perl для Windows, потому что в системе нет реального fork() .
  3. В «родительском» создайте другой экземпляр IO::Socket::Unix с тем же путем для чтения.
  4. В «дочернем» считайте из стандартного ввода с getline() помощью . Конечно, это блокирует. Каждая прочитанная строка передается на конец записи сокета.
  5. «Родительский» использует конец чтения сокета в качестве замены стандартного ввода и переводит его в неблокирующий режим. Это работает даже под Windows, потому что это сокет.

С этого момента все работает так же, как и в Unix: все входные данные считываются в неблокирующем режиме с IO::Select помощью .

Вместо доменного сокета Unix, вероятно, разумнее направлять связь через интерфейс обратной связи, потому что в Windows трудно гарантировать, что временный файл будет удален при завершении процесса, поскольку вы не можете разорвать его, пока он используется. В комментариях также указано, что это IO::Socket::UNIX может не работать в более старых версиях Windows, и поэтому сокеты inet, вероятно, более переносимы в использовании.

У меня также возникли проблемы с завершением обоих потоков. Вызов kill() , похоже, не работает. В моем случае протокол, который реализует программа, таков, что команда «quit», считываемая из стандартного ввода, должна привести к завершению программы. Поэтому дочерний поток проверяет, была ли прочитанная строка «завершена» и завершается exit в этом случае. Правильное решение должно найти лучший способ позволить родителю убить дочернего элемента.

Я не стал игнорировать SIGCHLD (потому что он не существует в Windows) или вызывать wait*() , потому что fork не создает новый образ процесса в Windows, а только новый поток.

Этот подход близок к тому, который был предложен в одном из комментариев к вопросу, только в том, что поток замаскирован под дочерний процесс, созданный fork() .

Другим предложением было использовать модуль Win32::Console . Это не работает по двум причинам:

  1. Как следует из названия, это работает только для консоли. Но мое программное обеспечение является бэкэндом для интерфейса с графическим интерфейсом и редко запускается в консоли.
  2. Базовый API предназначен для событий клавиатуры и мыши. Он отлично работает для нажатий клавиш и большинства событий мыши, но опрос события блокируется, как только пользователь что-то выбрал с помощью мыши. Таким образом, даже для реального консольного приложения этот подход не будет работать. Решение, построенное на Win32::Console нем, также должно обрабатывать такие события, как нажатие клавиш CTRL, ALT или Shift, поскольку они не гарантируют, что ввод может быть немедленно прочитан из tty.

Несколько удивительно, что такую тривиальную задачу, как неблокирующий ввод-вывод в файловом дескрипторе, так сложно реализовать переносимым способом в Perl, потому что Windows на самом деле имеет аналогичную концепцию, называемую «перекрытым» вводом-выводом. Я попытался понять эту концепцию, потерпел неудачу и пришел к выводу, что это соответствует максиме Windows «делайте простые вещи сложными, а сложные вещи невозможными». Поэтому я просто не могу винить разработчиков Perl в том, что они не используют его как эмуляцию неблокирующего ввода-вывода. Возможно, это просто невозможно.

Комментарии:

1. Круто, что IO::Socket::Unix работает в Windows. В соответствии с этим требуется как минимум Windows 10 1803 и Socket.pm версия> = 2.029.

2. Смотрите также IO ::Async::Function

3. Я думаю IO::Async , что модули не помогут, потому что моя программа требует постоянной связи между потоками. Я также отредактировал ответ, чтобы упомянуть проблемы совместимости IO::Socket::UNIX .