Запуск внешнего приложения в отдельном процессе ОС защищает виртуальную машину 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: