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