erlang functional python статьи

Работа с внешними приложениями через порты Erlang

Dmitry Vasiliev 23:16, 2012 2 6 20:11, 2010 1 10

Один из наиболее простых способов работы с внешними приложениями в Erlang - это использование портов. Упрощенно работу порта с внешними приложениями можно рассматривать следующим образом: внешнее приложение запускается в отдельном процессе операционной системы, параллельно виртуальной машине Erlang, и общение с ним происходит через 2 канала (pipes) для ввода и вывода.

Запуск внешнего приложения в отдельном процессе ОС защищает виртуальную машину Erlang от ошибок, которые могут возникать во внешнем приложении, но, в свою очередь, необходимость использования каналов для общения между процессами может снизить эффективность взаимодействия. Для оптимального использования, как и в случае с сетевыми приложениями, рекомендуется выполнять во внешнем приложении какой-то законченный объем работ, а не пытаться вызывать таким способом небольшие функции.

Интерфейс порта

Порт создается с помощью следующей функции:

open_port(PortName, PortSettings)

Эта функция возвращает идентификатор порта с помощью которого можно посылать и получать от порта сообщения, как и в случае с идентификаторами процессов. Так же идентификатор может использоваться для связывания, или регистрации с помощью, соответственно link/1 и register/2.

Из значений PortName нас в данный момент интересует {spawn, Command}, запускающее команду Command. Другие значения для PortName можно посмотреть в документации по open_port/2.

PortSettings должен быть списком опций порта. Рассмотрим наиболее полезные опции, работающие со spawn:

  • {cd, Dir} - внешнее приложение запускается с текущей директорией Dir;
  • {env, Env} - позволяет установить, или удалить переменные среды для внешнего приложения. Env должен быть списком пар {Name, Value}, при этом если Value равно false, то переменная удаляется из среды приложения;
  • in - порт может быть использован только для ввода;
  • out - порт может быть использован только для вывода;
  • binary - порт будет возвращать и принимать бинарные данные;
  • use_stdio - для общения с внешним приложением будут использоваться стандартный ввод/вывод (дескрипторы 0 и 1, соответственно). По-умолчанию;
  • nouse_stdio - вместо использования стандартного ввода/вывода для общения с внешним приложением используются дескрипторы 3 и 4. Судя по всему, эта опция игнорируется под Windows и всегда используется use_stdio;
  • stderr_to_stdout - стандартный вывод ошибок перенаправляется на стандартный вывод. Не может использоваться вместе с nouse_stdio;
  • exit_status - когда внешнее приложение заканчивает свою работу порт получает сообщение с кодом завершения приложения в виде {Port, {exit_status, Status}};
  • {packet, N} - перед сообщениями, которыми обменивается Erlang и внешнее приложение, передается их длина в виде N (1, 2, 4) байтов (в порядке от старшего к младшему);
  • stream - сообщения посылаются без указания их длины. По-умолчанию;
  • {line, Len} - сообщения от внешнего процесса передаются построчно (без символа перевода строки), но не более Len символов в строке. При этом, если строка меньше Len и оканчивается переводом строки она возвращается в виде {eol, Line}, иначе она разбивается на несколько сообщений и возвращается в виде {noeol, Line} и только последняя часть (которая оканчивается переводом строки) возвращается в виде {eol, Line};

Сообщения для передачи порту

Процесс-владелец порта может посылать порту следующие сообщения:

  • {Port, {command, Data}} - послать данные Data в порт. Так же можно использовать функцию port_command(Port, Data) (функции могут использоваться любыми процессами);
  • {Port, close} - закрыть порт. Если порт еще не был закрыт он отвечает сообщением {Port, closed}. Так же можно использовать функцию port_close(Port);
  • {Port, {connect, NewPid}} - устанавливает NewPid в качестве владельца порта. Если порт еще не был закрыт он отвечает сообщением {Port, connected}. При этом, старый владелец порта остается связанным (linked) с портом. Так же можно использовать функцию port_connect(Port, NewPid);

Сообщения получаемые от порта

Процесс-владелец порта, или любой процесс вызывающий описанные выше функции, может получать от порта следующие сообщения:

  • {Port, {data, Data}} - получены данные Data от внешнего приложения;
  • {Port, closed} - ответ на {Port, close};
  • {Port, connected} - ответ на {Port, {connect, NewPid}};
  • {Port, {exit_status, Status}} - передается если при создании порта использовалась опция exit_status и внешнее приложение завершило свою работу;
  • {'EXIT', Port, Reason} - работа порта была прервана;

Примеры

Рассмотрим примеры работы с портами Erlang в различных ситуациях. Работу с внешними приложениями можно разделить на две большие группы:

  • Запуск сторонних приложений, которые не предназначены напрямую для работы через порт;
  • Запуск приложений специально написанных для работы через порт;

Сторонние приложения

В простейшем случае при работе со сторонними приложениями требуется получить только вывод этого приложения. Вот несколько простых примеров с использованием оболочки Erlang:

1> open_port({spawn, "ls -l"}, []).
#Port<0.483>
2> flush().
Shell got {#Port<0.483>,
           {data,"total 4\ndrwxr-xr-x 2 user user 4096 2010-01-10 17:16 test\n"}}
ok
3> open_port({spawn, "ls -l"}, [{line, 80}]).
#Port<0.494>
4> flush().
Shell got {#Port<0.494>,{data,{eol,"total 4"}}}
Shell got {#Port<0.494>,
           {data,{eol,"drwxr-xr-x 2 user user 4096 2010-01-10 17:16 test"}}}
ok

Пример более сложной функции может выглядеть так:

-module(command).
-export([run/1]).


run(Command) ->
    Port = open_port({spawn, Command},
        [{line, 80}, exit_status, stderr_to_stdout, in, binary]),
    run(Port, [], <<>>).

run(Port, Lines, OldLine) ->
    receive
        {Port, {data, Data}} ->
            case Data of
                {eol, Line} ->
                    run(Port, [<<OldLine/binary,Line/binary>> | Lines], <<>>);
                {noeol, Line} ->
                    run(Port, Lines, <<OldLine/binary,Line/binary>>)
            end;
        {Port, {exit_status, 0}} ->
            {ok, Lines};
        {Port, {exit_status, Status}} ->
            {error, Status, Lines}
    after
        30000 ->
            {error, timeout}
    end.

Здесь мы получаем построчно вывод внешнего приложения пока оно не закончит свою работу. Результат работы можно увидеть используя оболочку Erlang:

1> c(command).
{ok,command}
2> command:run("ls -l").
{ok,[<<"-rw-r--r-- 1 user user 714 2010-01-10 17:45 command.erl">>,
     <<"-rw-r--r-- 1 user user 932 2010-01-10 17:45 command.beam">>,
     <<"total 0">>]}
3> command:run("ls -l unknown").
{error,2,
       [<<"ls: cannot access unknown: No such file or directory">>]}

И в итоге, небольшой пример с использованием стандартного ввода внешней команды:

1> Port = open_port({spawn, "grep --line-buffered test"}, [{line, 80}]).
#Port<0.483>
2> port_command(Port, "a line\n").
true
3> port_command(Port, "a test line\n").
true
4> flush().
Shell got {#Port<0.483>,{data,{eol,"a test line"}}}
ok
5> port_close(Port).
true

Специализированные приложения

Приложения специально предназначенные для работы через порт должны использовать какой-либо протокол известный двум сторонам. В простейшем случае это может быть, например, построчный протокол, как в примере ниже с внешним приложением написанным на Python:

import sys
import time

def timer():
    while True:
        line = sys.stdin.readline()
        if not line:
            break
        if line.rstrip() != "time":
            result = "error"
        else:
            result = str(time.time())
        sys.stdout.write("%s\n" % result)
        sys.stdout.flush()

if __name__ == "__main__":
    timer()

Клиент на Erlang может выглядеть так:

-module(timer).
-export([get_timer/0]).


get_timer() ->
    Port = open_port({spawn, "python -u timer.py"}, [{line, 40}]),
    fun() ->
        port_command(Port, "time\n"),
        receive
            {Port, {data, {eol, Time}}} ->
                {ok, Time}
        after
            1000 ->
                {error, timeout}
        end
    end.

Здесь опция -u при вызове Python необходима для корректной работы под Windows - в этом случае стандартный ввод/вывод открывается в бинарном режиме. Работает такая связка следующим образом:

1> c(timer).
{ok,timer}
2> Timer = timer:get_timer().
#Fun<timer.0.126277284>
3> Timer().
{ok,"1263138448.25"}
4> Timer().
{ok,"1263138449.82"}
Использование формата обмена сообщениями Erlang

Построчный протокол, описанный выше, работает только в простейших случаях. В более сложных ситуациях нужно либо писать собственный протокол, либо использовать формат обмена сообщениями Erlang, если он реализован для языка на котором написано внешнее приложение. Приложения на C для этих целей могут использовать библиотеку erl_interface поставляемую вместе с Erlang. Приложения на Python могу воспользоваться библиотекой erlport.

Перепишем пример выше с использованием erlport:

import time

from erlport import Port, Protocol, Atom

class TimerProtocol(Protocol):

    def handle_time(self):
        return Atom("ok"), time.time()

if __name__ == "__main__":
    proto = TimerProtocol()
    proto.run(Port(use_stdio=True))

Внешнее приложение на Python заметно упростилось - осталась только основная логика. И обновленный клиент на Erlang:

-module(timer).
-export([get_timer/0]).


get_timer() ->
    Port = open_port({spawn, "python -u timer.py"}, [{packet, 1}, binary]),
    fun() ->
        port_command(Port, term_to_binary(time)),
        receive
            {Port, {data, Data}} ->
                binary_to_term(Data)
        after
            1000 ->
                {error, timeout}
        end
    end.

Здесь изменились опции при вызове open_port/2 и для обработки данных теперь используются функции term_to_binary/1/binary_to_term/1. Проверим как это работает с помощью оболочки:

1> c(timer).
{ok,timer}
2> Timer = timer:get_timer().
#Fun<timer.0.7073589>
3> Timer().
{ok,1263139909.105159}
4> Timer().
{ok,1263139910.075071}

Заметьте, что значение time.time() теперь возвращается, не в виде строки, а именно так, как его возвращает функция time().

Заключение

При правильном использовании порты Erlang могут быть очень удобным инструментом для подключения приложений написанных на других языках. Более подробно о работе с портами можно прочитать в документации Erlang: