Перехват Erlport/Python STDOUT в 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, но это самая простая часть (надеюсь). Любой совет? Спасибо.

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
0
404
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Для тех, кто еще застрял с этим: поскольку у меня есть 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)

My idea was to call something like Python.call(pid, :builtins, :print, [self()]) but that results in a bunch of errors and I really don't know if that's the right direction at all.

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

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

myprint.py:

def print_this(str):
    print str

I just want to send the STDOUT stuff from Python back to Elixir for logging but I can't wrap my head around how to achieve that. I know it's possible even though I'm using Python 2.7

Документы Эрлпорт говорят:

As a convenient feature ErlPort also supports redirection of Python`s STDOUT to Erlang...

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

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

друзья.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

В иксе:

~/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 файловый ввод-вывод обрабатывается запуском процесса, которому отправляются запросы либо на запись в файл, либо на чтение файла. Я думаю, что стандартный вывод отправляется любому процессу, который является group_leader, и если процесс, обрабатывающий файловый ввод-вывод, является group_leader, то стандартный вывод отправляется в файл.

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

The group leader should be rarely changed in applications with a supervision tree, because OTP assumes the group leader of their processes is their application master.

.

Спасибо за ваш вдумчивый ответ. Я отмечу его как принятый, так как он определенно объясняет лучше, чем мой специальный подход.

t56k 09.07.2019 01:40

@cd-rum, Ах, не делай этого. В своем ответе вы сказали, У меня есть Genserver вокруг экземпляра :python, - я думаю, было бы полезно, если бы вы показали, как вы это настроили. Недавно я использовал GenServer, чтобы обойти аналогичную проблему. В моем случае процесс вызвал функцию, которая сделала этот процесс владельцем сокета, а это означало, что только этот процесс мог отправлять и получать сообщения из сокета. Я использовал handle_call() GenServer для вызова функции, что означало, что GenServer стал владельцем, а сообщения из сокета обрабатывались handle_info().

7stud 09.07.2019 01:55

Справедливо, я скоро расширю свой ответ.

t56k 09.07.2019 01:58

@cd-rum, Хорошо, я отредактировал свой ответ, что означает, что вы можете снять галочку, и когда вы расширите свой ответ, вы можете принять свой собственный ответ.

7stud 09.07.2019 02:01
Я не знаю, что это за мусор в последней строке. Редактировать: Хммм... это атом: хорошо, окруженный некоторыми другими вещами.) -- "Прочие вещи" состоят из цветовых кодов, интерпретируемых терминалом. :ok будет отображаться синим цветом (или чем-то подобным) в терминале.
7stud 09.07.2019 18:31

Другие вопросы по теме