Coder Social home page Coder Social logo

porcelain's Introduction

Porcelain

Build status Hex version Hex downloads

Porcelain implements a saner approach to launching and communicating with external OS processes from Elixir. Built on top of Erlang's ports, it provides richer functionality and simpler API.

Simply put, Porcelain removes the pain of dealing with ports and substitutes it with happiness and peace of mind.

Overview

Having some 20 odd options, the Erlang port API can be unwieldy and cumbersome to use. Porcelain replaces it with a simpler approach and provides defaults for the common cases.

User-level features include:

  • sane API

  • ability to launch external programs in a synchronous or asynchronous manner

  • multiple ways of passing input to the program and getting back its output (including working directly with files and Elixir streams)

  • being able to work with programs that try to read the whole input until EOF before producing output

  • ability to send OS signals to external processes (requires goon v2.0)

To read background story on the library's design and possible future extensions, please refer to the wiki.

Installation

Add Porcelain as a dependency to your Mix project:

def application do
  [applications: [:porcelain]]
end

defp deps do
  [{:porcelain, "~> 2.0"}]
end

Now, some of the advanced functionality is provided by the external program called goon. See which particular features it implements in the reference docs here. Go to goon's project page to find out how to install it.

Usage

Examples below show some of the common use cases. See also this demo app. Refer to the API docs to familiarize yourself with the complete set of provided functions and options.

Launching one-off programs

If you need to launch an external program, feed it some input and capture its output and maybe also exit status, use exec() or shell():

alias Porcelain.Result

%Result{out: output, status: status} = Porcelain.shell("date")
IO.inspect status   #=> 0
IO.inspect output   #=> "Fri Jun  6 14:12:02 EEST 2014\n"

result = Porcelain.shell("date | cut -b 1-3")
IO.inspect result.out   #=> "Fri\n"

# Use exec() when you want launch a program directly without using a shell
File.write!("input.txt", "lines\nread\nfrom\nfile\n")
result = Porcelain.exec("sort", ["input.txt"])
IO.inspect result.out   #=> "file\nfrom\nlines\nread\n"

Passing input and getting output

Porcelain gives you many options when it comes to interacting with external processes. It is possible to feed input from a file or a stream, same for output:

File.write!("input.txt", """
  This file contains some patterns
  >like this<
  interspersed with other text
  ... >like this< the end.
  """)

Porcelain.exec("grep", [">like this<", "-m", "2"],
                    in: {:path, "input.txt"}, out: {:append, "output.txt"})
IO.inspect File.read!("output.txt")
#=> ">like this<\n... >like this< the end.\n"

Streams

Programs can be spawned asynchronously (using spawn() and spawn_shell()) allowing for continuously exchanging data between Elixir and the external process.

In the next example we will use streams for both input and output.

alias Porcelain.Process, as: Proc

instream = SocketStream.new('example.com', 80)
opts = [in: instream, out: :stream]
proc = %Proc{out: outstream} = Porcelain.spawn("grep", ["div", "-m", "4"], opts)

Enum.into(outstream, IO.stream(:stdio, :line))
#     div {
#         div {
# <div>
# </div>

Proc.alive?(proc)   #=> false

Alternatively, we could pass the output stream directly to the call to spawn():

opts = [
  in: SocketStream.new('example.com', 80),
  out: IO.stream(:stderr, :line),
]
Porcelain.exec("grep", ["div", "-m", "4"], opts)
#=> this will be printed to stderr of the running Elixir process:
#     div {
#         div {
# <div>
# </div>

The SocketStream module used above wraps a tcp socket in a stream. Its implementation can be found in the test/util/socket_stream.exs file.

Messages

If you prefer to exchange messages with the external process, you can do that:

alias Porcelain.Process, as: Proc
alias Porcelain.Result

proc = %Proc{pid: pid} =
  Porcelain.spawn_shell("grep ohai -m 2 --line-buffered",
                                in: :receive, out: {:send, self()})

Proc.send_input(proc, "ohai proc\n")
receive do
  {^pid, :data, :out, data} -> IO.inspect data   #=> "ohai proc\n"
end

Proc.send_input(proc, "this won't match\n")
Proc.send_input(proc, "ohai")
Proc.send_input(proc, "\n")
receive do
  {^pid, :data, :out, data} -> IO.inspect data   #=> "ohai\n"
end
receive do
  {^pid, :result, %Result{status: status}} -> IO.inspect status   #=> 0
end

Configuring the Goon driver

There are a number of options you can tweak to customize the way goon is used. All of the options described below should be put into your config.exs file.

Setting the driver

config :porcelain, :driver, <driver>

This option allows you to set a particular driver to be used at all times.

By default, Porcelain will try to detect the goon executable. If it can find one, it will use Porcelain.Driver.Goon. Otherwise, it will print a warning to stderr and fall back to Porcelain.Driver.Basic.

By setting Porcelain.Driver.Basic above you can force Porcelain to always use the basic driver.

If you set Porcelain.Driver.Goon, Porcelain will always use the Goon driver and will fail to start if the goon executable can't be found.

Goon options

config :porcelain, :goon_driver_path, <path>

Set an absolute path to the goon executable. If this is not set, Porcelain will search your system's PATH by default.

config :porcelain, :goon_stop_timeout, <integer>

This setting is used by Porcelain.Process.stop/1. It specifes the number of seconds goon will wait for the external process to terminate before it sends SIGKILL to it. Default timeout is 10 seconds.

config :porcelain, :goon_warn_if_missing, <boolean>

Print a warning to the console if the goon executable isn't found. Default: true.

Going deeper

Take a look at the reference docs for the full description of all provided functions and supported options.

Known issues and roadmap

  • there are known crashes happening when using Porcelain across two nodes
  • error handling when using the Goon driver is not completely shaped out

Acknowledgements

Huge thanks to all who have been test-driving the library in production, in particular to

  • Josh Adams
  • Tim Ruffles

License

This software is licensed under the MIT license.

porcelain's People

Contributors

alco avatar davydog187 avatar ericmj avatar ivan avatar lsimoneau avatar mad42 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

porcelain's Issues

Goon protocol version error

Getting the following error when trying to use the goon driver:

Could not start application porcelain: Porcelain.App.start(:normal, []) returned an error: "goon executable at /... does not support protocol version 2.0"

Porcelain.spawn does not return status nor writes to stderr

Hey,
I just started using elixir and I love it.
I encountered a problem where I do not have the exit code when executing a shell.
If, for example, I run ipconfig everything works. If I send a bad command I get an error immediately which is great, but the problem is when I need to get stderr + status code, for exmaple, running ipconfig /notexists porcelain writes everything to stdout and I do not get the exit code anywhere at all.
Thanks!

 def run_cmd(command, args \\ [], opts \\ []) do
    combined_opts = [
      dir: File.cwd!,
      out: :stream,
      result: :discard,
      err: :stream,
    ] ++ opts
    # spawn process that returns a stream, 
    # enum each on stream and send it to server for use, send ok after finish
    Porcelain.spawn(command, args, combined_opts)
    |> case do
      {:error, err} -> {:error, err}
      %Porcelain.Process{:out => out} -> 
        Enum.each(out, &stream_to_server/1)
        {:ok, ""}
    end`
run_cmd("ipconfig", ["/all"]) # works normal
run_cmd("notexists") # works normal
run_cmd("ipconfig", ["notexists"]) # returns std out instead of stderr + status code

crashing the goroutine with something I'd expect is 'reasonable'

proc = Porcelain.spawn_shell("bash --no-editing -i", in: :receive, out: {:send, self()})
Proc.send_input(proc, "ls")

This crashes the goroutine and I get a reasonable error struct sent to self(), but I'd have expected it to just work properly vis-a-vis sending ls (without a newline, but meh) into the bash process that's running. spawn produced the same behaviour (and spawn seems more reasonable here obv. but whatever)

Can you explain what I should be doing in order to get this sort of thing to work? In Port I could do this:

port = Port.open({:spawn, "bash --noediting -i"}, [:stderr_to_stdout, :binary, :exit_status])
send(port, {self, {:command, "ls\n"}})

Porcelain doesn't seem to close stdin

Maybe I'm missing something, but all of these calls hang forever here (Erlang 19.3, Elixir master, Ubuntu 16.04.2):

Porcelain.exec("cat", [], in: "")
Porcelain.exec("cat", [], in: "hello\n")
Porcelain.exec("python", ["-c", "import sys; print sys.stdin.read()"], in: "")
Porcelain.exec("python", ["-c", "import sys; print sys.stdin.read()"], in: "hello\n")

I am guessing this happens because Porcelain doesn't close stdin on the spawned process. Do I need to do something special, or is this a bug?

I am using the basic driver, not the goon driver.

Behaviour module has been deprecated in Elixir 1.4

warning: the Behaviour module is deprecated. Instead of using this module, use the @callback and @macrocallback module attributes. See the documentation for Module for more information on these attributes
lib/porcelain/drivers/driver_common.ex:4: (module)
(elixir) src/elixir_compiler.erl:125: :elixir_compiler.dispatch_loaded/6
(elixir) src/elixir_module.erl:191: :elixir_module.eval_form/6
(elixir) src/elixir_module.erl:71: :elixir_module.do_compile/5
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6

can't send signals

Signals do not work, at least on OSX with goon:

iex(3)> proc = Porcelain.spawn("sleep", ["30"], [])
%Porcelain.Process{err: nil, out: :string, pid: #PID<0.153.0>}
iex(4)> Porcelain.Process.signal proc, 15
{:signal, 15}
iex(5)> Porcelain.Process.signal proc, :kill
{:signal, :kill}
iex(6)>

I still keep seeing the process:

$ ps axww | fgrep sleep
48681 ?? Ss 0:00.01 /usr/local/bin/goon -proto 2.0 -out -- /bin/sleep 30
48682 ?? S 0:00.00 /bin/sleep 30
48690 s007 S+ 0:00.00 fgrep sleep

error condition not properly documented for Porcelain.spawn

For some cases, Porcelain.spawn returns data of structure {:error, <reason>}. E.g:

iex(23)> Porcelain.spawn("whoami2", [])
{:error, "Command not found: whoami2"}

But the spec doesn't capture it:

@spec spawn(binary, [binary])            :: Porcelain.Process.t
  @spec spawn(binary, [binary], Keyword.t) :: Porcelain.Process.t

  def spawn(prog, args, options \\ [])

leading to little annoyances like warning messages about pattern matching with the error msg type.

input: signal end of file, goon

Hi I had the following line of code:

Porcelain.exec("wkhtmltopdf", ["-", "-"], [in: "some html", out: :string])

but after this line the pgm blocked waiting forever for an answer. I tried with wc -l and it is the same pbm, I suggested it was because porcelain coulnd'nt send EOF signal. I tested to install goon, and it worked.
It took me quite some time to understand that goon was necessary for this feature.
I was expecting it to work out of the box. Maybe you should explicitely tells that you can't use custom input output without goon when displaying "goon executable not found" and/or enumerate the features that can"t work without goon?

Porcelain.exec does not capture stderr output

stderr output produced by the process invoked via Porcelain.exec is not captured into result (printed by the program instead).

Repro info:

  1. External program, prints to stderr:
15:20:55 markmark ~$ cat mine_err.sh
#!/usr/bin/env bash
echo "mine stderr" 1>&2
15:23:39 markmark ~$ ./mine_err.sh
mine stderr

(if attempting a repro, please make sure to chmod +x mine_err.sh)

  1. Elixir code:
r = Porcelain.exec("/Users/markmark/mine_err.sh", [])
IO.inspect r
  1. Elixir output:
mine stderr
%Porcelain.Result{err: nil, out: "", status: 0}

^^^^ Note: mine stderr was printed by the program, but not included in Result.


For comparison, when the external program outputs to stdout, things work as expected:

  • external program, prints to stdout:
15:17:43 markmark  ~$ cat mine.sh
#!/usr/bin/env bash
echo "mine stdout"
15:20:23 markmark  ~$ ./mine.sh
mine stdout
  • elixir code:
r = Porcelain.exec("/Users/markmark/mine.sh", [])
IO.inspect r
  • elixir output (stdout text captured in out, as expected):
%Porcelain.Result{err: nil, out: "mine stdout\n", status: 0}

Support :raw files

Porcelain won't accept a {:file, <file>} option if <file> has been opened in :raw mode. Moreover, based on this quote from file module docs:

The functions in the io module cannot be used, because they can only talk to an Erlang process. Instead, use the read/2, read_line/1 and write/2 functions.

the way we currently write to the file will not work with :raw files.

Goon driver fails under heavy usage

Working with the goon driver sometimes I get the following error:

panic: write /dev/stdout: broken pipe

goroutine 21 [running]:
runtime.panic(0x4be580, 0xc208000350)
/usr/lib/go/src/pkg/runtime/panic.c:279 +0xf5
log.(*Logger).Panicf(0xc20801a230, 0x4fdc90, 0x3, 0x7f04e7dbbe38, 0x1, 0x1)
/usr/lib/go/src/pkg/log/log.go:200 +0xcd
main.fatal_if(0x4dfc20, 0xc2080264e0)
/root/home/test/goon/util.go:38 +0x180
main.outLoop(0x7f04e7f46670, 0xc208034030, 0x7f04e7f46318, 0xc208034008, 0x0, 0xc2080041e0)
/root/home/test/goon/io.go:151 +0x41b
created by main.wrapStdout
/root/home/test/goon/io.go:34 +0x181

goroutine 16 [chan receive]:
main.proto_2_0(0x7fffce170101, 0x503bf0, 0x3, 0x4fe1b0, 0x1, 0xc20800e060, 0x9, 0x9, 0x0, 0x0)
/root/home/test/goon/proto_2_0.go:58 +0x421
main.main()
/root/home/test/goon/main.go:51 +0x3d7

goroutine 19 [finalizer wait]:
runtime.park(0x417720, 0x5a78f0, 0x5a6409)
/usr/lib/go/src/pkg/runtime/proc.c:1369 +0x89
runtime.parkunlock(0x5a78f0, 0x5a6409)
/usr/lib/go/src/pkg/runtime/proc.c:1385 +0x3b
runfinq()
/usr/lib/go/src/pkg/runtime/mgc0.c:2644 +0xcf
runtime.goexit()
/usr/lib/go/src/pkg/runtime/proc.c:1445

Error when using darwin goon in linux docker container could be clearer

If you're using docker on OSX to run your Elixir project and you download the darwin version of goon, thinking you need the OSX version, you'll get

14:51:56.559 [info] Application porcelain exited: Porcelain.App.start(:normal, []) returned an error: "goon executable at /code/goon does not support protocol version 2.0"

But the actual problem is you needed to install the linux amd64 version of goon. I don't know if it would be possible to detect you're using the wrong OS's goon, but it would be helpful.

Silence goon executable warning

Maybe move the executable decision to a configuration instead of always printing to stdout if a user decides to not use goon.

cd into before exec

Hi,

thanks for the lib. I need to use zip. But it doesnยดt allow to set a specific directory for zipping. How can i set a dynamic root folder from which the command gets executed?

Porcelain.exec("zip", [document.name,* ] )

The contents are in random folders like 3635433.

Hot code upgrades do not start porcelain

I am using https://github.com/gutschilla/elixir-pdf-generator which depends on porcelain.
Elixir: 1.4
Erlang: 19
Porcelain: 2.0.3

Everything works fine, but when I do a hot code upgrade using releases with distillery, I can get error when call the pdf/porcelain code after the upgrade:

Ranch listener MyApp.Endpoint.HTTP had connection process started with :cowboy_protocol:start_link/4 at #PID<0.24063.0> exit with reason: 
{{%Porcelain.UsageError{message: "Looks like the :porcelain app is not running. Make sure you've added :porcelain to the list of applications in your mix.exs."}, 
[{Porcelain, :driver, 0, [file: 'lib/porcelain.ex', line: 419]}, 
 {Porcelain, :"-exec/3-fun-0-", 3, [file: 'lib/porcelain.ex', line: 131]}, 
 {Porcelain, :catch_throws, 1, [file: 'lib/porcelain.ex', line: 273]}, 
 {PdfGenerator, :generate, 2, [file: 'lib/pdf_generator.ex', line: 136]}, 
 {PdfGenerator, :generate_binary, 2, [file: 'lib/pdf_generator.ex', line: 188]}, 
 ...
]}, 
{MyApp.Endpoint, :call, [
  %Plug.Conn{ ... }}

When I restart my application/release it works again. So this currently prevents me from using hot code upgrades. Everything else works after the upgrade.
Porcelain is added in the mix.exs file:

def application do
    [mod: {MyApp, []},
     extra_applications: [
       :logger,
       :runtime_tools,
       :misc_random,
       :porcelain
      ]
    ]
  end

I also tried to add it to the distillery release config, without a difference.
Any ideas? Is it better to post this on pdf_generator or distillery issue tracker?

Is porcelaim dead?

Is this project dead? There hasn't been a release for years or a commit for a long while as well and PR's have been waiting for a bit. Even the goon driver hasn't seen an update in years. I'm just wondering what the status of this project is as I would like to use it if possible.

Porcelain with destillary

Hello I'm trying to produce a release with distillery with goon bundle in the release itself.

Since one can't use :code.priv_dir(:app) in config.exs, I tried adding this to my application bootstrap code:

Application.put_env(:porcelain, :goon_driver_path, Path.join(:code.priv_dir(:app), 'goon'))
:ok = Porcelain.reinit(:goon)

I've checked the correct path is being generated, but porcelain still complains it can't find goon.

Any ideas on how to solve this? Or any other strategy for bundling goon in the release itself?

Porcelain.spawn_shell error on windows

Hi,

I am using Porcelain.spawn_shell to execute certain commands on windows. Below is the code sample from a Phoenix application using websockets:
Porcelain.spawn_shell("ping -n 5 -i 10 1.1.1.1", in: "", out: {:send, self()})

I am getting error on windows. Same code works fine on linux though...
** (ArgumentError) argument error
:erlang.++("C:\Windows\system32\cmd.exe", '.exe')
(kernel) os.erl:146: :os.verify_executable/3
(kernel) os.erl:126: :os.find_executable/2
(porcelain) lib/porcelain/drivers/basic_driver.ex:103: Porcelain.Driver.Basic.find_executable/2
(porcelain) lib/porcelain/drivers/basic_driver.ex:53: Porcelain.Driver.Basic.do_exec/4
(porcelain) lib/porcelain.ex:273: Porcelain.catch_throws/1

Porcelain.spawn is working with some code changes. I am not sure if there is some issue with my code or Porcelain (or something else). Also, what is the difference between spawn and spawn_shell?

Versions:

  1. Windows Server 2007, 32bit
  2. OTP: 18.3
  3. Porcelain: 2.0

Could not open input file when spawning multiple php scripts

Hello,

I am building a project that involves launching PHP script concurrently (between 3-4). When I launch only one script with
Porcelain.spawn("php", [Script.php_slave(job.worker_version), boot, "#{environment_id}"], in: :receive, out: {:send, OutputManager.where_is(environment_id)})

it works very well but when I launch this instruction few times I have the output:
Could not open input file: /Users/test/tasky/worker_umbrella/apps/worker/libraries/v1/tasky-php/scripts/slave.php

Is there an option or a limit in the spawning?

Thanks for your feedback

Warnings: Elixir 1.11

Compilation warnings fixed in #52

warning: function init/1 required by behaviour GenServer is not implemented (in module Porcelain.Driver.Common.StreamServer).

We will inject a default implementation for now:

    def init(init_arg) do
      {:ok, init_arg}
    end

You can copy the implementation above or define your own that converts the arguments given to GenServer.start_link/3 to the server state.

  lib/porcelain/drivers/stream_server.ex:1: Porcelain.Driver.Common.StreamServer (module)

Receiving messages from an external process without sending?

Does Porcelain support receiving messages from an external process without triggering a response from Elixir first?

The reason I ask, is because i'm using raw Ports right now and the external process should be constantly calling my handle_info/2 as the external process is essentially a while loop that just spams to STDOUT.

I have limited debugging tight now, but it seems to be sleeping between calls from Elixir. I added a timestamp and a counter to figure out if it was buffering or just outright sleeping and the latter seems more probable as the timestamps are exact of the external process receiving a message and the counter is just incremented from last time even though it's incremented in a while loop.

Release new version?

version v2.0.3 has many compile warning in elixir-1.5.2, but master branch hasn't.

Could you bump new version please ?

Thanks.

type t() undefined in Elixir 0.14.3

Hi

i am using porcelain as a dependency in a mix project.

When I run mix deps.get it passes but when I try to run a test file with Porcelain it fails with the following error:

== Compilation error on file lib/porcelain/process.ex ==
** (CompileError) lib/porcelain/process.ex:76: type t() undefined
    (stdlib) lists.erl:1336: :lists.foreach/2
    (stdlib) erl_eval.erl:657: :erl_eval.do_apply/6
    (elixir) src/elixir.erl:170: :elixir.erl_eval/3
    (elixir) src/elixir.erl:158: :elixir.eval_forms/4
    (elixir) src/elixir_lexical.erl:17: :elixir_lexical.run/2
    (elixir) src/elixir.erl:170: :elixir.erl_eval/3

could not compile dependency porcelain, mix compile failed. You can recompile this dependency with `mix deps.compile porcelain` or update it with `mix deps.update porcelain`

My elixir version is 0.14.3 and erlang is version 17

I'm uncertain about how to debug or resolve this as I'm still learning Elixir ? any help or suggestion would be useful. Thanks

Spawn iex terminal inside a phoenix app with porcelain?

Hi

I'm trying to pass a command using porcelain to an external iex process as part of a project to make an interactive in browser elixir terminal

Is this possible with porcelain? I keep getting the following error?

Porcelain.spawn_shell("iex", in: :receive, out: {:send, self()})

ERROR REPORT==== 2-Aug-2014::15:19:31 ===\nError in process <0.25.0> with exit value: {{badmatch,{error,enotsup}},[{'Elixir.IEx','-start/2-fun-0-',2,[{file,\"lib/iex.ex\"},{line,454}]}]}\n\n1> 

I believe I am using it wrongly?

Any advice or help will be most grateful

Starting Multiple External Processes under Supervisor

When leveraging Porcelain.Process(s) under a Supervisor the best way for the processes to be automatically restarted when they crash as "supervised" workers is to call Porcelain.Process.await/2 in callback function body. However, this is not in the documentation and there may not be the place for it since the docs do get at such behavior for Porcelain.Process.await/2.

Perhaps an example of it should be added to the README.md. What do you think?

Porcelain.spawn does not send result message with exit status

I use goon driver,

defmodule Breakout.Exec do
  def start(cmd, args) do
    pid = spawn fn -> init(cmd, args) end
    {:ok, pid}
  end

  def init(cmd, args) do
    options = [in: :receive, out: {:send, self}, err: {:send, self}]
    process = Porcelain.spawn(cmd, args, options)
    state = %{proc: process}
    loop(state)
  end

  def loop(state) do
    receive do
      {_from, :data, :out, data} ->
        IO.puts "out: #{inspect data}"
      {_from, :data, :err, data} ->
        IO.puts "err: #{inspect data}"
      msg ->
        IO.puts "unexpected msg: #{inspect msg}"
    end

    IO.puts "loop: #{inspect state}"
    loop(state)
  end
end

But I do not receive :result message with exit status.

iex(1)> Breakout.Exec.start "echo", ["1234"]
{:ok, #PID<0.136.0>}
out: "1234\n"
loop: %{proc: %Porcelain.Process{err: {:send, #PID<0.136.0>}, out: {:send, #PID<0.136.0>}, pid: #PID<0.138.0>}}

I can not know when to stop the receive loop. What am I doing wrong?

config'd as per docs, but all keys nil when running

my config/config.exs is:

use Mix.Config

config :porcelain, :driver, :goon
config :porcelain, :goon_driver_path, "#{__DIR__}/../bin/goon"

porcelain is registered:

  def application do
    [ applications: [
        :porcelain
      ],
      mod: {Skrun, []} ]
  end

but on running I get

[Porcelain]: goon executable not found
[Porcelain]: falling back to the basic driver

Goon driver doesn't send SIGKILL in Process.stop

I installed goon to get SIGKILL support when calling Process.stop/1 but it actually doesn't work. See this iex session:

iex(1)> p = Porcelain.spawn("bash", ["-c", "trap '' SIGTERM; echo sleeping...; sleep 30; date >here.txt"])
%Porcelain.Process{err: nil, out: :string, pid: #PID<0.315.0>}
iex(2)> Porcelain.Process.await(p, 1000)
{:error, :timeout}
iex(3)> Porcelain.Process.stop(p)
true
iex(4)> File.read "here.txt"
{:error, :enoent}

30 seconds later:

iex(5)> File.read "here.txt"
{:ok, "Tue May 30 09:54:24 UTC 2017\n"}

Investigate ways to terminate external processes cleanly (with and without goon)

The current behaviour of stopping a process is not satisfactory no matter how you slice it.

Without goon

Below, we have an error in the stream name which happens in the spawned process that controls the Erlang port.

iex(1)> p = Porcelain.spawn_shell "ping google.com", out: IO.stream(:stdout, :line)
%Porcelain.Process{err: nil,
 out: %IO.Stream{device: :stdout, line_or_bytes: :line, raw: false},
 pid: #PID<0.74.0>}
iex(2)>
=ERROR REPORT==== 20-Jan-2015::00:14:52 ===
Error in process <0.78.0> with exit value: {badarg,[{io,put_chars,[stdout,unicode,<<112 bytes>>],[]},{'Elixir.Enum','-reduce/3-fun-0-',3,[{file,"lib/enum.ex"},{line,1266}]},{'Elixir.Stream',do_unfold,4,[{file,"lib/stream.ex"},{line,1126}]},{'Elixir.Enum',reduce,3,[{file,"lib/enum.ex"},{line,1265}]},{...

iex(3)> Porcelain.Process.alive? p
true
iex(4)> Porcelain.Process.stop p

# the shell just hangs
# the external process 'ping' remain alive even after terminating the VM

An example of successfully stopping a port:

iex(1)> p = Porcelain.spawn_shell "ping google.com", out: IO.binstream(:stdio, :line)
%Porcelain.Process{err: nil,
 out: %IO.Stream{device: :standard_io, line_or_bytes: :line, raw: true},
 pid: #PID<0.73.0>}
PING google.com (173.194.113.193): 56 data bytes
64 bytes from 173.194.113.193: icmp_seq=0 ttl=57 time=11.042 ms
...
iex(2)> Porcelain.Process.stop p
true

We don't get any more input, but ping keeps running in the background.

With goon

iex(1)> p = Porcelain.spawn_shell "ping google.com", out: IO.binstream(:stdio, :line)
%Porcelain.Process{err: nil,
 out: %IO.Stream{device: :standard_io, line_or_bytes: :line, raw: true},
 pid: #PID<0.74.0>}
PING google.com (173.194.113.194): 56 data bytes
64 bytes from 173.194.113.194: icmp_seq=0 ttl=57 time=8.044 ms
...
iex(2)> Porcelain.Process.stop p
true
iex(3)> panic: write /dev/stdout: broken pipe


                                             goroutine 3 [running]:
                                                                   runtime.panic(0xa4ba0, 0x2102a5420)
                                                                                                        /usr/local/Cellar/go/1.2.2/libexec/src/pkg/runtime/panic.c:266 +0xb6
     log.(*Logger).Panicf(0x2102a6190, 0xde260, 0x3, 0x221040fe30, 0x1, ...)
                                                                                /usr/local/Cellar/go/1.2.2/libexec/src/pkg/log/log.go:200 +0xbd
                                                                                                                                               main.fatal_if(0xc2840, 0x2102bf7e0)
            /Users/alco/extra/goworkspace/src/goon/util.go:38 +0x17e
                                                                        main.outLoop(0x257338, 0x2102860e8, 0x256fe8, 0x210286008, 0x0, ...)
                                                                                                                                                /Users/alco/extra/goworkspace/src/goon/io.go:151 +0x44a
                                created by main.wrapStdout
                                                            /Users/alco/extra/goworkspace/src/goon/io.go:34 +0x16a

                                                                                                                      goroutine 1 [chan receive]:
                                                                                                                                                 main.proto_2_0(0x7fff5fbf0100, 0xe3fc0, 0x3, 0xde7a0, 0x1, ...)
                                            /Users/alco/extra/goworkspace/src/goon/proto_2_0.go:58 +0x3a3
                                                                                                             main.main()
                                                                                                                            /Users/alco/extra/goworkspace/src/goon/main.go:51 +0x3b6

ping terminates, but goon panics.

** (Porcelain.UsageError) Looks like the :porcelain app is not running

I'm trying to write an Elixir app (using GenServer) that spawns a Node.js server on start, and keeps it running until it's shutdown. I'm trying to use Porcelain for this, but I keep getting this error even though :porcelain is listed among the applications in my mix.exs:

$ mix run
== Compilation error on file lib/my_app.ex ==
** (exit) an exception was raised:
    ** (Porcelain.UsageError) Looks like the :porcelain app is not running. Make sure you've added :porcelain to the list of applications in your mix.exs.
        lib/porcelain.ex:419: Porcelain.driver/0
        lib/porcelain.ex:228: anonymous fn/3 in Porcelain.spawn/3
        lib/porcelain.ex:273: Porcelain.catch_throws/1
        lib/my_app.ex:15: MyApp.init/1
        (stdlib) gen_server.erl:328: :gen_server.init_it/6
        (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3

Here's my GenServer:

defmodule MyApp do
  use GenServer
  alias Porcelain.Process, as: Proc

  @node_app "#{__DIR__}/my_node_app/server.js"

  def start do
    GenServer.start_link(__MODULE__, nil)
  end

  def init(state) do
    Process.flag(:trap_exit, true)

    state = %Proc{out: stream} =
      Porcelain.spawn("node", [@node_app], [out: :stream])

    Enum.into(stream, IO.stream(:stdio, :line))

    IO.puts "Started Node Server: #{inspect(state)}"
    {:ok, state}
  end

  def terminate(reason, state) do
    IO.puts "Going Down: #{inspect(state)}"
    Proc.stop(state)
    :normal
  end
end

MyApp.start

If I remove the last line (MyApp.start), the app works (although nothing happens) but I can start it from the iex -S mix session. What am I doing wrong?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.