akash-akya
Hi all,
I’m tinkering around the idea of streaming data through an external program (think streaming video through ffmpeg command and receiving the output back) from the last few weeks. Mainly focused on communicating with long-running programs with back-pressure. After exploring many approaches I settled on this. ExCmd uses named FIFO to solve back-pressure and other issues. It also uses odu (which is based on goon) to fill gaps in the erlang ports.
Currently, it’s at an early stage. I’m still thinking about the interface it should provide to expose all its functionality for different use cases effectively.
Please check it out and share your feedback ![]()
Background
Why not use built-in ports?
- Unlike beam ports, ExCmd puts back pressure on the external program
- Proper program termination. No more zombie process
- Ability to close stdin and wait for output (with ports one can not selectively close stdin)
While exploring the options, I also played around another approach, which does not use named FIFO. Its more like GenStage, the receiver beam process “demands” external program for output using stdin and stdout., but it has its own set of other issues.
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #phoenix_html
- #iex
- #graphql
- #genstage
- #ai
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex










First 10 of 18 Posts
akash-akya
Added ability to stream input and output. Now one can do something like this
Along with this there are many changes related to interface and error handling. Please check documentation for more details
v0.1.0
Github
dimitarvp
I have installed both
ex_cmdandodu(and have put it in myPATH). Then:Blocks for 5 seconds and gives me this:
Am I doing something wrong? The directory has 10 files in total, 2 of which are HTML.
akash-akya
Hi @dimitarvp, thanks for taking your time to check it.
In this case we are trying to read output FIFO (that is the output of the find command), without writing any input. This fails because the external wrapper command
oduis expecting a process to open input FIFO in write mode.This is so because we can not know if the external command needs input or not. odu assume every command needs an input stream and blocks till streams are connected.
So to fix we just have to open a dummy writer
I think, we should have better error message and maybe we should have an option for explicitly saying this command does not use input.
dimitarvp
Thanks for clarifying. This code is however not intuitive (and why do we need to spawn a
Taskfor it to work?). Would you consider adding options toExCmd.stream!then? For example, something likeExCmd.stream!(..., :stdout_only)would invisibly execute the code you pasted above.akash-akya
I agree I’ll add an option to disable stdin
tl;dr if we do not spawn separate process it will cause deadlock.
If anyone interested in this topic,
Stream is hiding synchronization happening between beam processes and external programs under the hood.
Without
Taskit would look something similar to thisthis is roughly equivalent to following steps with syscalls
step-2 is blocking call, this will return only after “input.pipe” is opened by the reader, which is the external program. But we start the external program at step-4, hence the deadlock.
This behavior is more visible if one uses low-level API instead of using stream abstraction.
opendoes have a non-blocking flagO_NONBLOCK. but,Interestingly, before OTP-21 allowed opening FIFO. A popular solution to open a FIFO in erlang/elixir was to use
:erlang.open_port.:erlang.open_portis blocking call too, but in this case, it blocks the whole vm!dimitarvp
Thanks a lot for the explanation! Makes sense.
Options like
:stdin_only,:stdout_onlyand:stdin_and_stdout(the default) would help a lot. Boilerplate should be hidden away behind options and/or convenience functions likestream_stdin!,stream_stdout!andstream!(which expects both as it is right now).Not sure about the names, they might not be good.
As for
oduitself, it introduces a needlessly complex external dependency installation that some programmers might not be willing to subject themselves to. I’d suggest you write an in-app small Rust library. I can help you integrate Elixir with Rust – the release candidate of Rustler 0.22 has a much nicer and shorter syntax compared to previous versions and is now a joy to use.dimitarvp
Another thing: would there be a way to stream the spawned command’s output line by line? Currently I have to store a rather huge string in memory and then call
String.split(the_whole_command_output, "\n"). Defeats the whole purpose of usingStreamreally.dimitarvp
I am also interested in your opinion on:
erlexec)akash-akya
Yes. I’m more inclined towards just adding additional
no_stdin,no_stdoutoptions and keep the samestream!interface. Just to avoid adding more functions, which can be confusing. But we can have separate functions if that makes more sense.Agree, I want to ditch the odu and have everything in a single library. When I started odu, I was mostly experimenting, and keeping it separate seemed simpler. Rustler looks interesting, I’ll look into it as soon as I get some time
.
Another thing, currently all this ceremony is because beam does not expose file descriptors for stdin and stdout. Definitely there will be some valid reason for that. But if we some how get a NIF/driver interface which let us access stdin/stdout fd, then we can just use
:file.openwith that fd and we can get rid of whole fifo thingy. This is all hand-waving, there might some issue in actual implementation.We can split as soon as we get the output right? something like
˚The size of data chunk we get (
datain abve example) depends on the command we are running and when that command flushs its output and when we are issuingread. This size is limited by fifo buffer size which is controlled by OS. Usually this size will be max 65kb (sometimes its less in mac os). So unless user is explicitly collecting output for something there should not be memory leak.I prefer to avoid adding spliting lines to ex_cmd itself. But if enough people want this, we can add
dimitarvp
Let me see if I am doing this right:
Then it can just be used like
stream_html_files_excmd("~/data/scraped.website") |> Enum.to_list().This gives me the list of files that I need (double-checked with previously stored runs of
finditself) and is working tens of times faster thanPath.wildcard.What sets your library apart is the
EnumerableandCollectableintegrations. Took me a bit to brush myStreamknowledge and with your help all is clear now.Thank you for helping.