Захват STDOUT из Erlport / Python в Elixir

Я пытаюсь передать STDOUT из Python / Erlport обратно в Elixir. У меня :python вызовы работают нормально, я просто хочу отправить материал STDOUT из Python обратно в Elixir для регистрации, но я не могу понять, как этого добиться. Я знаю, что возможно, даже если я использую Python 2.7.

У меня есть оболочка Genserver вокруг модуля :python, так что мой вызов работает так:

pid = Python.start()
Python.call(pid, :bridge, :register_handler, [self()]) 

Python.call выглядит так:

def call(pid, module, function, args \\ []) do
  :python.call(pid, module, function, args)
end

Все из :bridge (т.е. bridge.py) теряется для STDOUT, если я явно не верну что-то (очевидно, остановив функцию). Что я могу сделать, чтобы захватить STDOUT?

Моя идея заключалась в том, чтобы вызвать что-то вроде Python.call(pid, :builtins, :print, [self()]), но это привело к кучке ошибок, и я действительно не знаю, правильное ли это направление вообще.

Я действительно хочу передать это в канал Phoenix, но это простая часть (я надеюсь). Любой совет? Спасибо.


person okay56k    schedule 03.07.2019    source источник


Ответы (2)


Моя идея заключалась в том, чтобы вызвать что-то вроде Python.call(pid, :builtins, :print, [self()]), но это привело к кучей ошибок, и я действительно не знаю, правильное ли это направление вообще.

self() - это не то, куда идет вывод, а self() - это аргумент для print, то есть то, что python распечатает.

Я думаю, что erlport может обрабатывать только вызовы MFA (модуль, функция, аргумент), и поскольку print не является функцией в python 2.7, я думаю, вам нужно обернуть функцию вокруг print, например:

myprint.py:

def print_this(str):
    print str

Я просто хочу отправить материал STDOUT из Python обратно в Elixir для регистрации, но я не могу понять, как этого добиться. Я знаю, что это возможно, даже если я использую Python 2.7.

В документах erlport говорится:

В качестве удобной функции ErlPort также поддерживает перенаправление Python STDOUT на Erlang ...

Похоже, это настройка по умолчанию, поэтому вам не нужно ничего делать, чтобы python stdout был перенаправлен на stdout elixir. Тогда возникает вопрос: «Как вы записываете stdout elixir в файл?»

Я могу записать elixir stdout в такой файл:

friends.ex:

defmodule Friends do

  use Export.Python

  def go do

    #Get path to logfile:

    priv_path = :code.priv_dir(:friends)
    logfile_path = Path.join([priv_path, "log", "mylog.log"])

    #Redirect stdout:

    {:ok, io_pid} = File.open(logfile_path, [:append])
    Process.group_leader(self(), io_pid)

    #Send output to stdout:

    IO.puts "Am I in the log file??!"

    python_path = Path.expand("lib/python") 
    {:ok, py} = Python.start(
                 python: "python2.7",
                 python_path: python_path
               )

    Python.call(py, "myprint", "print_this", ["hello world!"])
    Python.call(py, "myprint", "print_this", ["goodbye..."])

    Python.stop(py)
  end

end

Это моя структура каталогов:

friends
    /lib
      /friends
      /python
          myprint.py
      friends.ex
   /priv
      /log
          mylog.log

В iex:

~/elixir_programs/friends$ iex -S mix
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Compiling 1 file (.ex)
Interactive Elixir (1.8.2) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> Friends.go
iex(2)>

В файле журнала:

Am I in the log file??!
hello world!
goodbye...
[33m[36m:ok[0m[33m[0m

(Я не знаю, что это за мусор в последней строке. Изменить: Хммм ... это атом :ok, окруженный каким-то другим материалом.)

Если я закомментирую все внутри go() над строкой python_path, то получу:

iex(1)> Friends.go
hello world!
goodbye...
:ok

В файлах erlang / elixir ввод-вывод обрабатывается запуском процесса, которому отправляются запросы либо на запись в файл, либо на чтение файла. Я думаю, что stdout отправляется любому процессу, который является group_leader, и если ввод-вывод файла обработки процесса является group_leader, тогда stdout отправляется в файл.

Я не знаю, испортит ли игра с group_leader, когда вы используете GenServer. В документах по erlang есть предупреждение. :

Лидера группы следует редко менять в приложениях с деревом контроля, поскольку OTP предполагает, что лидер группы их процессов является хозяином приложения.

.

person 7stud    schedule 05.07.2019
comment
Спасибо за вдумчивый ответ. Я отмечу его как принятый, поскольку он определенно объясняет ситуацию лучше, чем мой специальный подход. - person okay56k; 09.07.2019
comment
@ cd-ром, Ах, не делай этого. В своем ответе вы сказали: У меня есть Genserver вокруг экземпляра: python, - я думаю, было бы полезно, если бы вы показали, как вы это настраиваете. Недавно я использовал GenServer, чтобы обойти проблему подобного типа. В моем случае процесс вызвал функцию, которая сделала этот процесс владельцем сокета, а это означало, что только этот процесс мог отправлять и получать сообщения из сокета. Я использовал handle_call() GenServer для вызова функции, что означало, что GenServer стал владельцем, а сообщения из сокета обрабатывались handle_info(). - person 7stud; 09.07.2019
comment
Справедливо, я скоро расширю свой ответ. - person okay56k; 09.07.2019
comment
@ cd-rum, хорошо, я отредактировал свой ответ, что означает, что вы можете убрать галочку, а когда вы развернете свой ответ, вы можете принять свой собственный ответ. - person 7stud; 09.07.2019
comment
Я не знаю, что это за хлам в последней строке. Изменить: Хммм ... это атом: ок, окруженный каким-то другим материалом.) - Другой материал состоит из цветовых кодов, которые интерпретируются терминалом. :ok будет отображаться в терминале синим (или чем-то еще). - person 7stud; 09.07.2019

Для тех, кто застрял в подобной ситуации: поскольку у меня есть GenServer вокруг экземпляра :python, я только что использовал handle_info:

def handle_info({:python, message}, session) do
  message |> String.split("\n", trim: true)
  SomeWeb.Endpoint.broadcast("log", "update", %{body: message})

  {:stop, :normal,  session}
end

Подробно

Чтобы более полно изложить свое решение, как советовал @ 7stud, я включу более широкий подход, основанный на erlport и этот отличный пост. Соответственно, у меня есть модуль Python, который выглядит так:

defmodule App.Python do
   @doc """
      Python instance pointing to priv/python.
    """
   def start() do
      path = [
         :code.priv_dir(:prefect),
         "python"
      ]|> Path.join()

      {:ok, pid} = :python.start([
         {:python_path, to_charlist(path)}
      ])
      pid
   end

   def call(pid, module, function, args \\ []) do
      :python.call(pid, module, function, args)
   end

   def cast(pid, message) do
      :python.cast(pid, message)
   end

   def stop(pid) do
      :python.stop(pid)
   end
end

Он вызывается из GenServer, который обрабатывает его порождение и завершение:

defmodule App.PythonServer do
   @doc """
      Receives async. messages from Python instance.
    """
   use GenServer

   alias App.Python

   def start_link() do
      GenServer.start_link(__MODULE__, [])
   end

   def init(_args) do
      pid = Python.start()
      Python.call(pid, :bridge, :register_handler, [self()])
      App.Application.broadcast_change

      {:ok, pid}
   end

   def cast_draw(id) do
      {:ok, pid} = start_link()

      GenServer.cast(pid, {:id, id})
   end

   def call_draw(id) do
      {:ok, pid} = start_link()

      GenServer.call(pid, {:id, id}, 10_000)
   end

   def handle_call({:id, id}, _from, session) do
      result = Python.call(session, :bridge, :draw, [id])

      {:reply, result, session}
   end

   def handle_cast({:id, id}, session) do
      Python.cast(session, id)

      {:noreply, session}
   end

   def handle_info({:python, message}, session) do
      msg = message |> format_response
      {:ok, time} = Timex.now |> Timex.format("{h12}:{m}{am} {D}/{M}/{YYYY}")
      AppWeb.Endpoint.broadcast("log", "update", %{time: time, body: msg, process: message})

      {:stop, :normal,  session}
   end

   def terminate(_reason, session) do
      Python.stop(session)
      App.Application.broadcast_change

      :ok
   end

   defp format_response(message) do
      if String.contains? message, "[result] Sent" do
         message |> String.split("\n", trim: true) |> Enum.at(-2)
      else
         message |> String.split("\n", trim: true) |> Enum.take(-12) |> Enum.join("\n")
      end
   end
end

Вы можете увидеть в конце, если STDOUT не возвращает определенную строку из bridge.py (или любого другого модуля Python), он вернет трассировку стека. Кстати, bridge.py выглядит так:

import os
import sys
import subprocess

from erlport.erlang import set_message_handler, cast
from erlport.erlterms import Atom

message_handler = None # reference to the elixir process to send

cmd = "xvfb-run -a python"
py = os.path.join("/home/ubuntu/app/priv/python/export.py")

def cast_message(pid, message):
  cast(pid, message)

def register_handler(pid):
  global message_handler
  message_handler = pid

def handle_message(id):
    try:
      result = draw(id)
      print result
      if message_handler:
        cast_message(message_handler, (Atom('python'), result))
    except Exception, error:
      print error
      if message_handler:
        cast_message(message_handler, (Atom('python'), error))
      pass

def draw(id):
  proc = subprocess.check_output(
    "{0} {1} {2}".format(cmd, py, id), stderr = subprocess.STDOUT, shell = True
  )
  return proc

set_message_handler(handle_message)
person okay56k    schedule 04.07.2019