简体   繁体   English

Erlport/Python STDOUT 捕获到 Elixir

[英]Erlport/Python STDOUT capture to Elixir

I'm trying to pipe STDOUT from Python/Erlport back to Elixir.我正在尝试将 STDOUT 从 Python/Erlport 通过管道传输回 Elixir。 I've got :python calls working fine, 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.我有:python调用工作正常,我只想将 Python 中的 STDOUT 内容发送回 Elixir 进行日志记录,但我无法理解如何实现这一点。 I know it'spossible even though I'm using Python 2.7.我知道即使我使用的是 Python 2.7,这也是可能的

I've got a Genserver wrapper around the :python module so that my call works like so:我在:python模块周围有一个 Genserver 包装器,以便我的调用工作如下:

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

Python.call just looks like this: Python.call 看起来像这样:

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

Anything from :bridge (ie, bridge.py ) is lost to STDOUT unless I explicitly return something (obviously halting the function).任何来自:bridge (即bridge.py )的东西都会丢失到 STDOUT 除非我明确返回某些东西(显然是停止函数)。 What can I do to capture STDOUT?我可以做什么来捕获标准输出?

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.我的想法是调用Python.call(pid, :builtins, :print, [self()])但这会导致一堆错误,我真的不知道这是否是正确的方向。

I actually want to pipe it into a Phoenix channel, but that's the easy part (I hope).我实际上想把它传送到凤凰频道,但这是简单的部分(我希望)。 Any advice?有什么建议吗? Thanks.谢谢。

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.我的想法是调用Python.call(pid, :builtins, :print, [self()])但这会导致一堆错误,我真的不知道这是否是正确的方向。

self() is not where the output goes--rather self() is the argument for print , ie what python will print out. self()不是输出去的地方——而是self()print的参数,即 python 将打印出来的内容。

I think erlport can only handle MFA calls(module, function, argument), and because print isn't a function in python 2.7 , I think you need to wrap a function around print , eg:我认为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.我只是想将 Python 中的 STDOUT 内容发送回 Elixir 进行日志记录,但我无法理解如何实现这一点。 I know it's possible even though I'm using Python 2.7我知道即使我使用的是 Python 2.7 也是可能的

Theerlport docs say: erlport文档说:

As a convenient feature ErlPort also supports redirection of Python`s STDOUT to Erlang...作为一个方便的特性,ErlPort 还支持将 Python 的 STDOUT 重定向到 Erlang...

That appears to be the default setup, so you don't have to do anything to get python stdout to be redirected to elixir stdout.这似乎是默认设置,因此您无需执行任何操作即可将 python 标准输出重定向到 elixir 标准输出。 The question then becomes: "How do you log elixir stdout to a file?"那么问题就变成了:“如何将 elixir stdout 记录到文件中?”

I'm able to log elixir stdout to a file like this:我可以将 elixir stdout记录到这样的文件中:

friends.ex:朋友.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

This is my directory structure:这是我的目录结构:

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

In iex:在 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)>

In the log file:在日志文件中:

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

(I don't know what that junk is on the last line. Edit: Hmmm...it's the atom :ok surrounded by some other stuff.) (我不知道最后一行上的垃圾是什么。编辑:嗯......它是原子:ok被其他一些东西包围。)

If I comment out everything inside go() above the python_path line, then I get:如果我在 python_path 行上方注释掉go()所有内容,那么我会得到:

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

In erlang/elixir file I/O is handled by starting a process to which requests are sent to either write to the file or read the file.在 erlang/elixir 文件 I/O 是通过启动一个进程来处理的,该进程将请求发送到写入文件或读取文件。 I think stdout gets sent to whatever process is the group_leader , and if the process handling file I/O is the group_leader, then stdout gets sent to the file.我认为 stdout 会被发送到group_leader任何进程,如果处理文件 I/O 的进程是 group_leader,那么 stdout 会被发送到文件。

I don't know if messing with the group_leader will screw things up when you are using a GenServer .我不知道当您使用GenServer时,弄乱group_leader是否会搞砸。 There is a warning in the erlang docs : erlang 文档中有一个警告:

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.在具有监督树的应用程序中,组长应该很少更改,因为 OTP 假定其进程的组长是它们的应用程序主人。

. .

For anyone else getting stuck with this kinda thing: since I've got a GenServer around the :python instance, I've just leveraged handle_info :对于其他被这种事情卡住的人:因为我在:python实例周围有一个 GenServer,我只是利用了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

Detail细节

To more fully outline my solution as @7stud advised, I'll include the wider approach based on erlport and this great post .为了按照@7stud 的建议更全面地概述我的解决方案,我将包括基于erlport的更广泛的方法和这篇很棒的文章 Accordingly, I've got a Python module that looks like this:因此,我有一个看起来像这样的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

It's called from a GenServer that handles its spawning and termination:它是从处理其生成和终止的 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

You can see at the end if STDOUT doesn't return a certain string from bridge.py (or any other Python module) it'll return a stacktrace.您可以在最后看到如果 STDOUT 没有从bridge.py (或任何其他 Python 模块)返回某个字符串, bridge.py返回一个bridge.py Speaking of, bridge.py looks like this:说起来, 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)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM