• Stars
    star
    147
  • Rank 243,164 (Top 5 %)
  • Language
    Elixir
  • Created about 6 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Keep your ports contained

MuonTrap

CircleCI Hex version REUSE status

Keep programs, deamons, and applications launched from Erlang and Elixir contained and well-behaved. This lightweight library kills OS processes if the Elixir process running them crashes and if you're running on Linux, it can use cgroups to prevent many other shenanigans.

Some other features:

  • Attach your OS process to a supervision tree via a convenient child_spec
  • Set cgroup controls like thresholds on memory and CPU utilization
  • Start OS processes as a different user or group
  • Send SIGKILL to processes that aren't responsive to SIGTERM
  • With cgroups, ensure that all children of launched processes have been killed too

TL;DR

Add muontrap to your project's mix.exs dependency list:

def deps do
  [
    {:muontrap, "~> 1.0"}
  ]
end

Run a command similar to System.cmd/3:

iex>  MuonTrap.cmd("echo", ["hello"])
{"hello\n", 0}

Attach a long running process to a supervision tree using a child_spec like the following:

{MuonTrap.Daemon, ["long_running_command", ["arg1", "arg2"], options]}

Running on Linux and can use cgroups? Then create a new cgroup:

sudo cgcreate -a $(whoami) -g memory:mycgroup
{MuonTrap.Daemon,
 [
   "long_running_command",
   ["arg1", "arg2"],
   [cgroup_controllers: ["memory"], cgroup_base: "mycgroup"]
 ]}

MuonTrap will create a cgroup under "mycgroup" to run the "long_running_command". If the command fails, it will be restarted. If it should no longer be running (like if something else crashed in Elixir and supervision needs to clean up) then MuonTrap will kill "long_running_command" and all of its children.

Want to know more? Read on...

The problem

The Erlang VM's port interface lets Elixir applications run external programs. This is important since it's not practical to rewrite everything in Elixir. Plus, if the program is long running like a daemon or a server, you use Elixir to supervise it and restart it on crashes. The catch is that the Erlang VM expects port processes to be well-behaved. As you'd expect, many useful programs don't quite meet the Erlang VM's expectations.

For example, let's say that you want to monitor a network connection and decide that ping is the right tool. Here's how you could start ping in a process.

iex> pid = spawn(fn -> System.cmd("ping", ["-i", "5", "localhost"], into: IO.stream(:stdio, :line)) end)
#PID<0.6116.0>
PING localhost (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.032 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.077 ms

To see that ping is running, call ps to look for it. You can also do this from a separate terminal window outside of IEx:

iex> :os.cmd('ps -ef | grep ping') |> IO.puts
  501 38820 38587   0  9:26PM ??         0:00.01 /sbin/ping -i 5 localhost
  501 38824 38822   0  9:27PM ??         0:00.00 grep ping
:ok

Now exit the Elixir process. Imagine here that in the real program that something happened in Elixir and the process needs to exit and be restarted by a supervisor.

iex> Process.exit(pid, :oops)
true
iex> :os.cmd('ps -ef | grep ping') |> IO.puts
  501 38820 38587   0  9:26PM ??         0:00.02 /sbin/ping -i 5 localhost
  501 38833 38831   0  9:34PM ??         0:00.00 grep ping

As you can tell, ping is still running after the exit. If you run :observer you'll see that Elixir did indeed terminate both the process and the port, but that didn't stop ping. The reason for this is that ping doesn't pay attention to stdin and doesn't notice the Erlang VM closing it to signal that it should exit.

Imagine now that the process was supervised and it restarts. If this happens a regularly, you could be running dozens of ping commands.

This is just one of the problems that muontrap fixes.

Applicability

This is intended for long running processes. It's not great for interactive programs that communicate via the port or send signals. That feature is possible to add, but you'll probably be happier with other solutions like erlexec.

Running commands

The simplest way to use muontrap is as a replacement to System.cmd/3. Here's an example using ping:

iex> pid = spawn(fn -> MuonTrap.cmd("ping", ["-i", "5", "localhost"], into: IO.stream(:stdio, :line)) end)
#PID<0.30860.0>
PING localhost (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.027 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.081 ms

Now if you exit that process, ping gets killed as well:

iex> Process.exit(pid, :oops)
true
iex> :os.cmd('ps -ef | grep ping') |> IO.puts
  501 38898 38896   0  9:58PM ??         0:00.00 grep ping

:ok

Containment with cgroups

Even if you don't make use of any cgroup controller features, having your port process contained can be useful just to make sure that everything is cleaned up on exit including any subprocesses.

To set this up, first create a cgroup with appropriate permissions. Any path will do; muontrap just needs to be able to create a subdirectory underneath it for its use. For example:

sudo cgcreate -a $(whoami) -g memory,cpu:mycgroup

Be sure to create the group for all of the cgroup controllers that you wish to use with muontrap. The above example creates it for the memory and cpu controllers.

In Elixir, call MuonTrap.cmd/3 with the cgroup options now. In this case, we'll use the cpu controller, but this example would work fine with any of the controllers.

iex>  MuonTrap.cmd("spawning_program", [], cgroup_controllers: ["cpu"], cgroup_base: "mycgroup")
{"hello\n", 0}

In this example, muontrap runs spawning_program in a sub-cgroup under the cpu/mycgroup group. The cgroup parameters may be modified outside of muontrap using cgset or my accessing the cgroup mountpoint manually.

On any error or if the Erlang VM closes the port or if spawning_program exits, muontrap will kill all OS processes in cgroup. No need to worry about random processes accumulating on your system.

Note that if you use cgroup_base, a temporary cgroup is created for running the command. If you want muontrap to use a particular cgroup and not create a subgroup for the command, use the :cgroup_path option. Note that if you explicitly specify a cgroup, be careful not to use it for anything else. MuonTrap assumes that it owns the cgroup and when it needs to kill processes, it kills all of them in the cgroup.

Limit the memory used by a process

Linux's cgroups are very powerful and the examples here only scratch the surface. If you'd like to limit an OS process and all of its child processes to a maximum amount of memory, you can do that with the memory controller:

iex>  MuonTrap.cmd("memory_hog", [], cgroup_controllers: ["memory"], cgroup_base: "mycgroup", cgroup_sets: [{"memory", "memory.limit_in_bytes", "268435456"}])

That line restricts the total memory used by memory_hog to 256 MB.

Limit CPU usage in a port

Limiting the maximum CPU usage is also possible. Two parameters control that with the cpu controller: cpu.cfs_period_us specifies the number of microseconds in the scheduling period and cpu.cfs_quota_us specifies how many of those microseconds can be used. Here's an example call that prevents a program from using more than 50% of the CPU:

iex>  MuonTrap.cmd("cpu_hog", [], cgroup_controllers: ["cpu"], cgroup_base: "mycgroup", cgroup_sets: [{"cpu", "cpu.cfs_period_us", "100000"}, {"cpu", "cpu.cfs_quota_us", 50000}])

Supervision

For many long running programs, you may want to restart them if they crash. Luckily Erlang already has mechanisms to do this. MuonTrap provides a GenServer called MuonTrap.Daemon that you can hook into one of your supervision trees. For example, you could specify it like this in your application's supervisor:

  def start(_type, _args) do
    children = [
      {MuonTrap.Daemon, ["command", ["arg1", "arg2"], options]}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end

Supervisors provide three restart strategies, :permanent, :temporary, and :transient. They work as follows:

  • :permanent - Always restart the command if it exits or crashes. Restarts are limited to the Supervisor's restart intensity settings as they would be with normal GenServers. This is the default.
  • :transient - If the exit status of the command is 0 (i.e., success), then don't restart. Any other exit status is considered an error and the command is restarted.
  • :temporary - Don't restart

If you're running more than one MuonTrap.Daemon under the same Supervisor, then you'll need to give each one a unique :id. Here's an example child_spec for setting the :id and the :restart parameters:

    Supervisor.child_spec(
        {MuonTrap.Daemon, ["command", ["arg1"], options]},
         id: :my_daemon,
         restart: :transient
      )

stdio flow control

The Erlang port feature does not implement flow control from messages coming from the port process. Since MuonTrap captures stdio from the program being run, it's possible that the program sends output so fast that it grows the Elixir process's mailbox big enough to cause an out-of-memory error.

MuonTrap protects against this by implementing a flow control mechanism. When triggered, the running program's stdout and stderr file handles won't be read and hence it will eventually be blocked from writing to those handles.

The :stdio_window option specifies the maximum number of unacknowledged bytes allowed. The default is 10 KB.

muontrap development

In order to run the tests, some additional tools need to be installed. Specifically the cgcreate and cgget binaries need to be installed (and available on $PATH). Typically the package may be called cgroup-tools (on arch linux you need to install the libcgroup aur package).

Then run:

sudo cgcreate -a $(whoami) -g memory,cpu:muontrap_test

License

All original source code in this project is licensed under Apache-2.0.

Additionally, this project follows the REUSE recommendations and labels so that licensing and copyright are clear at the file level.

Exceptions to Apache-2.0 licensing are:

  • Configuration and data files are licensed under CC0-1.0
  • Documentation is CC-BY-4.0

More Repositories

1

elixir_ale

Interact with hardware in Elixir - GPIOs, I2C and SPI
C
339
star
2

code128

Trivial Code 128 barcode encoder
C
51
star
3

elixirbot

Elixir robots created for Erlang Factory 2015
Eagle
35
star
4

bbb-buildroot-fwup

Example project showing how to use buildroot and fwup
Makefile
26
star
5

midi_synth

Play music in Elixir
Elixir
26
star
6

relsync

Synchronize Erlang/OTP releases to remote nodes
Erlang
26
star
7

snake

Snake in Elixir
Elixir
23
star
8

nerves_system_npi_imx6ull

Nerves system for the Seeed Studio i.MX6ULL-eMMC dev board
Elixir
14
star
9

mmccopy

Convenient alternative to dd for writing images to SDCards
C
14
star
10

embedded-elixir

News and Articles on Elixir in Embedded Systems
HTML
10
star
11

qt-rectangles

Test program for OpenGL ES performance on RPi
C++
10
star
12

qt-opencl

Fixes for http://qt.gitorious.org/qt-labs/opencl
C++
8
star
13

raspijpgs

Simple command-line driven MotionJPEG streamer for the Raspberry Pi
C
8
star
14

see-experiments

Erlang
8
star
15

one_dhcpd

The Elixir DHCP Server that only serves one very important client
Elixir
8
star
16

hollowcore-h264

The Android OpenCORE multimedia framework stripped down to just the H.264 decoder to make it easier to integrate.
C++
7
star
17

vultr_example

Experiment in using Nerves to deploy to the clouds
JavaScript
7
star
18

fhunleth-buildroot-experiments

A Buildroot repository for various experiments
Shell
6
star
19

u-boot-squashfs

Experimental support for SquashFS in U-Boot
C
6
star
20

buildroot-edison

Working on Intel Edison support in Buildroot...
Makefile
6
star
21

buildroot-bbb

Read README below
C
5
star
22

wpa_supplicant.ex

See Nerves.WpaSupplicant now
C
5
star
23

nerves-see

Joe Armstrong's SEE code in the Nerves embedded environment
Erlang
5
star
24

rpi_video

Try to playback video on the RPi on Nerves
C
5
star
25

gpio_twiddler

GPIO twiddling benchmark
Elixir
5
star
26

android_external_i2c_tools

i2c_tools for Android
Perl
4
star
27

beam_benchmarks

Collection of benchmarks to run on devices running the BEAM
Erlang
4
star
28

mdns_query

C
4
star
29

circuits_led

Experiments
Elixir
4
star
30

nbtty

Simplification of dtach to prevent things from blocking a tty
C
4
star
31

osd32mp157c-brk-buildroot

Buildroot fork to support the OSD32MP157C-BRK board
Makefile
4
star
32

nerves_ev3_example

Example application for getting started with the Lego EV3 and Nerves
Elixir
4
star
33

dumpdm365

Utility for dumping and modifying registers on TI Davinci DM36x processors
C
4
star
34

ffmpeg

Experimental ffmpeg modifications
C
3
star
35

kiosk_system_rpi4

Nerves QtWebEngine Kiosk system for Raspberry Pi 4
Elixir
3
star
36

binary_clock

Build a Binary Clock with Elixir and Nerves
3
star
37

export_server

A port of Garrett Smith's port_server example to Elixir
Elixir
3
star
38

nerves_presenter

C++
3
star
39

nerves_system_up_board

Elixir
3
star
40

realtime_tests

Real-time experiments in Elixir
Elixir
3
star
41

nv12topnm

Simple utility for converting NV12 memory dumps to PNM images
C
3
star
42

keytar

Rockband 3 Keytar USB driver and key generator
C
3
star
43

nerves_system_jslinux

Elixir
3
star
44

eef-embedded-systems-wg

Temporary location for the EEF Embedded Systems WG repo
2
star
45

am3358_memtester

Bare metal port of memtester to AM335x-based boards
C
2
star
46

fwup-snap

Metadata for building snap packages for fwup
2
star
47

canbcm

Old Erlang project to interface with the Linux SocketCAN BCM driver (not maintained)
C
2
star
48

fbgrab

Random fbgrab updates
C
2
star
49

logging_demo

RingLogger demo code for the Baltimore Elixir meetup
Elixir
2
star
50

intel_hex

Decode Intel hex records in Elixir
Elixir
2
star
51

nerves_system_rpi3_kiosk

See https://github.com/LeToteTeam/kiosk_system_rpi3 now
HTML
2
star
52

elixir-ply

ply for Elixir (VERY EXPERIMENTAL)
Elixir
2
star
53

l2elog

Linux syslog/klog bridge to Erlang's Lager logger
Erlang
2
star
54

nerves_system_orange_pi_zero_3

Nerves system for the OrangePi Zero 3
Elixir
2
star
55

iodata_nif

IOData NIF experiments
Elixir
2
star
56

dtach

Patches to http://dtach.sourceforge.net/ (These have been merged!)
C
1
star
57

nerves_key

1
star
58

nerves_system_alix

Base Nerves system configuration for the PC Engines ALIX - WARNING: Unmaintained
Elixir
1
star
59

dotfiles

Yay
Shell
1
star
60

net_managers.ex

Use Nerves.InterimWifi for wifi support
Elixir
1
star
61

elixirport

Elixir/Port process test app
C
1
star
62

erlangdc_demo

Nerves demo app for ErlangDC 2013
Erlang
1
star
63

nerves_system_galileo

Base Nerves system configuration for the Intel Galileo Gen 2 - WARNING: Unmaintained
Elixir
1
star
64

tls_experiment

Elixir
1
star
65

erlang_fileutils

Erlang versions of Unix file utilities to enable distributed use
Erlang
1
star
66

sdburner

UI for CoderDojoDC Raspberry Pi SDCard burner device
C++
1
star
67

dumb_ldattach

ldattach, but dumber
C
1
star
68

jsnerves

Emscripten, TinyEMU, Nerves experiment
JavaScript
1
star
69

silhouette

Experiments in making shadows
C++
1
star
70

test_build_embedded

Elixir
1
star
71

troodon-cam

Work in progress AM335x PRU -> camera interface with Erlang
C
1
star
72

psk_maker

A to-be-named library that supports the passphrase->psk algorithm and maybe more
Elixir
1
star
73

buildroot-pc-dhcpserver

DHCP/DNS server for managing a LAN island - ISO image
C
1
star
74

android_external_lsof

lsof for Android
C
1
star
75

buildroot-olinuxino

Buildroot repo for Olimex OLinuxino support
C
1
star
76

dm36x-packager

Python
1
star
77

presi-aoke

Presentation Karaoke Player
C++
1
star
78

troodonsw-website

Troodon Software website source code
CSS
1
star
79

laser-cutter-svgs

SVG files and utilities for various laser cutter designs
1
star
80

v4l2reg

Utility for getting and setting registers on V4L2 cameras
C
1
star
81

nerves_system_bbb_kiosk

Beaglebone Black configuration that drives a display
Shell
1
star
82

nerves_system_ag150

Base Nerves system configuration for the Logic Supply AG150 - WARNING: Unmaintained
Elixir
1
star
83

htc-kernel-incrediblec

2.6.32.15 kernel source for HTC incredible (use CyanogenMod version of kernel once it gets updated)
C
1
star
84

net_basic.ex

This has been moved to Nerves.NetworkInterface
C
1
star