Full Code of fhunleth/muontrap for AI

main 172a9ba49a0b cached
40 files
156.6 KB
41.9k tokens
162 symbols
1 requests
Download .txt
Repository: fhunleth/muontrap
Branch: main
Commit: 172a9ba49a0b
Files: 40
Total size: 156.6 KB

Directory structure:
gitextract_g35awbco/

├── .circleci/
│   └── config.yml
├── .credo.exs
├── .formatter.exs
├── .gitignore
├── CHANGELOG.md
├── LICENSES/
│   ├── Apache-2.0.txt
│   ├── CC-BY-4.0.txt
│   └── CC0-1.0.txt
├── Makefile
├── NOTICE
├── README.md
├── REUSE.toml
├── c_src/
│   ├── Makefile
│   └── muontrap.c
├── lib/
│   ├── muontrap/
│   │   ├── cgroups.ex
│   │   ├── daemon.ex
│   │   ├── options.ex
│   │   └── port.ex
│   └── muontrap.ex
├── mix.exs
└── test/
    ├── Makefile
    ├── cgroup_test.exs
    ├── chatty.c
    ├── daemon_test.exs
    ├── do_nothing.c
    ├── echo_both.c
    ├── echo_junk.c
    ├── echo_stderr.c
    ├── echo_stdio.c
    ├── fork_a_lot.c
    ├── ignore_sigterm.c
    ├── kill_self_with_signal.c
    ├── kill_self_with_sigusr1.c
    ├── muontrap_test.exs
    ├── options_test.exs
    ├── port_test.exs
    ├── print_a_lot.c
    ├── succeed_second_time.c
    ├── support/
    │   └── test_case.ex
    └── test_helper.exs

================================================
FILE CONTENTS
================================================

================================================
FILE: .circleci/config.yml
================================================
version: 2.1

latest: &latest
  pattern: "^1.19.*-erlang-28.*$"

tags: &tags
  [
    1.19.4-erlang-28.2-alpine-3.22.2,
    1.18.4-erlang-27.3.3-alpine-3.21.3,
    1.17.3-erlang-27.1.3-alpine-3.20.3,
    1.16.1-erlang-26.2.2-alpine-3.19.1,
    1.15.7-erlang-26.2.2-alpine-3.19.1,
    1.14.5-erlang-25.3.2.9-alpine-3.19.1,
    1.13.4-erlang-24.3.4-alpine-3.15.3,
    1.12.3-erlang-24.3.4-alpine-3.15.3,
    1.11.4-erlang-23.3.4.13-alpine-3.15.3,
  ]

jobs:
  check-license:
    docker:
      - image: fsfe/reuse:latest
    steps:
      - checkout
      - run: reuse lint

  build-test:
    parameters:
      tag:
        type: string
    docker:
      - image: hexpm/elixir:<< parameters.tag >>
    working_directory: ~/repo
    environment:
      LC_ALL: C.UTF-8
    steps:
      - run:
          name: Install system dependencies
          command: apk add --no-cache build-base procps
      - checkout
      - run:
          name: Install hex and rebar
          command: |
            mix local.hex --force
            mix local.rebar --force
      - restore_cache:
          keys:
            - v1-mix-cache-<< parameters.tag >>-{{ checksum "mix.lock" }}
      - run: mix deps.get
      - run: mix test --exclude cgroup
      - when:
          condition:
            matches: { <<: *latest, value: << parameters.tag >> }
          steps:
            - run: mix format --check-formatted
            - run: mix deps.unlock --check-unused
            - run: mix docs
            - run: mix hex.build
            - run: mix credo -a --strict
            - run: mix dialyzer
      - save_cache:
          key: v1-mix-cache-<< parameters.tag >>-{{ checksum "mix.lock" }}
          paths:
            - _build
            - deps

workflows:
  checks:
    jobs:
      - check-license
      - build-test:
          name: << matrix.tag >>
          matrix:
            parameters:
              tag: *tags


================================================
FILE: .credo.exs
================================================
# .credo.exs
%{
  configs: [
    %{
      name: "default",
      strict: true,
      checks: [
        {Credo.Check.Refactor.MapInto, false},
        {Credo.Check.Warning.LazyLogging, false},
        {Credo.Check.Readability.LargeNumbers, only_greater_than: 86400},
        {Credo.Check.Readability.ParenthesesOnZeroArityDefs, parens: true},
        {Credo.Check.Readability.Specs, tags: []},
        {Credo.Check.Readability.StrictModuleLayout, tags: []}
      ]
    }
  ]
}


================================================
FILE: .formatter.exs
================================================
# Used by "mix format"
[
  inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"]
]


================================================
FILE: .gitignore
================================================
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
muontrap-*.tar

/priv

*.o
/muontrap-*.log
test/*.test


================================================
FILE: CHANGELOG.md
================================================
# Changelog

## v1.7.0

* New feature
  * Add `:capture_stderr_only` option to capture only stderr while ignoring stdout.
    This is useful when you want to capture error messages but not regular output.
    Works with both `MuonTrap.cmd/3` and `MuonTrap.Daemon`. (@fermuch)

## v1.6.1

* Bug fixes
  * Ignore transient EAGAIN, EWOULDBLOCK, and EINTR errors when processing
    acknowledgments from Erlang. These would cause unneeded restarts.
    (@mediremi)

## v1.6.0

* New feature
  * Add `:logger_fun` option to `MuonTrap.Daemon` to allow complete
    customization of the logging process. Pass it a 1-arity function or `mfargs`
    tuple. This option takes precedence over all of the other log related
    options.  (@bjyoungblood)

## v1.5.0

* New feature
  * Add Logger metadata in `MuonTrap.Daemon`. See the `:logger_metadata` option.
    (@bjyoungblood)

## v1.4.1

* Bug fixes
  * Support logging output to all Elixir logger levels. Previously the "new" set
    that includes emergency, critical, warning, etc. would fail the option check
  * Default the `log_transform` option to replace invalid UTF8 characters so
    they don't crash the Logger. This fixes an annoyance where a program would
    do this and there'd be log crash spam. It's still overridable, so users
    using custom loggers that already handle this can pass
    `Function.identity/1` to disable. (@jjcarstens)

## v1.4.0

* New feature
  * Add a timeout option to `MuonTrap.cmd/3`. OS processes that take too long
    will be killed and a `:timeout` return status returned. This is backwards
    compatible. Thanks to @bjyoungblood for adding this feature.

## v1.3.3

* Bug fixes
  * Fix issue where lots of prints from a child process when the Erlang process
    side is killed can cause MuonTrap to not clean up the child process. There
    are some potential variations on this that were also fixed even though they
    were unseen. Thanks to @bjyoungblood for figuring this out.

* Improvements
  * Improve debug logging so that when enabled, fatal errors are written to the
    log as well and not to stderr.

## v1.3.2

* Bug fixes
  * Fix C compiler error when building with older versions of gcc. This fixes an
    compile error with Ubuntu 20.04, for example.

## v1.3.1

* Bug fixes
  * Fix regression where stderr would be printed when `stderr_to_stdout: true`
    was specified and logging disabled.

## v1.3.0

* New feature
  * Add flow control to stdout (and stderr if capturing it) to prevent
    out-of-memory VM crashes from programs that can spam stdout. The output
    would accumulate in the process mailbox waiting to be processed. The flow
    control implementation will push back and slow down output generation. The
    number of bytes in flight defaults to 10 KB and is set with the new
    `:stdio_window` parameter. (@jjcarstens)

* Bug fixes
  * Fix various minor issues preventing unit tests from passing on MacOS.
    (@jjcarstens)

## v1.2.0

* New feature
  * Added `:exit_status_to_reason` to the `Daemon` to be able to change how the
    `Daemon` GenServer exits based on the exit status of the program being run.
    (@erauer)

## v1.1.0

* New features
  * Support transforming output from programs before sending to the log. See the
    new `:log_transform` option. (@brunoro)

## v1.0.0

This release only changes the version number. It has no code changes.

## v0.6.1

This release has no code changes.

* Improvements
  * Clean up build prints, fix a doc typo, and update dependencies for fresher
    docs.

## v0.6.0

* Bug fixes
  * Fix the `:delay_to_sigkill` option so that it takes milliseconds as
    documented and remove the max delay check. Previously, the code used
    microseconds for the delay despite the documentation. If you were using
    `:delay_to_sigkill`, this is a backwards incompatible change and your delays
    will be 1000x longer. Thanks to Almir for reporting this issue.

## v0.5.1

* New features
  * Added the `:log_prefix` option to MuonTrap.Daemon so that logged output can
    be annotated in more helpful ways. This is useful when running the same
    program multiple times, but with different configurations.

## v0.5.0

This update contains many changes throughout. If you're using cgroups, please
review the changes as they likely affect your code.

* New features
  * Added `:cgroup_base`. The preferred way of using cgroups now is for MuonTrap
    to create a sub-cgroup for running the command. This removes the need to
    keep track of cgroup paths on your own when you run more than one command at
    a time. `:cgroup_path` is still available.
  * Almost all inconsistencies between MuonTrap.Daemon and MuonTrap.cmd/3 have
    been fixed. As a result, MuonTrap.Daemon detects and raises more exceptions
    than previous. It is possible that code that worked before will now break.
  * MuonTrap.Daemon sets its exit status based on the process's exit code.
    Successful exit codes (exit code 0) exit `:normal` and failed exit codes
    (anything else) do not. This makes it possible to use the Supervisor
    `:temporary` restart strategy that only restarts failures.
  * MuonTrap.Daemon supports a `:name` parameter for setting GenServer names.
  * MuonTrap.Daemon `cgget` and `cgset` helpers return ok/error tuples now since
    it was too easy to accidentally call them such that they'd raise.

* Bug fixes
  * Forcefully killed processes would get stuck in a zombie state until the kill
    timeout expired due to a missing call to wait(2). This has been fixed.
  * Exit status of process killed by a signal reflects that. I.e., a process
    killed by a signal exits with a status of 128+signal.

## v0.4.4

* Bug fixes
  * Fixed an issue where environment variable lists passed to MuonTrap.Daemon
    had to be charlists rather than Elixir strings like MuonTrap.cmd/3 and
    System.cmd/3.

## v0.4.3

* Bug fixes
  * Reverted removal of `child_spec`

## v0.4.2

* New features
  * MuonTrap.Daemon can log stderr now as well as stdout. Pass
    `stderr_to_stdout: true` in the options. Thanks to Timmo Verlaan for this
    update.

## v0.4.1

* Improvements
  * Move port process build products under `_build`. This fixes an issue where
    changes in MIX_TARGET settings would not be picked up.
  * Improved some specs to remove Dialyzer warnings in some cases

## v0.4.0

* New features
  * MuonTrap.Daemon no longer sends all of the output from the process to the
    logger by default. If you want it logged, pass in a `{:log_output, level}`
    option. This also slightly improves the logged message to make it easier
    to read.

## v0.3.1

* Bug fixes
  * Make MuonTrap.Daemon usable (child_specs, options)

## v0.3.0

* Bug fixes
  * Make MuonTrap.cmd/3 pass the System.cmd/3 tests
  * Add a few more specs and fix Dialyzer errors

## v0.2.2

* Bug fixes
  * Add missing dependency on `:logger`

## v0.2.1

* Bug fixes
  * Fix hex package contents

## v0.2.0

* Bug fixes
  * Fix shutdown timeout and issues with getting EINTR
  * More progress on cgroup testing; docs

## v0.1.0

* Initial release


================================================
FILE: LICENSES/Apache-2.0.txt
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.

"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.

"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.

"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.

"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.

"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.

"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).

"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.

"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."

"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.

3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:

     (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and

     (b) You must cause any modified files to carry prominent notices stating that You changed the files; and

     (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and

     (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.

     You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!)  The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.

Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


================================================
FILE: LICENSES/CC-BY-4.0.txt
================================================
Creative Commons Attribution 4.0 International

 Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.

Using Creative Commons Public Licenses

Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.

Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors.

Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public.

Creative Commons Attribution 4.0 International Public License

By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.

Section 1 – Definitions.

     a.	Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.

     b.	Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.

     c.	Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.

     d.	Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.

     e.	Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.

     f.	Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.

     g.	Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.

     h.	Licensor means the individual(s) or entity(ies) granting rights under this Public License.

     i.	Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.

     j.	Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.

     k.	You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.

Section 2 – Scope.

     a.	License grant.

          1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:

               A. reproduce and Share the Licensed Material, in whole or in part; and

               B. produce, reproduce, and Share Adapted Material.

          2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.

          3. Term. The term of this Public License is specified in Section 6(a).

          4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.

          5. Downstream recipients.

               A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.

               B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.

          6.  No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).

b. Other rights.

          1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.

          2. Patent and trademark rights are not licensed under this Public License.

          3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.

Section 3 – License Conditions.

Your exercise of the Licensed Rights is expressly made subject to the following conditions.

     a.	Attribution.

          1. If You Share the Licensed Material (including in modified form), You must:

               A. retain the following if it is supplied by the Licensor with the Licensed Material:

                    i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);

                    ii. a copyright notice;

                    iii. a notice that refers to this Public License;

                    iv.	a notice that refers to the disclaimer of warranties;

                    v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;

               B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and

               C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.

          2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.

          3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.

          4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License.

Section 4 – Sui Generis Database Rights.

Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:

     a.	for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;

     b.	if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and

     c.	You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.

Section 5 – Disclaimer of Warranties and Limitation of Liability.

     a.	Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.

     b.	To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.

     c.	The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.

Section 6 – Term and Termination.

     a.	This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.

     b.	Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:

          1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or

          2. upon express reinstatement by the Licensor.

     c.	For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.

     d.	For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.

     e.	Sections 1, 5, 6, 7, and 8 survive termination of this Public License.

Section 7 – Other Terms and Conditions.

     a.	The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.

     b.	Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.

Section 8 – Interpretation.

     a.	For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.

     b.	To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.

     c.	No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.

     d.	Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.

Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.

Creative Commons may be contacted at creativecommons.org.


================================================
FILE: LICENSES/CC0-1.0.txt
================================================
Creative Commons Legal Code

CC0 1.0 Universal

    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
    HEREUNDER.

Statement of Purpose

The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").

Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.

For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.

1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:

  i. the right to reproduce, adapt, distribute, perform, display,
     communicate, and translate a Work;
 ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
     likeness depicted in a Work;
 iv. rights protecting against unfair competition in regards to a Work,
     subject to the limitations in paragraph 4(a), below;
  v. rights protecting the extraction, dissemination, use and reuse of data
     in a Work;
 vi. database rights (such as those arising under Directive 96/9/EC of the
     European Parliament and of the Council of 11 March 1996 on the legal
     protection of databases, and under any national implementation
     thereof, including any amended or successor version of such
     directive); and
vii. other similar, equivalent or corresponding rights throughout the
     world based on applicable law or treaty, and any national
     implementations thereof.

2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.

3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.

4. Limitations and Disclaimers.

 a. No trademark or patent rights held by Affirmer are waived, abandoned,
    surrendered, licensed or otherwise affected by this document.
 b. Affirmer offers the Work as-is and makes no representations or
    warranties of any kind concerning the Work, express, implied,
    statutory or otherwise, including without limitation warranties of
    title, merchantability, fitness for a particular purpose, non
    infringement, or the absence of latent or other defects, accuracy, or
    the present or absence of errors, whether or not discoverable, all to
    the greatest extent permissible under applicable law.
 c. Affirmer disclaims responsibility for clearing rights of other persons
    that may apply to the Work or any use thereof, including without
    limitation any person's Copyright and Related Rights in the Work.
    Further, Affirmer disclaims responsibility for obtaining any necessary
    consents, permissions or other rights required for any use of the
    Work.
 d. Affirmer understands and acknowledges that Creative Commons is not a
    party to this document and has no duty or obligation with respect to
    this CC0 or use of the Work.


================================================
FILE: Makefile
================================================
calling_from_make:
	mix compile

all:
	$(MAKE) -C c_src all
	if [ -f test/Makefile ]; then $(MAKE) -C test; fi

clean:
	$(MAKE) -C c_src clean
	if [ -f test/Makefile ]; then $(MAKE) -C test clean; fi

.PHONY: all clean calling_from_make

.SILENT:


================================================
FILE: NOTICE
================================================
Muontrap is open-source software licensed under the Apache License, Version
2.0.

Copyright holders include Frank Hunleth, Matt Ludwigs, Jason Axelson, Timmo
Verlaan, Aldebaran Alonso, Gustavo Brunoro, Ben Youngblood, Eric Rauer, Jon
Carstens, Milan Vit, Fernando Mumbach and Médi-Rémi Hashim.

Authoritative REUSE-compliant copyright and license metadata available at
https://hex.pm/packages/muontrap.


================================================
FILE: README.md
================================================
# MuonTrap

[![Hex version](https://img.shields.io/hexpm/v/muontrap.svg "Hex version")](https://hex.pm/packages/muontrap)
[![API docs](https://img.shields.io/hexpm/v/muontrap.svg?label=hexdocs "API docs")](https://hexdocs.pm/muontrap/MuonTrap.html)
[![CircleCI](https://dl.circleci.com/status-badge/img/gh/fhunleth/muontrap/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/fhunleth/muontrap/tree/main)
[![REUSE status](https://api.reuse.software/badge/github.com/fhunleth/muontrap)](https://api.reuse.software/info/github.com/fhunleth/muontrap)

Keep programs, daemons, 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:

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

Run a command similar to
[`System.cmd/3`](https://hexdocs.pm/elixir/System.html#cmd/3):

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

Attach a long running process to a supervision tree using a
[child_spec](https://hexdocs.pm/elixir/Supervisor.html#module-child-specification)
like the following:

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

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

```bash
sudo cgcreate -a $(whoami) -g memory:mycgroup
```

```elixir
{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 about the motivations for this library? Read on in the
[Background](#background) section.

## FAQ

### How do I watch stdout?

If you're using `MuonTrap.cmd/3`, you don't get the called program's output
until after it exits. Just like `System.cmd/3`, the `:into` option can be used
to get the output as it's printed. Here's an example.

```elixir
MuonTrap.cmd("my_program", [], stderr_to_stdout: true, into: IO.binstream(:stdio, :line))
```

If you're using `MuonTrap.Daemon`, then the best way is to send output to the
logger. There are quite a few options, so see the `MuonTrap.Daemon` docs on what
makes sense for you.

### How do I stop a MuonTrap.Daemon?

Treat the `MuonTrap.Daemon` process just like any other Elixir process. If you
put it in a supervision tree, call `Supervisor.terminate_child/2`. If you have
it's pid, call `Process.exit/2`.

## Background

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.

```elixir
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:

```elixir
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.

```elixir
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](https://github.com/saleyn/erlexec/).

## Running commands

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

```elixir
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:

```elixir
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:

```bash
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.

```elixir
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:

```elixir
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:

```elixir
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:

```elixir
  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 `GenServer`s. 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:

```elixir
    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:

```sh
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](https://reuse.software)
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


================================================
FILE: REUSE.toml
================================================
version = 1

[[annotations]]
path = [
 ".circleci/config.yml",
 ".credo.exs",
 ".formatter.exs",
 ".github/dependabot.yml",
 ".gitignore",
 "CHANGELOG.md",
 "Makefile",
 "c_src/Makefile",
 "test/Makefile",
 "NOTICE",
 "REUSE.toml",
 "mix.exs",
 "mix.lock"
]
precedence = "aggregate"
SPDX-FileCopyrightText = "None"
SPDX-License-Identifier = "CC0-1.0"

[[annotations]]
path = [
 "README.md"
]
precedence = "aggregate"
SPDX-FileCopyrightText = "2018 Frank Hunleth"
SPDX-License-Identifier = "CC-BY-4.0"


================================================
FILE: c_src/Makefile
================================================
# Makefile for building the muontrap port process
#
# Makefile targets:
#
# all/install   build and install
# clean         clean build products and intermediates
#
# Variables to override:
#
# MIX_APP_PATH  path to the build directory
# CC            C compiler. MUST be set if crosscompiling
# CFLAGS        compiler flags for compiling all C files
# LDFLAGS       linker flags for linking all binaries

PREFIX = $(MIX_APP_PATH)/priv
BUILD  = $(MIX_APP_PATH)/obj

MUONTRAP = $(PREFIX)/muontrap

LDFLAGS +=
CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter
# _GNU_SOURCE is needed for splice(2) on Linux
CFLAGS += -std=c99 -D_GNU_SOURCE -Wno-empty-body

#CFLAGS += -DDEBUG

SRC = $(wildcard *.c)
OBJ = $(SRC:%.c=$(BUILD)/%.o)

calling_from_make:
	cd .. && mix compile

all: install

install: $(PREFIX) $(BUILD) $(MUONTRAP)

$(OBJ): Makefile

$(BUILD)/%.o: %.c
	@echo " CC $(notdir $@)"
	$(CC) -c $(CFLAGS) -o $@ $<

$(MUONTRAP): $(OBJ)
	@echo " LD $(notdir $@)"
	$(CC) $^ $(LDFLAGS) -o $@

$(PREFIX) $(BUILD):
	mkdir -p $@

clean:
	$(RM) $(MUONTRAP) $(BUILD)/*.o

.PHONY: all clean calling_from_make install

# Don't echo commands unless the caller exports "V=1"
${V}.SILENT:


================================================
FILE: c_src/muontrap.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
// SPDX-FileCopyrightText: 2023 Jon Carstens
// SPDX-FileCopyrightText: 2025 Fernando Mumbach
// SPDX-FileCopyrightText: 2025 Médi-Rémi Hashim
//
// SPDX-License-Identifier: Apache-2.0

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <grp.h>
#include <poll.h>
#include <pwd.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

// IMPORTANT:
// The FATAL* macros mirror err(3) and errx(3) which also exit. Exiting does not clean up
// the child process which defeats one of the reasons to use MuonTrap in the first place.
// Be careful to use these macros in places where the child is not running.
#ifdef DEBUG
static FILE *debug_fp = NULL;
#define INFO(MSG, ...) do { fprintf(debug_fp, "%d INFO:" MSG "\n", microsecs(), ## __VA_ARGS__); fflush(debug_fp); } while (0)
#define WARN(MSG, ...) do { fprintf(debug_fp, "%d WARN:" MSG "\n", microsecs(), ## __VA_ARGS__); fflush(debug_fp); } while (0)
#define WARNX(MSG, ...) do { fprintf(debug_fp, "%d WARN:" MSG "\n", microsecs(), ## __VA_ARGS__); fflush(debug_fp); } while (0)
#define FATAL(MSG, ...) do { fprintf(debug_fp, "%d  ERR:" MSG "\n", microsecs(), ## __VA_ARGS__); fflush(debug_fp); exit(EXIT_FAILURE); } while (0)
#define FATALX(MSG, ...) do { fprintf(debug_fp, "%d  ERR:" MSG "\n", microsecs(), ## __VA_ARGS__); fflush(debug_fp); exit(EXIT_FAILURE); } while (0)
#else
#define INFO(MSG, ...) ;
#define WARN(MSG, ...) ;
#define WARNX(MSG, ...) ;
#define FATAL(MSG, ...) do { fprintf(stderr, "MUONTRAP: " MSG "\n",  ## __VA_ARGS__); exit(EXIT_FAILURE); } while (0)
#define FATALX(MSG, ...) do { fprintf(stderr, "MUONTRAP: " MSG "\n",  ## __VA_ARGS__); exit(EXIT_FAILURE); } while (0)
#endif

// asprintf can fail, but it's so rare that it's annoying to see the checks in the code.
#define checked_asprintf(MSG, ...) do { if (asprintf(MSG, ## __VA_ARGS__) < 0) FATAL("asprintf"); } while (0)

static struct option long_options[] = {
    {"arg0", required_argument, 0, '0'},
    {"controller", required_argument, 0, 'c'},
    {"help",     no_argument,       0, 'h'},
    {"delay-to-sigkill", required_argument, 0, 'k'},
    {"group", required_argument, 0, 'g'},
    {"set", required_argument, 0, 's'},
    {"uid", required_argument, 0, 'u'},
    {"gid", required_argument, 0, 'a'},
    {"stdio-window", required_argument, 0, 'l'},
    {"capture-output", no_argument, 0, 'o'},
    {"capture-stderr", no_argument, 0, 'e'},
    {"capture-stderr-only", no_argument, 0, 'r'},
    {0,          0,                 0, 0 }
};

#define CGROUP_MOUNT_PATH "/sys/fs/cgroup"

struct controller_var {
    struct controller_var *next;
    const char *key;
    const char *value;
};

struct controller_info {
    const char *name;
    char *group_path;
    char *procfile;

    struct controller_var *vars;
    struct controller_info *next;
};

static struct controller_info *controllers = NULL;
static const char *cgroup_path = NULL;
static int brutal_kill_wait_ms = 500;
static uid_t run_as_uid = 0; // 0 means don't set, since we don't support privilege escalation
static gid_t run_as_gid = 0; // 0 means don't set, since we don't support privilege escalation

static int signal_pipe[2] = { -1, -1};
static int stdout_pipe[2] = { -1, -1};
static int stderr_pipe[2] = { -1, -1};

#define DEFAULT_STDIO_WINDOW 10240 // Allow up to 10 KB out to Elixir at a time
static int stdio_bytes_max = DEFAULT_STDIO_WINDOW;
static int stdio_bytes_avail = DEFAULT_STDIO_WINDOW;
static int capture_output = 0; // Don't capture output by default
static int capture_stderr = 0; // If capturing output, don't capture stderr by default
static int capture_stderr_only = 0; // Capture stderr only, ignore stdout

#define FOREACH_CONTROLLER for (struct controller_info *controller = controllers; controller != NULL; controller = controller->next)

static void move_pid_to_cgroups(pid_t pid);

static void usage()
{
    printf("Usage: muontrap [OPTION] -- <program> <args>\n");
    printf("\n");
    printf("Options:\n");

    printf("--arg0,-0 <arg0>\n");
    printf("--controller,-c <cgroup controller> (may be specified multiple times)\n");
    printf("--group,-g <cgroup path>\n");
    printf("--set,-s <cgroup variable>=<value>\n (may be specified multiple times)\n");
    printf("--delay-to-sigkill,-k <milliseconds>\n");
    printf("--stdio-window <bytes>\n");
    printf("--capture-output\n");
    printf("--capture-stderr\n");
    printf("--capture-stderr-only\n");
    printf("--uid <uid/user> drop privilege to this uid or user\n");
    printf("--gid <gid/group> drop privilege to this gid or group\n");
    printf("-- the program to run and its arguments come after this\n");
}

static int microsecs()
{
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return (ts.tv_sec * 1000000) + (ts.tv_nsec / 1000);
}

void sigchild_handler(int signum)
{
    if (signal_pipe[1] >= 0 &&
            write(signal_pipe[1], &signum, sizeof(signum)) < 0)
        WARN("write(signal_pipe)");
}

void enable_signal_handlers()
{
    struct sigaction sa;
    sa.sa_handler = sigchild_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    sigaction(SIGCHLD, &sa, NULL);
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGQUIT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
}

void disable_signal_handlers()
{
    sigaction(SIGCHLD, NULL, NULL);
    sigaction(SIGINT, NULL, NULL);
    sigaction(SIGQUIT, NULL, NULL);
    sigaction(SIGTERM, NULL, NULL);
}

static int fork_exec(const char *path, char *const *argv)
{
    INFO("Running %s", path);
    for (char *const *arg = argv; *arg != NULL; arg++) {
        INFO("  arg: %s", *arg);
    }

    pid_t pid = fork();
    if (pid == 0) {
        // child

        // Move to the container
        move_pid_to_cgroups(getpid());

        if (capture_stderr_only) {
            // Capture stderr only, send stdout to /dev/null
            int dev_null_fd = open("/dev/null", O_WRONLY);
            if (dev_null_fd < 0)
                FATAL("Can't open /dev/null");

            // Send stdout to /dev/null
            if (dup2(dev_null_fd, STDOUT_FILENO) < 0)
                FATAL("dup2 STDOUT_FILENO");

            // Capture stderr
            if (dup2(stderr_pipe[1], STDERR_FILENO) < 0)
                FATAL("dup2 STDERR_FILENO");

            close(dev_null_fd);
        } else if (capture_output) {
            // Replace stdout a with flow controlled versions
            if (dup2(stdout_pipe[1], STDOUT_FILENO) < 0)
                FATAL("dup2 STDOUT_FILENO");

            // If capturing stderr too, do the same thing.
            if (capture_stderr) {
                if (dup2(stderr_pipe[1], STDERR_FILENO) < 0)
                    FATAL("dup2 STDERR_FILENO");
            }
        } else {
            // Not capturing stdout, so send it to /dev/null to get it dropped with as little processing as possible
            int dev_null_fd = open("/dev/null", O_WRONLY);
            if (dev_null_fd < 0)
                FATAL("Can't open /dev/null");

            if (dup2(dev_null_fd, STDOUT_FILENO) < 0)
                FATAL("dup2 STDOUT_FILENO");

            // If not capturing output at all, but the user says to capture
            // stderr, send stderr to /dev/null as well. As odd as this sounds
            // here, it's due to the `:stderr_to_stdout` option mapping to
            // `capture_stderr`.
            if (capture_stderr) {
                if (dup2(dev_null_fd, STDERR_FILENO) < 0)
                    FATAL("dup2 STDERR_FILENO");
            }

            close(dev_null_fd);
        }

        // Drop/change privilege if requested
        // See https://wiki.sei.cmu.edu/confluence/display/c/POS36-C.+Observe+correct+revocation+order+while+relinquishing+privileges
        if (run_as_gid > 0 && setgid(run_as_gid) < 0)
            FATAL("setgid(%d)", run_as_gid);

        if (run_as_uid > 0 && setuid(run_as_uid) < 0)
            FATAL("setuid(%d)", run_as_uid);

        execvp(path, argv);

        // Not supposed to reach here.
        exit(EXIT_FAILURE);
    } else {

        return pid;
    }
}

static int mkdir_p(const char *abspath, int start_index)
{
    int rc = 0;
    int last_errno = 0;
    char *group_path = strdup(abspath);
    for (int i = start_index; ; i++) {
        if (group_path[i] == '/' || group_path[i] == 0) {
            char save = group_path[i];
            group_path[i] = 0;
            rc = mkdir(group_path, 0755);
            if (rc < 0)
                last_errno = errno;

            group_path[i] = save;
            if (save == 0)
                break;
        }
    }
    free(group_path);

    // Return the last call to mkdir since that's the one that matters
    // and earlier directories are likely already created.
    errno = last_errno;
    return rc;
}

static void create_cgroups()
{
    FOREACH_CONTROLLER {
        int start_index = strlen(CGROUP_MOUNT_PATH) + 1 + strlen(controller->name) + 1;
        INFO("Create cgroup: mkdir -p %s", controller->group_path);
        if (mkdir_p(controller->group_path, start_index) < 0) {
            if (errno == EEXIST)
                FATALX("'%s' already exists. Please specify a deeper group_path or clean up the cgroup",
                     controller->group_path);
            else
                FATAL("Couldn't create '%s'. Check permissions.", controller->group_path);
        }
    }
}

static int write_file(const char *group_path, const char *value)
{
   FILE *fp = fopen(group_path, "w");
   if (!fp)
       return -1;

   int rc = fwrite(value, 1, strlen(value), fp);
   fclose(fp);
   return rc;
}

static void update_cgroup_settings()
{
    FOREACH_CONTROLLER {
        for (struct controller_var *var = controller->vars;
             var != NULL;
             var = var->next) {
            char *setting_file;
            checked_asprintf(&setting_file, "%s/%s", controller->group_path, var->key);
            if (write_file(setting_file, var->value) < 0)
                FATAL("Error writing '%s' to '%s'", var->value, setting_file);
            free(setting_file);
        }
    }
}

static void move_pid_to_cgroups(pid_t pid)
{
    FOREACH_CONTROLLER {
        FILE *fp = fopen(controller->procfile, "w");
        if (fp == NULL ||
            fprintf(fp, "%d", pid) < 0)
            FATAL("Can't add pid to %s", controller->procfile);
        fclose(fp);
    }
}

static void destroy_cgroups()
{
    FOREACH_CONTROLLER {
        // Only remove the final directory, since we don't keep track of
        // what we actually create.
        INFO("rmdir %s", controller->group_path);
        if (rmdir(controller->group_path) < 0) {
            INFO("Error removing %s (%s)", controller->group_path, strerror(errno));
            WARN("Error removing %s", controller->group_path);
        }
    }
}

static int procfile_killall(const char *group_path, int sig)
{
    int children_killed = 0;

    FILE *fp = fopen(group_path, "r");
    if (!fp)
        return children_killed;

    int pid;
    while (fscanf(fp, "%d", &pid) == 1) {
        INFO("  kill -%d %d", sig, pid);
        kill(pid, sig);
        children_killed++;
    }
    fclose(fp);
    return children_killed;
}

static int kill_children(int sig)
{
    int children_killed = 0;
    FOREACH_CONTROLLER {
        INFO("killall -%d from %s", sig, controller->procfile);
        children_killed += procfile_killall(controller->procfile, sig);
    }
    return children_killed;
}

#ifdef DEBUG
static void read_proc_cmdline(int pid, char *cmdline)
{
    char *cmdline_filename;

    checked_asprintf(&cmdline_filename, "/proc/%d/cmdline", pid);
    FILE *fp = fopen(cmdline_filename, "r");
    if (fp) {
        size_t len = fread(cmdline, 1, 128, fp);
        if (len > 0)
            cmdline[len] = 0;
        else
            strcpy(cmdline, "<NULL>");
        fclose(fp);
    } else {
        sprintf(cmdline, "Error reading %s", cmdline_filename);
    }

    free(cmdline_filename);
}

static void procfile_dump_children(const char *group_path)
{
    INFO("---Begin child list for %s", group_path);
    FILE *fp = fopen(group_path, "r");
    if (!fp) {
        INFO("Error reading child list!");
        return;
    }

    int pid;
    while (fscanf(fp, "%d", &pid) == 1) {
        char cmdline[129];
        read_proc_cmdline(pid, cmdline);
        INFO("  %d: %s", pid, cmdline);
    }
    fclose(fp);
    INFO("---End child list for %s", group_path);
}

static void dump_all_children_from_cgroups()
{
    FOREACH_CONTROLLER {
        procfile_dump_children(controller->procfile);
    }
}
#endif

static void finish_controller_init()
{
    FOREACH_CONTROLLER {
        checked_asprintf(&controller->group_path, "%s/%s/%s", CGROUP_MOUNT_PATH, controller->name, cgroup_path);
        checked_asprintf(&controller->procfile, "%s/cgroup.procs", controller->group_path);
    }
}

static int wait_for_sigchld(pid_t pid_to_match, int timeout_ms)
{
    struct pollfd fds[1];
    fds[0].fd = signal_pipe[0];
    fds[0].events = POLLIN;

    int end_timeout_us = microsecs() + (1000 * timeout_ms);
    int next_time_to_wait_ms = timeout_ms;
    do {
        INFO("poll - %d ms", next_time_to_wait_ms);
        if (poll(fds, 1, next_time_to_wait_ms) < 0) {
            if (errno == EINTR)
                continue;

            WARN("poll");
            return -1;
        }

        if (fds[0].revents) {
            int signal;
            ssize_t amt = read(signal_pipe[0], &signal, sizeof(signal));
            if (amt < 0) {
                WARN("read signal_pipe");
                return -1;
            }

            INFO("signal_pipe - SIGNAL %d", signal);
            switch (signal) {
            case SIGCHLD: {
                int status;
                pid_t pid = wait(&status);
                if (pid_to_match == pid) {
                    INFO("cleaned up matching pid %d.", pid);
                    return 0;
                }
                INFO("cleaned up pid %d.", pid);
                break;
            }

            case SIGTERM:
            case SIGQUIT:
            case SIGINT:
                return -1;

            default:
                WARNX("unexpected signal: %d", signal);
                return -1;
            }
        }

        next_time_to_wait_ms = (end_timeout_us - microsecs()) / 1000;
    } while (next_time_to_wait_ms > 0);

    INFO("timed out waiting for pid %d", pid_to_match);
    return -1;
}

static void cleanup_all_children()
{
    // In order to cleanup the cgroup, all processes need to exit.
    // The immediate child of muontrap will have either exited
    // at this point, so any other processes are orphaned descendents.
    // I.e., Their parent is now PID 1 and we won't get a SIGCHLD when
    // they die. We only know who they are since they're in the cgroup.

    // Send every child a SIGKILL
    int children_left = kill_children(SIGKILL);
    if (children_left > 0) {
        INFO("Found %d pids and sent them a SIGKILL", children_left);
        // poll to see if the cleanup is done every 1 ms
        int poll_intervals = brutal_kill_wait_ms / 1;
        do {
            usleep(1000);

            // Check for children and send SIGKILLs again. This
            // handles the race where we a new process was spawned
            // when we iterated through the pids the previous time.
            children_left = kill_children(SIGKILL);
            INFO("%d pids are still around", children_left);
            poll_intervals--;
        } while (poll_intervals && children_left);

        if (children_left > 0) {
            WARNX("Failed to kill %d pids!", children_left);
#ifdef DEBUG
            dump_all_children_from_cgroups();
#endif
        }
    }
}

static void kill_child_nicely(pid_t child)
{
    // Start with SIGTERM
    int rc = kill(child, SIGTERM);
    INFO("kill -%d %d -> %d (%s)", SIGTERM, child, rc, rc < 0 ? strerror(errno) : "success");
    if (rc < 0)
        return;

    // Wait a little for the child to exit
    if (wait_for_sigchld(child, brutal_kill_wait_ms) < 0) {
        // Child didn't exit, so SIGKILL it.
        rc = kill(child, SIGKILL);
        INFO("kill -%d %d -> %d (%s)", SIGKILL, child, rc, rc < 0 ? strerror(errno) : "success");
        if (rc < 0)
            return;

        if (wait_for_sigchld(child, brutal_kill_wait_ms) < 0)
            WARNX("SIGKILL didn't work on %d", child);
    }
}

static struct controller_info *add_controller(const char *name)
{
    // If the controller exists, don't add it twice.
    for (struct controller_info *c = controllers; c != NULL; c = c->next) {
        if (strcmp(name, c->name) == 0)
            return c;
    }

    struct controller_info *new_controller = malloc(sizeof(struct controller_info));
    new_controller->name = name;
    new_controller->group_path = NULL;
    new_controller->vars = NULL;
    new_controller->next = controllers;
    controllers = new_controller;

    return new_controller;
}

static void add_controller_setting(struct controller_info *controller, const char *key, const char *value)
{
    struct controller_var *new_var = malloc(sizeof(struct controller_var));
    new_var->key = key;
    new_var->value = value;
    new_var->next = controller->vars;
    controller->vars = new_var;
}

#if defined(__linux__)
static int process_stdio(int from_fd)
{
    ssize_t written;
    if (stdio_bytes_avail <= 0)
        return 0;

retry:
    written = splice(from_fd, NULL, STDOUT_FILENO, NULL, stdio_bytes_avail, SPLICE_F_MOVE);
    if (written < 0) {
        if (errno == EINTR)
            goto retry;

        WARN("failed to splice stdio (%d bytes)", stdio_bytes_avail);
        return -1;
    }
    stdio_bytes_avail -= written;
    return 0;
}
#else
static int process_stdio(int from_fd)
{
    if (stdio_bytes_avail <= 0)
        return 0;

    size_t max_to_read = stdio_bytes_avail > 4096 ? 4096 : stdio_bytes_avail;
    char buff[max_to_read];
    ssize_t got = read(from_fd, buff, max_to_read);

    if (got > 0) {
        for (ssize_t i = 0; i < got;) {
            ssize_t written = write(STDOUT_FILENO, &buff[i], got - i);

            if (written <= 0) {
                if (errno == EINTR)
                    continue;

                WARN("failed to copy stdio");
                return -1;
            }
            stdio_bytes_avail -= written;
            i += written;
        }
    }
    return 0;
}
#endif

static int child_wait_loop(pid_t child_pid, int *still_running)
{
    struct pollfd fds[4];
    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN | POLLHUP; // POLLERR is implicit
    fds[1].fd = signal_pipe[0];
    fds[1].events = POLLIN;
    fds[2].fd = stdout_pipe[0];
    fds[2].events = POLLIN;
    fds[3].fd = stderr_pipe[0];
    fds[3].events = POLLIN;
    int poll_num = 2;

    for (;;) {
        poll_num = 2;
        // Also poll stdout and optionally stderr when capturing output and accepting stdio data
        if (capture_stderr_only && stdio_bytes_avail > 0) {
            // Only polling stderr in stderr-only mode
            // fds[2] will be stderr_pipe since we're not using stdout_pipe
            fds[2].fd = stderr_pipe[0];
            fds[2].events = POLLIN;
            poll_num++;
        } else if (capture_output && stdio_bytes_avail > 0) {
            poll_num++;

            if (capture_stderr)
                poll_num++;
        }

        if (poll(fds, poll_num, -1) < 0) {
            if (errno == EINTR)
                continue;

            WARN("poll");
            return EXIT_FAILURE;
        }

        if (fds[0].revents & POLLHUP) {
            // Erlang signals that it's done by closing stdin. Exit immediately.
            INFO("stdin closed. Exiting...");
            return EXIT_FAILURE;
        }

        if (fds[0].revents & POLLIN) {
            uint8_t acknowledgments[32];
            ssize_t amt = read(STDIN_FILENO, acknowledgments, sizeof(acknowledgments));
            if (amt >= 0) {
                // More than one acknowledgment may have come in, so process them all.
                // NOTE: each ack is 1+its_value
                int total_acks = amt;
                for (ssize_t i = 0; i < amt; i++)
                    total_acks += acknowledgments[i];

                stdio_bytes_avail += total_acks;
                if (stdio_bytes_avail > stdio_bytes_max) {
                    WARNX("Too many acks %d/%d, got %d", (int) stdio_bytes_avail, (int) stdio_bytes_max, total_acks);
                    return EXIT_FAILURE;
                }
            } else if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
                INFO("read STDIN_FILENO error: %s", strerror(errno));
                return EXIT_FAILURE;
            }
        }

        if (poll_num > 2 && fds[2].revents) {
            if (process_stdio(fds[2].fd) < 0)
                return EXIT_FAILURE;
        }

        if (poll_num > 3 && fds[3].revents) {
            if (process_stdio(fds[3].fd) < 0)
                return EXIT_FAILURE;
        }

        if (fds[1].revents) {
            int signal;
            ssize_t amt = read(signal_pipe[0], &signal, sizeof(signal));
            if (amt < 0) {
                WARN("read signal_pipe");
                return EXIT_FAILURE;
            }

            switch (signal) {
            case SIGCHLD: {
                int status;
                pid_t dying_pid = wait(&status);
                if (dying_pid == child_pid) {
                    // Let the caller know that the child isn't running and has been cleaned up
                    *still_running = 0;

                    int exit_status;
                    if (WIFSIGNALED(status)) {
                        // Crash on signal, return the signal in the exit status. See POSIX:
                        // http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02
                        exit_status = 128 + WTERMSIG(status);
                        INFO("child terminated via signal %d. our exit status: %d", status, exit_status);
                    } else if (WIFEXITED(status)) {
                        exit_status = WEXITSTATUS(status);
                        INFO("child exited with exit status: %d", exit_status);
                    } else {
                        INFO("child terminated with unexpected status: %d", status);
                        exit_status = EXIT_FAILURE;
                    }
                    return exit_status;
                } else {
                    INFO("something else caused sigchild: pid=%d, status=%d. our child=%d", dying_pid, status, child_pid);
                }
                break;
            }

            case SIGTERM:
            case SIGQUIT:
            case SIGINT:
                return EXIT_FAILURE;

            default:
                WARNX("unexpected signal: %d", signal);
                return EXIT_FAILURE;
            }
        }
    }
}

int main(int argc, char *argv[])
{
#ifdef DEBUG
    char filename[64];
    sprintf(filename, "muontrap-%d.log", getpid());
    debug_fp = fopen(filename, "w");
    if (!debug_fp)
        debug_fp = stderr;
#endif
    INFO("muontrap argc=%d", argc);
    if (argc == 1) {
        usage();
        exit(EXIT_FAILURE);
    }

    int opt;
    char *argv0 = NULL;
    struct controller_info *current_controller = NULL;
    while ((opt = getopt_long(argc, argv, "a:c:g:hk:s:0:", long_options, NULL)) != -1) {
        switch (opt) {
        case 'a': // --gid
        {
            char *endptr;
            run_as_gid = strtoul(optarg, &endptr, 0);
            if (*endptr != '\0') {
                struct group *group = getgrnam(optarg);
                if (!group)
                    FATALX("Unknown group '%s'", optarg);
                run_as_gid = group->gr_gid;
            }
            if (run_as_gid == 0)
                FATALX("Setting the group to root or gid 0 is not allowed");
            break;
        }

        case 'c':
            current_controller = add_controller(optarg);
            break;

        case 'g':
            if (cgroup_path)
                FATALX("Only one cgroup group_path supported.");
            cgroup_path = optarg;
            break;

        case 'h':
            usage();
            exit(EXIT_SUCCESS);

        case 'k': // --delay-to-sigkill
            brutal_kill_wait_ms = strtoul(optarg, NULL, 0);
            break;

        case 'l': // --stdio-window
            stdio_bytes_max = strtol(optarg, NULL, 0);
            if (stdio_bytes_max < 16)
                stdio_bytes_max = 16;

            stdio_bytes_avail = stdio_bytes_max;
            break;

        case 'o': // --capture-output
            capture_output = 1;
            break;

        case 'e': // --capture-stderr
            capture_stderr = 1;
            break;

        case 'r': // --capture-stderr-only
            capture_stderr_only = 1;
            break;

        case 's':
        {
            if (!current_controller)
                FATALX("Specify a cgroup controller (-c) before setting a variable");

            char *equalsign = strchr(optarg, '=');
            if (!equalsign)
                FATALX("No '=' found when setting a variable: '%s'", optarg);

            // NULL terminate the key. We can do this since we're already modifying
            // the arguments by using getopt.
            *equalsign = '\0';
            add_controller_setting(current_controller, optarg, equalsign + 1);
            break;
        }

        case 'u': // --uid
        {
            char *endptr;
            run_as_uid = strtoul(optarg, &endptr, 0);
            if (*endptr != '\0') {
                struct passwd *passwd = getpwnam(optarg);
                if (!passwd)
                    FATALX("Unknown user '%s'", optarg);
                run_as_uid = passwd->pw_uid;
            }
            if (run_as_uid == 0)
                FATALX("Setting the user to root or uid 0 is not allowed");
            break;
        }

        case '0': // --argv0
            argv0 = optarg;
            break;

        default:
            usage();
            exit(EXIT_FAILURE);
        }
    }

    if (argc == optind)
        FATALX("Specify a program to run");

    if (cgroup_path == NULL && controllers)
        FATALX("Specify a cgroup group_path (-g)");

    if (cgroup_path && !controllers)
        FATALX("Specify a cgroup controller (-c) if you specify a group_path");

    finish_controller_init();

    // Finished processing commandline. Initialize and run child.

    if (pipe(signal_pipe) < 0)
        FATAL("pipe");
    if (fcntl(signal_pipe[0], F_SETFD, FD_CLOEXEC) < 0 ||
        fcntl(signal_pipe[1], F_SETFD, FD_CLOEXEC) < 0)
        WARN("fcntl(FD_CLOEXEC)");

    if (capture_stderr_only) {
        // Only capturing stderr, create stderr pipe
        if (pipe(stderr_pipe) < 0)
            FATAL("pipe");
        if (fcntl(stderr_pipe[0], F_SETFD, FD_CLOEXEC) < 0 ||
            fcntl(stderr_pipe[1], F_SETFD, FD_CLOEXEC) < 0)
            WARN("fcntl(FD_CLOEXEC)");
    } else if (capture_output) {
        if (pipe(stdout_pipe) < 0)
            FATAL("pipe");
        if (fcntl(stdout_pipe[0], F_SETFD, FD_CLOEXEC) < 0 ||
            fcntl(stdout_pipe[1], F_SETFD, FD_CLOEXEC) < 0)
            WARN("fcntl(FD_CLOEXEC)");

        if (capture_stderr) {
            if (pipe(stderr_pipe) < 0)
                FATAL("pipe");
            if (fcntl(stderr_pipe[0], F_SETFD, FD_CLOEXEC) < 0 ||
                fcntl(stderr_pipe[1], F_SETFD, FD_CLOEXEC) < 0)
                WARN("fcntl(FD_CLOEXEC)");
        }
    }

    enable_signal_handlers();

    create_cgroups();

    update_cgroup_settings();

    const char *program_name = argv[optind];
    if (argv0)
        argv[optind] = argv0;
    pid_t pid = fork_exec(program_name, &argv[optind]);

    int still_running = 1;
    int exit_status = child_wait_loop(pid, &still_running);

    if (still_running) {
        // Kill our immediate child if it's still running
        kill_child_nicely(pid);
    }

    // Cleanup all descendents if using cgroups
    cleanup_all_children();

    destroy_cgroups();
    disable_signal_handlers();

    exit(exit_status);
}


================================================
FILE: lib/muontrap/cgroups.ex
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
#
# SPDX-License-Identifier: Apache-2.0

defmodule MuonTrap.Cgroups do
  @moduledoc false

  @cgroup_fs "/sys/fs/cgroup"

  @doc """
  Return true if it looks like the system has cgroups support enabled
  """
  @spec cgroups_enabled?() :: boolean()
  def cgroups_enabled?() do
    case get_controllers() do
      {:ok, []} -> false
      {:ok, _list} -> true
      {:error, _anything} -> false
    end
  end

  @doc """
  Return a list available cgroup controllers
  """
  @spec get_controllers() :: {:ok, [String.t()]} | {:error, :enoent}
  def get_controllers() do
    File.ls("/sys/fs/cgroup")
  end

  @doc """
  Get a cgroup variable (like cgget)
  """
  @spec cgget(String.t(), String.t(), String.t()) :: {:ok, String.t()} | {:error, File.posix()}
  def cgget(controller, cgroup_path, variable_name) do
    path = Path.join([@cgroup_fs, controller, cgroup_path, variable_name])
    File.read(path)
  end

  @doc """
  Set a cgroup variable (like cgset)
  """
  @spec cgset(String.t(), String.t(), String.t(), String.t()) :: :ok | {:error, File.posix()}
  def cgset(controller, cgroup_path, variable_name, value) do
    path = Path.join([@cgroup_fs, controller, cgroup_path, variable_name])
    File.write(path, value)
  end
end


================================================
FILE: lib/muontrap/daemon.ex
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
# SPDX-FileCopyrightText: 2018 Matt Ludwigs
# SPDX-FileCopyrightText: 2021 Aldebaran Alonso
# SPDX-FileCopyrightText: 2023 Eric Rauer
# SPDX-FileCopyrightText: 2023 Jon Carstens
# SPDX-FileCopyrightText: 2024 Ben Youngblood
# SPDX-FileCopyrightText: 2024 Milan Vit
# SPDX-FileCopyrightText: 2025 Fernando Mumbach
#
# SPDX-License-Identifier: Apache-2.0

defmodule MuonTrap.Daemon do
  @moduledoc """
  Wrap an OS process in a GenServer so that it can be supervised.

  For example, in your children list add MuonTrap.Daemon like this:

  ```elixir
  children = [
    {MuonTrap.Daemon, ["my_server", ["--options", "foo"], [cd: "/some_directory"]]}
  ]

  opts = [strategy: :one_for_one, name: MyApplication.Supervisor]
  Supervisor.start_link(children, opts)
  ```

  In the `child_spec` tuple, the second element is a list that corresponds to
  the `MuonTrap.cmd/3` parameters. I.e., The first item in the list is the
  program to run, the second is a list of commandline arguments, and the third
  is a list of options. The same options as `MuonTrap.cmd/3` are available with
  the following additions:

  * `:name` - Name the Daemon GenServer
  * `:logger_fun` - Pass a 1-arity function or `t:mfargs/0` tuple to replace
    the default logging behavior. When set, `:log_output`, `:log_prefix`,
    `:log_transform`,
    and `:logger_metadata` will be ignored.
  * `:log_output` - When set, send output from the command to the Logger.
    Specify the log level (e.g., `:debug`)
  * `:log_prefix` - Prefix each log message with this string (defaults to the
    program's path)
  * `:log_transform` - Pass a function that takes a string and returns a string
    to format output from the command. Defaults to `String.replace_invalid/1`
    on Elixir 1.16+ to avoid crashing the logger on non-UTF8 output.
  * `:logger_metadata` - A keyword list to merge into the process's logger metadata.
    The `:muontrap_cmd` and `:muontrap_args` keys are automatically added and
    cannot be overridden.
  * `:stderr_to_stdout` - When set to `true`, redirect stderr to stdout.
    Defaults to `false`.
  * `:capture_stderr_only` - When set to `true`, capture only stderr and ignore stdout.
    This is useful when you want to capture error messages but not regular output.
    Defaults to `false`.
  * `:exit_status_to_reason` - Optional function to convert the exit status (a
    number) to stop reason for the Daemon GenServer. Use if error exit codes
    carry information or aren't errors.

  If you want to run multiple `MuonTrap.Daemon`s under one supervisor, they'll
  all need unique IDs. Use `Supervisor.child_spec/2` like this:

  ```elixir
  Supervisor.child_spec({MuonTrap.Daemon, ["my_server", []]}, id: :server1)
  ```
  """
  use GenServer

  alias MuonTrap.Cgroups

  require Logger

  defstruct [
    :buffer,
    :command,
    :port,
    :cgroup_path,
    :logger_fun,
    :exit_status_to_reason,
    :output_byte_count
  ]

  @max_data_to_buffer 256

  @spec child_spec(keyword()) :: Supervisor.child_spec()
  def child_spec([command, args]) do
    child_spec([command, args, []])
  end

  def child_spec([command, args, opts]) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [command, args, opts]},
      type: :worker,
      restart: :permanent,
      shutdown: 500
    }
  end

  @doc """
  Start/link a deamon GenServer for the specified command.
  """
  @spec start_link(binary(), [binary()], keyword()) :: GenServer.on_start()
  def start_link(command, args, opts \\ []) do
    {genserver_opts, opts} =
      case Keyword.pop(opts, :name) do
        {nil, _opts} -> {[], opts}
        {name, new_opts} -> {[name: name], new_opts}
      end

    GenServer.start_link(__MODULE__, [command, args, opts], genserver_opts)
  end

  @doc """
  Get the value of the specified cgroup variable.
  """
  @spec cgget(GenServer.server(), binary(), binary()) ::
          {:ok, String.t()} | {:error, File.posix()}
  def cgget(server, controller, variable_name) do
    GenServer.call(server, {:cgget, controller, variable_name})
  end

  @doc """
  Modify a cgroup variable.
  """
  @spec cgset(GenServer.server(), binary(), binary(), binary()) :: :ok | {:error, File.posix()}
  def cgset(server, controller, variable_name, value) do
    GenServer.call(server, {:cgset, controller, variable_name, value})
  end

  @doc """
  Return the OS pid to the muontrap executable.
  """
  @spec os_pid(GenServer.server()) :: non_neg_integer() | :error
  def os_pid(server) do
    GenServer.call(server, :os_pid)
  end

  @doc """
  Return statistics about the daemon

  Statistics:

  * `:output_byte_count` - bytes output by the process being run
  """
  @spec statistics(GenServer.server()) :: %{output_byte_count: non_neg_integer()}
  def statistics(server) do
    GenServer.call(server, :statistics)
  end

  @impl GenServer
  def init([command, args, opts]) do
    options = MuonTrap.Options.validate(:daemon, command, args, opts)
    port_options = MuonTrap.Port.port_options(options) ++ [:stream]

    port = Port.open({:spawn_executable, to_charlist(MuonTrap.muontrap_path())}, port_options)

    # Logger.metadata/0 has a side effect to set the metadata for the current process
    options
    |> Map.get(:logger_metadata, [])
    |> Keyword.merge(muontrap_cmd: command, muontrap_args: Enum.join(args, " "))
    |> Logger.metadata()

    {:ok,
     %__MODULE__{
       buffer: "",
       command: command,
       port: port,
       cgroup_path: Map.get(options, :cgroup_path),
       logger_fun: logger_fun(options, command),
       exit_status_to_reason:
         Map.get(options, :exit_status_to_reason, fn _ -> :error_exit_status end),
       output_byte_count: 0
     }}
  end

  defp logger_fun(%{logger_fun: fun}, _command) when is_function(fun, 1), do: fun
  defp logger_fun(%{logger_fun: {m, f, a}}, _command), do: &apply(m, f, [&1 | a])

  defp logger_fun(options, command) do
    log_output = Map.get(options, :log_output)

    if log_output == nil do
      fn _line -> :ok end
    else
      log_prefix = Map.get(options, :log_prefix, command <> ": ")
      log_transform = Map.get(options, :log_transform, &default_transform/1)

      fn line ->
        Logger.log(log_output, [log_prefix, log_transform.(line)])
      end
    end
  end

  if Version.match?(System.version(), ">= 1.16.0") do
    defp default_transform(line) do
      String.replace_invalid(line)
    end
  else
    defp default_transform(line) do
      if String.valid?(line) do
        line
      else
        "** MuonTrap filtered #{byte_size(line)} non-UTF8 bytes **"
      end
    end
  end

  @impl GenServer
  def handle_call({:cgget, controller, variable_name}, _from, %{cgroup_path: cgroup_path} = state) do
    result = Cgroups.cgget(controller, cgroup_path, variable_name)

    {:reply, result, state}
  end

  def handle_call(
        {:cgset, controller, variable_name, value},
        _from,
        %{cgroup_path: cgroup_path} = state
      ) do
    result = Cgroups.cgset(controller, cgroup_path, variable_name, value)

    {:reply, result, state}
  end

  def handle_call(:os_pid, _from, state) do
    os_pid =
      case Port.info(state.port, :os_pid) do
        {:os_pid, p} -> p
        nil -> :error
      end

    {:reply, os_pid, state}
  end

  def handle_call(:statistics, _from, state) do
    statistics = %{output_byte_count: state.output_byte_count}
    {:reply, statistics, state}
  end

  @impl GenServer
  def handle_info({port, {:data, message}}, %__MODULE__{port: port} = state) do
    bytes_received = byte_size(message)
    state = split_and_log(message, state)

    MuonTrap.Port.report_bytes_handled(state.port, bytes_received)

    {:noreply, %{state | output_byte_count: state.output_byte_count + bytes_received}}
  end

  def handle_info({port, {:exit_status, status}}, %__MODULE__{port: port} = state) do
    reason =
      case status do
        0 ->
          Logger.info("#{state.command}: Process exited successfully")
          :normal

        _failure ->
          Logger.error("#{state.command}: Process exited with status #{status}")
          state.exit_status_to_reason.(status)
      end

    {:stop, reason, state}
  end

  def handle_info(_message, state) do
    {:noreply, state}
  end

  defp split_and_log(data, state) do
    {lines, remainder} = process_data(state.buffer <> data)

    Enum.each(lines, &state.logger_fun.(&1))

    %{state | buffer: remainder}
  end

  @doc false
  @spec process_data(binary()) :: {[String.t()], binary()}
  def process_data(data) do
    data |> String.split("\n") |> process_lines([])
  end

  defp process_lines([leftovers], acc) do
    {Enum.reverse(acc), trim_buffer(leftovers)}
  end

  defp process_lines([line | rest], acc) do
    process_lines(rest, [line | acc])
  end

  defp trim_buffer(data) when byte_size(data) > @max_data_to_buffer,
    do: :binary.part(data, 0, @max_data_to_buffer)

  defp trim_buffer(data), do: data
end


================================================
FILE: lib/muontrap/options.ex
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
# SPDX-FileCopyrightText: 2023 Ben Youngblood
# SPDX-FileCopyrightText: 2023 Eric Rauer
# SPDX-FileCopyrightText: 2023 Jon Carstens
#
# SPDX-License-Identifier: Apache-2.0

defmodule MuonTrap.Options do
  @moduledoc """
  Validate and normalize the options passed to MuonTrap.cmd/3 and MuonTrap.Daemon.start_link/3

  This module is generally not called directly, but it's likely
  the source of exceptions if any options aren't quite right. Call `validate/4` directly to
  debug or check options without invoking a command.
  """

  @typedoc """
  The following fields are always present:

  * `:cmd` - the command to run
  * `:args` - a list of arguments to the command

  The next fields are optional:

  * `:into` - `MuonTrap.cmd/3` only
  * `:cd`
  * `:arg0`
  * `:stderr_to_stdout`
  * `:capture_stderr_only`
  * `:parallelism`
  * `:env`
  * `:name` - `MuonTrap.Daemon`-only
  * `:logger_fun` - `MuonTrap.Daemon`-only
  * `:log_output` - `MuonTrap.Daemon`-only, ignored if logger_fun is set
  * `:log_prefix` - `MuonTrap.Daemon`-only, ignored if logger_fun is set
  * `:log_transform` - `MuonTrap.Daemon`-only, ignored if logger_fun is set
  * `:logger_metadata` - `MuonTrap.Daemon`-only, ignored if logger_fun is set and doesn't call the Elixir Logger
  * `:stdio_window`
  * `:exit_status_to_reason` - `MuonTrap.Daemon`-only
  * `:cgroup_controllers`
  * `:cgroup_path`
  * `:cgroup_base`
  * `:delay_to_sigkill`
  * `:cgroup_sets`
  * `:uid`
  * `:gid`
  * `:timeout` - `MuonTrap.cmd/3` only

  """
  @type t() :: map()

  # See https://hexdocs.pm/logger/Logger.html#module-levels
  # Include `:warn` for older Elixir versions
  @log_levels [:emergency, :alert, :critical, :error, :warning, :warn, :notice, :info, :debug]

  @doc """
  Validate options and normalize them for invoking commands

  Pass in `:cmd` or `:daemon` for the first parameter to allow function-specific
  options.
  """
  @spec validate(:cmd | :daemon, binary(), [binary()], keyword()) :: t()
  def validate(context, cmd, args, opts) when context in [:cmd, :daemon] do
    assert_no_null_byte!(cmd, context)

    if !Enum.all?(args, &is_binary/1) do
      raise ArgumentError, "all arguments for #{operation(context)} must be binaries"
    end

    abs_command = System.find_executable(cmd) || :erlang.error(:enoent, [cmd, args, opts])

    validate_options(context, abs_command, args, opts)
    |> resolve_cgroup_path()
  end

  defp resolve_cgroup_path(%{cgroup_path: _path, cgroup_base: _base}) do
    raise ArgumentError, "cannot specify both a cgroup_path and a cgroup_base"
  end

  defp resolve_cgroup_path(%{cgroup_base: base} = options) do
    # Create a random subfolder for this invocation
    Map.put(options, :cgroup_path, Path.join(base, random_string()))
  end

  defp resolve_cgroup_path(other), do: other

  # Thanks https://github.com/danhper/elixir-temp/blob/master/lib/temp.ex
  defp random_string() do
    Integer.to_string(:rand.uniform(0x100000000), 36) |> String.downcase()
  end

  defp validate_options(context, cmd, args, opts) do
    Enum.reduce(
      opts,
      %{cmd: cmd, args: args, into: ""},
      &validate_option(context, &1, &2)
    )
  end

  # System.cmd/3 options
  defp validate_option(:cmd, {:into, what}, opts), do: Map.put(opts, :into, what)
  defp validate_option(_any, {:cd, bin}, opts) when is_binary(bin), do: Map.put(opts, :cd, bin)

  defp validate_option(_any, {:arg0, bin}, opts) when is_binary(bin),
    do: Map.put(opts, :arg0, bin)

  defp validate_option(_any, {:stderr_to_stdout, bool}, opts) when is_boolean(bool),
    do: Map.put(opts, :stderr_to_stdout, bool)

  defp validate_option(_any, {:capture_stderr_only, bool}, opts) when is_boolean(bool),
    do: Map.put(opts, :capture_stderr_only, bool)

  defp validate_option(_any, {:parallelism, bool}, opts) when is_boolean(bool),
    do: Map.put(opts, :parallelism, bool)

  defp validate_option(_any, {:env, enum}, opts),
    do: Map.put(opts, :env, validate_env(enum))

  # MuonTrap.Daemon options
  defp validate_option(:daemon, {:name, name}, opts),
    do: Map.put(opts, :name, name)

  defp validate_option(:daemon, {:log_output, level}, opts) when level in @log_levels,
    do: Map.put(opts, :log_output, level)

  defp validate_option(:daemon, {:log_prefix, prefix}, opts) when is_binary(prefix),
    do: Map.put(opts, :log_prefix, prefix)

  defp validate_option(:daemon, {:log_transform, log_transform}, opts)
       when is_function(log_transform),
       do: Map.put(opts, :log_transform, log_transform)

  defp validate_option(:daemon, {:logger_metadata, metadata}, opts) when is_list(metadata),
    do: Map.put(opts, :logger_metadata, metadata)

  defp validate_option(:daemon, {:logger_fun, logger}, opts) when is_function(logger, 1),
    do: Map.put(opts, :logger_fun, logger)

  defp validate_option(:daemon, {:logger_fun, {m, f, a}}, opts)
       when is_atom(m) and is_atom(f) and is_list(a),
       do: Map.put(opts, :logger_fun, {m, f, a})

  defp validate_option(:daemon, {:logger_fun, {m, f}}, opts)
       when is_atom(m) and is_atom(f),
       do: Map.put(opts, :logger_fun, {m, f, []})

  defp validate_option(:daemon, {:logger_fun, v}, _opts),
    do:
      raise(
        ArgumentError,
        "invalid option :logger_fun with value #{inspect(v)}, expected a 1-arity function or an mfa tuple"
      )

  defp validate_option(_any, {:stdio_window, count}, opts) when is_integer(count),
    do: Map.put(opts, :stdio_window, count)

  defp validate_option(:daemon, {:exit_status_to_reason, exit_status_to_reason}, opts)
       when is_function(exit_status_to_reason),
       do: Map.put(opts, :exit_status_to_reason, exit_status_to_reason)

  # MuonTrap common options
  defp validate_option(_any, {:cgroup_controllers, controllers}, opts) when is_list(controllers),
    do: Map.put(opts, :cgroup_controllers, controllers)

  defp validate_option(_any, {:cgroup_path, path}, opts) when is_binary(path) do
    Map.put(opts, :cgroup_path, path)
  end

  defp validate_option(_any, {:cgroup_base, path}, opts) when is_binary(path) do
    Map.put(opts, :cgroup_base, path)
  end

  defp validate_option(_any, {:delay_to_sigkill, delay}, opts) when is_integer(delay),
    do: Map.put(opts, :delay_to_sigkill, delay)

  defp validate_option(_any, {:cgroup_sets, sets}, opts) when is_list(sets),
    do: Map.put(opts, :cgroup_sets, sets)

  defp validate_option(_any, {:uid, id}, opts) when is_integer(id) or is_binary(id),
    do: Map.put(opts, :uid, id)

  defp validate_option(_any, {:gid, id}, opts) when is_integer(id) or is_binary(id),
    do: Map.put(opts, :gid, id)

  defp validate_option(:cmd, {:timeout, timeout}, opts) when is_integer(timeout) and timeout > 0,
    do: Map.put(opts, :timeout, timeout)

  defp validate_option(_any, {key, val}, _opts),
    do: raise(ArgumentError, "invalid option #{inspect(key)} with value #{inspect(val)}")

  defp validate_env(enum) do
    Enum.map(enum, fn
      {k, nil} ->
        {String.to_charlist(k), false}

      {k, v} ->
        {String.to_charlist(k), String.to_charlist(v)}

      other ->
        raise ArgumentError, "invalid environment key-value #{inspect(other)}"
    end)
  end

  # Copied from Elixir's system.ex to make MuonTrap.cmd pass System.cmd's tests
  defp assert_no_null_byte!(binary, context) do
    case :binary.match(binary, "\0") do
      {_, _} ->
        raise ArgumentError,
              "cannot execute #{operation(context)} for program with null byte, got: #{inspect(binary)}"

      :nomatch ->
        :ok
    end
  end

  defp operation(:cmd), do: "MuonTrap.cmd/3"
  defp operation(:daemon), do: "MuonTrap.Daemon.start_link/3"
end


================================================
FILE: lib/muontrap/port.ex
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
# SPDX-FileCopyrightText: 2023 Ben Youngblood
# SPDX-FileCopyrightText: 2023 Jon Carstens
#
# SPDX-License-Identifier: Apache-2.0

defmodule MuonTrap.Port do
  @moduledoc false

  @spec muontrap_path() :: String.t()
  def muontrap_path() do
    Application.app_dir(:muontrap, ["priv", "muontrap"])
  end

  @doc """
  Run a command in a similar way to System.cmd/3, but taking MuonTrap options

  This code is mostly copy/pasted from System.cmd/3's implementation so that
  it works similarly.
  """
  @spec cmd(MuonTrap.Options.t()) ::
          {Collectable.t(), exit_status :: non_neg_integer() | :timeout}
  def cmd(options) do
    opts = port_options(options, ["--capture-output"])
    {initial, fun} = Collectable.into(options.into)
    {maybe_timer, timeout_message} = maybe_start_timer(options[:timeout])

    try do
      port = Port.open({:spawn_executable, to_charlist(muontrap_path())}, opts)
      do_cmd(port, initial, fun, timeout_message)
    catch
      kind, reason ->
        fun.(initial, :halt)
        :erlang.raise(kind, reason, __STACKTRACE__)
    else
      {acc, status} -> {fun.(acc, :done), status}
    after
      maybe_stop_timer(maybe_timer, timeout_message)
    end
  end

  defp do_cmd(port, acc, fun, timeout_message) do
    receive do
      {^port, {:data, data}} ->
        report_bytes_handled(port, byte_size(data))
        do_cmd(port, fun.(acc, {:cont, data}), fun, timeout_message)

      {^port, {:exit_status, status}} ->
        {acc, status}

      ^timeout_message ->
        Port.close(port)
        {acc, :timeout}
    end
  end

  @spec port_options(MuonTrap.Options.t(), [String.t()]) :: list()
  def port_options(options, args \\ []) do
    [
      :use_stdio,
      :exit_status,
      :binary,
      :hide,
      {:args, args ++ muontrap_args(options)} | Enum.flat_map(options, &port_option/1)
    ]
  end

  defp muontrap_args(options) do
    Enum.flat_map(options, &muontrap_arg/1) ++ ["--", options.cmd] ++ options.args
  end

  defp muontrap_arg({:cgroup_path, path}), do: ["--group", path]
  defp muontrap_arg({:delay_to_sigkill, delay}), do: ["--delay-to-sigkill", to_string(delay)]
  defp muontrap_arg({:uid, id}), do: ["--uid", to_string(id)]
  defp muontrap_arg({:gid, id}), do: ["--gid", to_string(id)]
  defp muontrap_arg({:arg0, arg0}), do: ["--arg0", arg0]
  defp muontrap_arg({:stdio_window, count}), do: ["--stdio-window", to_string(count)]
  defp muontrap_arg({:stderr_to_stdout, true}), do: ["--capture-stderr"]
  defp muontrap_arg({:capture_stderr_only, true}), do: ["--capture-stderr-only"]

  defp muontrap_arg({log_opt, _}) when log_opt in [:log_output, :logger_fun],
    do: ["--capture-output"]

  defp muontrap_arg({:cgroup_controllers, controllers}) do
    Enum.flat_map(controllers, fn controller -> ["--controller", controller] end)
  end

  defp muontrap_arg({:cgroup_sets, sets}) do
    Enum.flat_map(sets, fn {controller, variable, value} ->
      ["--controller", controller, "--set", "#{variable}=#{value}"]
    end)
  end

  defp muontrap_arg(_other), do: []

  defp port_option({:env, env}), do: [{:env, env}]
  defp port_option({:cd, bin}), do: [{:cd, bin}]
  defp port_option({:arg0, bin}), do: [{:arg0, bin}]
  defp port_option({:parallelism, bool}), do: [{:parallelism, bool}]
  defp port_option(_other), do: []

  @spec report_bytes_handled(port(), pos_integer()) :: :ok
  def report_bytes_handled(port, count) when is_port(port) and is_integer(count) do
    cmd = encode_acks(count)
    _ = Port.command(port, cmd)
    :ok
  rescue
    # A process may attempt to mark the bytes processed after the port has
    # closed but before it received an :exit_status message. In those cases
    # the command will fail with ArgumentError, but should be safe to
    # ignore since we don't need to report anymore
    ArgumentError -> :ok
  end

  # Each acknowledgment is one unsigned byte that's the number of bytes to acknowledge
  # plus 1. E.g., 0 means to acknowledge 1 byte. 255 means to acknowledge 256 bytes.
  @spec encode_acks(pos_integer()) :: iodata()
  def encode_acks(count) when count > 0 do
    full_acks = div(count, 256)
    partial_acks = rem(count, 256)
    encode_acks_helper(full_acks, partial_acks)
  end

  defp encode_acks_helper(0, partial_acks), do: <<partial_acks - 1>>
  defp encode_acks_helper(full_acks, 0), do: :binary.copy(<<255>>, full_acks)

  defp encode_acks_helper(full_acks, partial_acks),
    do: [:binary.copy(<<255>>, full_acks), partial_acks - 1]

  @spec maybe_start_timer(non_neg_integer() | nil) :: {reference() | nil, {:timeout, reference()}}
  defp maybe_start_timer(timeout) when is_integer(timeout) do
    timeout_message = {:timeout, make_ref()}
    timer_ref = Process.send_after(self(), timeout_message, timeout)
    {timer_ref, timeout_message}
  end

  # When not setting a timer, return a fake message. This simplifies pattern
  # matching in cmd/1 and do_cmd/4.
  defp maybe_start_timer(_), do: {nil, {:timeout, make_ref()}}

  @spec maybe_stop_timer(reference() | nil, {:timeout, reference()}) :: :ok
  defp maybe_stop_timer(nil, _), do: :ok

  defp maybe_stop_timer(timer_ref, timeout_message) do
    # Ensure we capture the timeout message in case it arrives around the same
    # time the command completes.
    if Process.cancel_timer(timer_ref) == false do
      receive do
        ^timeout_message -> :ok
      after
        0 -> :ok
      end
    end

    :ok
  end
end


================================================
FILE: lib/muontrap.ex
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
# SPDX-FileCopyrightText: 2023 Ben Youngblood
#
# SPDX-License-Identifier: Apache-2.0

defmodule MuonTrap do
  @moduledoc """
  MuonTrap protects you from lost and out of control OS processes.

  You can use it as a `System.cmd/3` replacement or to pull OS processes into
  an Erlang supervision tree via `MuonTrap.Daemon`. Either way, if the Erlang
  process that runs the command dies, then the OS processes will die as well.

  MuonTrap tries very hard to kill OS processes so that remnants don't hang
  around the system when your Erlang code thinks they should be gone. MuonTrap
  can use the Linux kernel's `cgroup` feature to contain the child process and
  all of its children. From there, you can limit CPU and memory and other
  resources to the process group.

  MuonTrap does not require `cgroups` but keep in mind that OS processes can
  escape. It is, however, still an improvement over `System.cmd/3` which does
  not have a mechanism for dealing it OS processes that do not monitor their
  stdin for when to close.

  For more information, see the documentation for `MuonTrap.cmd/3` and
  `MuonTrap.Daemon`

  ## Configuring cgroups

  On most Linux distributions, use `cgcreate` to create a new cgroup.  You can
  name them almost anything. The command below creates one named `muontrap` for
  the current user. It supports memory and CPU controls.

  ```sh
  sudo cgcreate -a $(whoami) -g memory,cpu:muontrap
  ```

  Nerves systems do not contain `cgcreate` by default. Due to the simpler Linux
  setup, it may be sufficient to run `File.mkdir_p(cgroup_path)` to create a
  cgroup. For example:

  ```elixir
  File.mkdir_p("/sys/fs/cgroup/memory/muontrap")
  ```

  This creates the cgroup path, `muontrap` under the `memory` controller.  If
  you do not have the `"/sys/fs/cgroup"` directory, you will need to mount it
  or update your `erlinit.config` to mount it for you. See a newer official
  system for an example.
  """

  @doc ~S"""
  Executes a command like `System.cmd/3` via the `muontrap` wrapper.

  ## Options

    * `:cgroup_controllers` - run the command under the specified cgroup controllers. Defaults to `[]`.
    * `:cgroup_base` - create a temporary path under the specified cgroup path
    * `:cgroup_path` - explicitly specify a path to use. Use `:cgroup_base`, unless you must control the path.
    * `:cgroup_sets` - set a cgroup controller parameter before running the command
    * `:delay_to_sigkill` - milliseconds before sending a SIGKILL to a child process if it doesn't exit with a SIGTERM (default 500 ms)
    * `:uid` - run the command using the specified uid or username
    * `:gid` - run the command using the specified gid or group
    * `:timeout` - milliseconds to wait for the command to complete. If the
      command does not exit before the timeout, the return value will contain
      the output up to that point and `:timeout` as the exit status. The child
      process will be sent SIGTERM

  The following `System.cmd/3` options are also available:

    * `:into` - injects the result into the given collectable, defaults to `""`
    * `:cd` - the directory to run the command in
    * `:env` - an enumerable of tuples containing environment key-value as binary
    * `:arg0` - sets the command arg0
    * `:stderr_to_stdout` - redirects stderr to stdout when `true`
    * `:capture_stderr_only` - when `true`, captures only stderr and ignores stdout (useful for capturing errors while ignoring normal output)
    * `:parallelism` - when `true`, the VM will schedule port tasks to improve
      parallelism in the system. If set to `false`, the VM will try to perform
      commands immediately, improving latency at the expense of parallelism.
      The default can be set on system startup by passing the "+spp" argument
      to `--erl`.

  ## Examples

  Run a command:

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

  The next examples only run on Linux. To try this out, create new cgroups:

  ```sh
  sudo cgcreate -a $(whoami) -g memory,cpu:muontrap
  ```

  Run a command, but limit memory so severely that it doesn't work (for demo
  purposes, obviously):

  ```elixir
  iex-donttest> MuonTrap.cmd("echo", ["hello"], cgroup_controllers: ["memory"], cgroup_path: "muontrap/test", cgroup_sets: [{"memory", "memory.limit_in_bytes", "8192"}])
  {"", 1}
  ```

  Run a command with a timeout:

  iex> MuonTrap.cmd("/bin/sh", ["-c", "echo start && sleep 10 && echo end"], timeout: 100)
  {"start\n", :timeout}
  """
  @spec cmd(binary(), [binary()], keyword()) ::
          {Collectable.t(), exit_status :: non_neg_integer() | :timeout}
  def cmd(command, args, opts \\ []) when is_binary(command) and is_list(args) do
    options = MuonTrap.Options.validate(:cmd, command, args, opts)

    MuonTrap.Port.cmd(options)
  end

  @doc """
  Return the absolute path to the muontrap executable.

  Call this if you want to invoke the `muontrap` port binary manually.
  """
  defdelegate muontrap_path, to: MuonTrap.Port
end


================================================
FILE: mix.exs
================================================
defmodule MuonTrap.MixProject do
  use Mix.Project

  @version "1.7.0"
  @source_url "https://github.com/fhunleth/muontrap"

  def project do
    [
      app: :muontrap,
      version: @version,
      elixir: "~> 1.11",
      description: "Keep your ports contained",
      source_url: @source_url,
      elixirc_paths: elixirc_paths(Mix.env()),
      docs: docs(),
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      compilers: [:elixir_make | Mix.compilers()],
      make_targets: ["all"],
      make_clean: ["clean"],
      dialyzer: [
        flags: [:missing_return, :extra_return, :unmatched_returns, :error_handling, :underspecs]
      ],
      package: package()
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  def application do
    [extra_applications: [:logger]]
  end

  def cli do
    [preferred_envs: %{docs: :docs, "hex.publish": :docs, "hex.build": :docs}]
  end

  defp deps() do
    [
      {:elixir_make, "~> 0.6", runtime: false},
      {:ex_doc, "~> 0.19", only: :docs, runtime: false},
      {:dialyxir, "~> 1.2", only: :dev, runtime: false},
      {:credo, "~> 1.5", only: :dev, runtime: false}
    ]
  end

  defp docs do
    [
      extras: ["README.md"],
      main: "readme",
      source_ref: "v#{@version}",
      source_url: @source_url
    ]
  end

  defp package() do
    [
      files: [
        "CHANGELOG.md",
        "README.md",
        "lib",
        "c_src/*.[ch]",
        "c_src/Makefile",
        "Makefile",
        "mix.exs",
        "NOTICE",
        "LICENSES/*",
        "REUSE.toml"
      ],
      licenses: ["Apache-2.0"],
      links: %{
        "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md",
        "GitHub" => @source_url,
        "REUSE Compliance" => "https://api.reuse.software/info/github.com/fhunleth/muontrap"
      }
    ]
  end
end


================================================
FILE: test/Makefile
================================================
# Variables to override
#
# CC            C compiler
# CROSSCOMPILE	crosscompiler prefix, if any
# CFLAGS	compiler flags for compiling all C files
# LDFLAGS	linker flags for linking all binaries

LDFLAGS +=
CFLAGS ?= -O2 -Wall -Wextra -Wno-unused-parameter
CFLAGS += -std=c99 -D_GNU_SOURCE

SRC=$(wildcard *.c)
BIN=$(SRC:.c=.test)

.PHONY: all clean

all: $(BIN)

%.test: %.c
	$(CC) $(LDFLAGS) $(CFLAGS) -o $@ $<

clean:
	rm -f *.test


================================================
FILE: test/cgroup_test.exs
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
#
# SPDX-License-Identifier: Apache-2.0

defmodule CgroupTest do
  use MuonTrapTest.Case

  alias MuonTrap.Cgroups

  @tag :cgroup
  test "test environment cgroup support enabled" do
    assert Cgroups.cgroups_enabled?()

    {:ok, controllers} = Cgroups.get_controllers()
    # cpu and memory controllers need to be enabled for the unit tests
    assert "cpu" in controllers
    assert "memory" in controllers
  end

  @tag :cgroup
  test "cgroup gets created and removed on exit" do
    cgroup_path = random_cgroup_path()

    port =
      Port.open(
        {:spawn_executable, MuonTrap.muontrap_path()},
        args: ["-g", cgroup_path, "-c", "cpu", "./test/do_nothing.test"]
      )

    os_pid = os_pid(port)
    assert_os_pid_running(os_pid)
    assert cpu_cgroup_exists(cgroup_path)

    Port.close(port)

    wait_for_close_check()
    assert_os_pid_exited(os_pid)
    assert !cpu_cgroup_exists(cgroup_path)
  end

  @tag :cgroup
  test "cleans up after a forking process" do
    cgroup_path = random_cgroup_path()

    port =
      Port.open(
        {:spawn_executable, MuonTrap.muontrap_path()},
        args: ["-g", cgroup_path, "-c", "cpu", "./test/fork_a_lot.test"]
      )

    os_pid = os_pid(port)
    assert_os_pid_running(os_pid)
    assert cpu_cgroup_exists(cgroup_path)

    Port.close(port)

    wait_for_close_check()
    assert_os_pid_exited(os_pid)
    assert !cpu_cgroup_exists(cgroup_path)
  end

  @tag :cgroup
  test "get and set cgroup variables" do
    cgroup_path = random_cgroup_path()

    port =
      Port.open(
        {:spawn_executable, MuonTrap.muontrap_path()},
        args: ["-g", cgroup_path, "-c", "memory", "./test/do_nothing.test"]
      )

    os_pid = os_pid(port)
    assert_os_pid_running(os_pid)
    assert memory_cgroup_exists(cgroup_path)

    {:ok, memory_str} = Cgroups.cgget("memory", cgroup_path, "memory.limit_in_bytes")
    {memory, _} = Integer.parse(memory_str)
    assert memory > 1000

    # :ok = Cgroups.cgset("memory", cgroup_path, "memory.limit_in_bytes", "900")

    Port.close(port)
  end
end


================================================
FILE: test/chatty.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
// SPDX-FileCopyrightText: 2023 Ben Youngblood
//
// SPDX-License-Identifier: Apache-2.0

#include <stdio.h>

int main(void)
{
  /* Make standard output unbuffered. */
  setvbuf(stdout, (char *)NULL, _IONBF, 0);

  while (1)
    printf("Hello, world!\n");

  return 0;
}


================================================
FILE: test/daemon_test.exs
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
# SPDX-FileCopyrightText: 2018 Matt Ludwigs
# SPDX-FileCopyrightText: 2019 Timmo Verlaan
# SPDX-FileCopyrightText: 2022 Gustavo Brunoro
# SPDX-FileCopyrightText: 2023 Ben Youngblood
# SPDX-FileCopyrightText: 2023 Eric Rauer
# SPDX-FileCopyrightText: 2023 Jon Carstens
# SPDX-FileCopyrightText: 2025 Fernando Mumbach
#
# SPDX-License-Identifier: Apache-2.0

defmodule DaemonTest do
  use MuonTrapTest.Case
  import ExUnit.CaptureLog
  import ExUnit.CaptureIO

  alias MuonTrap.Daemon

  defp daemon_spec(cmd, args) do
    Supervisor.child_spec({Daemon, [cmd, args]}, id: :test_daemon)
  end

  defp daemon_spec(cmd, args, opts) do
    Supervisor.child_spec({Daemon, [cmd, args, opts]}, id: :test_daemon)
  end

  test "stopping the daemon kills the process" do
    {:ok, pid} = start_supervised(daemon_spec(test_path("do_nothing.test"), []))

    os_pid = Daemon.os_pid(pid)
    assert_os_pid_running(os_pid)

    :ok = stop_supervised(:test_daemon)

    wait_for_close_check()
    assert_os_pid_exited(os_pid)
  end

  test "stopping the daemon kill very chatty processes" do
    fun = fn ->
      # Try up to 5 times to avoid false negatives. If the error is present, the
      # test will nearly always fail on the first iteration.
      for _ <- 1..5 do
        {:ok, pid} =
          start_supervised(daemon_spec(test_path("chatty.test"), [], log_output: :debug))

        os_pid = Daemon.os_pid(pid)
        assert_os_pid_running(os_pid)

        child_pid = find_child_pid(os_pid)
        assert is_integer(child_pid)

        :ok = stop_supervised(:test_daemon)

        wait_for_close_check()
        assert_os_pid_exited(os_pid)

        if os_pid_around?(child_pid) do
          System.cmd("kill", ["-9", "#{child_pid}"])
          flunk("muontrap process exited but child process was still running")
        end
      end
    end

    # For this test, it's critical to capture the log output even though we don't
    # use it; not doing so significantly increases the likelihood of false
    # negatives.
    capture_log([level: :info], fun)
  end

  @spec find_child_pid(non_neg_integer()) :: non_neg_integer() | nil
  def find_child_pid(os_pid) do
    {output, _} = System.cmd("ps", ["-eo", "ppid,pid"])

    output
    |> String.split("\n")
    |> Enum.find_value(fn line ->
      parsed_line = line |> String.trim() |> String.split(~r/\s+/)

      with [ppid, pid] <- parsed_line,
           true <- ppid == to_string(os_pid),
           {pid, ""} <- Integer.parse(pid) do
        pid
      else
        _ -> nil
      end
    end)
  end

  test "daemon logs output when told" do
    fun = fn ->
      {:ok, _pid} = start_supervised(daemon_spec("echo", ["hello"], log_output: :error))

      wait_for_close_check()
      Logger.flush()
    end

    assert capture_log(fun) =~ "hello"
  end

  test "daemon logs are passed through log_transform fn" do
    fun = fn ->
      {:ok, _pid} =
        start_supervised(
          daemon_spec(
            "echo",
            ["hello"],
            log_output: :error,
            log_transform: &String.replace(&1, "hello", "goodbye")
          )
        )

      wait_for_close_check()
      Logger.flush()
    end

    assert capture_log(fun) =~ "goodbye"
  end

  test "daemon doesn't log output by default" do
    fun = fn ->
      {:ok, _pid} =
        start_supervised(daemon_spec(test_path("echo_stdio.test"), [], stderr_to_stdout: true))

      wait_for_close_check()

      Logger.flush()
    end

    assert capture_log(fun) == ""
  end

  test "daemon logs output to stderr when told" do
    fun = fn ->
      {:ok, pid} =
        start_supervised(
          daemon_spec(test_path("echo_stderr.test"), [],
            log_output: :error,
            stderr_to_stdout: true
          )
        )

      wait_for_output(pid, 15, 500)
      Logger.flush()
    end

    assert capture_log(fun) =~ "stderr message"
  end

  test "daemon does not log output to stderr when not told" do
    # Need to disable ANSI since new line in log message is important
    Application.put_env(:elixir, :ansi_enabled, false)

    fun = fn ->
      {:ok, pid} =
        start_supervised(
          daemon_spec(test_path("echo_stdio.test"), [],
            log_output: :error,
            stderr_to_stdout: false
          )
        )

      wait_for_output(pid, 12, 500)

      Logger.flush()
    end

    result = capture_log(fun)
    assert result =~ "echo_stdio.test: stdout here\n"
    refute result =~ ".."

    Application.delete_env(:elixir, :ansi_enabled)
  end

  test "daemon logs to a custom prefix" do
    fun = fn ->
      {:ok, _pid} =
        start_supervised(
          daemon_spec("echo", ["hello"], log_output: :error, log_prefix: "echo says: ")
        )

      wait_for_close_check()
      Logger.flush()
    end

    assert capture_log(fun) =~ "echo says: hello"
  end

  test "daemon logs include metadata" do
    fun = fn ->
      {:ok, _pid} =
        start_supervised(
          daemon_spec(
            "echo",
            ["-n", "hello"],
            log_output: :error,
            logger_metadata: [foo: :bar]
          )
        )

      wait_for_close_check()
      Logger.flush()
    end

    logger_opts = [
      metadata: [:foo, :muontrap_cmd, :muontrap_args],
      format: "[$level] $message $metadata\n"
    ]

    log_output = capture_log(logger_opts, fun)
    assert log_output =~ "foo=bar"
    assert log_output =~ "muontrap_cmd=echo"
    assert log_output =~ "muontrap_args=-n hello"
  end

  test "daemon supports custom logger (captured function)" do
    test_process = self()

    logger = fn line ->
      send(test_process, line)
    end

    fun = fn ->
      {:ok, pid} =
        start_supervised(
          daemon_spec(test_path("echo_stdio.test"), [],
            log_output: :error,
            logger_fun: logger,
            stderr_to_stdout: false
          )
        )

      wait_for_output(pid, 12, 500)

      Logger.flush()
    end

    log_output = capture_log(fun)

    refute log_output =~ "stdout here"

    assert_receive "stdout here", 500
    refute_receive _
  end

  test "daemon supports custom logger (mfa)" do
    fun = fn ->
      {:ok, pid} =
        start_supervised(
          daemon_spec(test_path("echo_stdio.test"), [],
            log_output: :error,
            logger_fun: {__MODULE__, :logger_fun_fun},
            stderr_to_stdout: false
          )
        )

      wait_for_output(pid, 12, 500)

      Logger.flush()
    end

    log_output = capture_log(fun)

    assert log_output =~ "stdout here"
    refute log_output =~ "logger_fun"

    stop_supervised(:test_daemon)

    fun = fn ->
      {:ok, pid} =
        start_supervised(
          daemon_spec(test_path("echo_stdio.test"), [],
            log_output: :error,
            logger_fun: {__MODULE__, :logger_fun_fun, ["logger_fun: "]},
            stderr_to_stdout: false
          )
        )

      wait_for_output(pid, 12, 500)

      Logger.flush()
    end

    log_output = capture_log(fun)

    assert log_output =~ "logger_fun: stdout here"
  end

  @spec logger_fun_fun(binary(), binary()) :: :ok
  def logger_fun_fun(line, prefix \\ "") do
    require Logger
    Logger.info([prefix, line])
  end

  defp wait_for_output(_pid, count, time_left) when time_left <= 0 do
    flunk("Didn't get #{count} output bytes from daemon process in time")
  end

  defp wait_for_output(pid, count, time_left) do
    got = Daemon.statistics(pid).output_byte_count

    cond do
      got < count ->
        Process.sleep(100)
        wait_for_output(pid, count, time_left - 100)

      got > count ->
        flunk("Got too much output: #{got}, but expected #{count}")

      true ->
        :ok
    end
  end

  test "can pass environment variables to the daemon" do
    fun = fn ->
      {:ok, _pid} =
        start_supervised(
          daemon_spec(
            "env",
            [],
            log_output: :error,
            stderr_to_stdout: true,
            env: [{"MUONTRAP_TEST_VAR", "HELLO_THERE"}]
          )
        )

      wait_for_close_check()

      Logger.flush()
    end

    assert capture_log(fun) =~ "MUONTRAP_TEST_VAR=HELLO_THERE"
  end

  test "transient daemon restarts on errored exits" do
    # :transient means that successful exits don't restart, but
    # failed exits do.

    tempfile = Path.join("test", "tmp-transient_daemon")
    _ = File.rm(tempfile)

    log =
      capture_log(fn ->
        {:ok, _pid} =
          start_supervised(
            {Daemon, [test_path("succeed_second_time.test"), [tempfile], [log_output: :error]]},
            restart: :transient
          )

        # Give it time to run twice if successful or more than twice if not.
        Process.sleep(500)

        Logger.flush()
      end)

    _ = File.rm(tempfile)

    assert log =~ "Called 0 times"
    assert log =~ "Called 1 times"
    refute log =~ "Called 2 times"
  end

  test "permanent daemon always restarts" do
    tempfile = Path.join("test", "tmp-permanent_deamon")
    _ = File.rm(tempfile)

    log =
      capture_log(fn ->
        {:ok, _pid} =
          start_supervised(
            Supervisor.child_spec(
              {Daemon, [test_path("succeed_second_time.test"), [tempfile], [log_output: :error]]},
              restart: :permanent,
              id: :test_daemon
            )
          )

        # Give it time to restart a few times.
        Process.sleep(500)

        stop_supervised(:test_daemon)

        Logger.flush()
      end)

    _ = File.rm(tempfile)

    assert log =~ "Called 0 times"
    assert log =~ "Called 1 times"
    assert log =~ "Called 2 times"
  end

  test "returns :error_exit_status for stop reason" do
    {:ok, pid} = start_supervised(daemon_spec(test_path("kill_self_with_sigusr1.test"), []))

    ref = Process.monitor(pid)

    os_pid = Daemon.os_pid(pid)

    assert_receive {:DOWN, ^ref, :process, _object, :error_exit_status}
    assert_os_pid_exited(os_pid)

    :ok = stop_supervised(:test_daemon)

    wait_for_close_check()
  end

  test "supports mapping exit status to stop reason" do
    # Some systems may have SIGUSR1 == 10 and others
    # SIGUSR1 == 30. Do a quick lookup for the expected
    # signal mapping to decide which one to expect
    sigusr1 = s2n("USR1", 10)

    {:ok, pid} =
      start_supervised(
        daemon_spec(test_path("kill_self_with_sigusr1.test"), [],
          exit_status_to_reason: fn s ->
            if s == 128 + sigusr1 do
              :error_exit_sigusr1
            else
              {:error_exit_status, s}
            end
          end
        )
      )

    ref = Process.monitor(pid)

    os_pid = Daemon.os_pid(pid)

    assert_receive {:DOWN, ^ref, :process, _object, :error_exit_sigusr1}
    assert_os_pid_exited(os_pid)

    :ok = stop_supervised(:test_daemon)

    wait_for_close_check()
  end

  defp s2n(name, default) do
    with :error <- s2n_kill_l_name(name),
         :error <- s2n_kill_l(name) do
      default
    end
  end

  defp s2n_kill_l_name(name) do
    with {results, 0} <- System.cmd("kill", ["-l", name], stderr_to_stdout: true),
         {number, _} <- Integer.parse(results),
         true <- is_integer(number) do
      number
    else
      _ -> :error
    end
  end

  defp s2n_kill_l(name) do
    # Parse the result from MacOS kill.
    #
    # There are many formats for `kill -l` and this only supports the one on
    # MacOS that we're getting.
    case System.cmd("kill", ["-l"], stderr_to_stdout: true) do
      {signals, 0} ->
        String.split(signals)
        |> Enum.with_index(1)
        |> List.keyfind(name, 0, {:hack, :error})
        |> elem(1)

      _ ->
        :error
    end
  end

  @tag :cgroup
  test "can start daemon with cgroups" do
    {:ok, pid} =
      start_supervised(
        daemon_spec(
          test_path("do_nothing.test"),
          [],
          cgroup_base: "muontrap_test",
          cgroup_controllers: ["memory"]
        )
      )

    os_pid = Daemon.os_pid(pid)
    assert_os_pid_running(os_pid)

    {:ok, memory_str} = Daemon.cgget(pid, "memory", "memory.limit_in_bytes")
    {memory, _} = Integer.parse(memory_str)
    assert memory > 1000
  end

  test "flow control when logging" do
    fun = fn ->
      {:ok, _pid} =
        start_supervised(
          daemon_spec(test_path("print_a_lot.test"), [],
            log_output: :error,
            stdio_window: 101
          )
        )

      wait_for_close_check(200)
      Logger.flush()
    end

    results = capture_log(fun)

    split =
      String.split(results, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")

    # Check that we have a log message for all 1000 lines plus the leftovers at the end.
    assert length(split) == 1001
  end

  test "line splits on newlines" do
    # Daemon.process_data(data) :: {lines, leftovers}
    assert {[], "abcd"} == Daemon.process_data("abcd")
    assert {["abcd"], ""} == Daemon.process_data("abcd\n")
    assert {["abcd", ""], ""} == Daemon.process_data("abcd\n\n")
    assert {[""], "abcd"} == Daemon.process_data("\nabcd")
    assert {["abcd"], ""} == Daemon.process_data("abcd\n")
    assert {["a", "b", "c", "d"], ""} == Daemon.process_data("a\nb\nc\nd\n")
  end

  test "line splits trim max amount to buffer" do
    a255 = :binary.copy("a", 255)
    a256 = :binary.copy("a", 256)
    a265 = :binary.copy("a", 265)

    # Trims amount to buffer when no newlines
    assert {[], a256} == Daemon.process_data(a265)

    # Doesn't trim if not needed
    assert {[], a255} == Daemon.process_data(a255)

    # Doesn't trim full lines if complete
    assert {[a265, "abcd"], "ef"} == Daemon.process_data(a265 <> "\nabcd\nef")

    # Trims leftovers and returns lines
    assert {["abc"], a256} == Daemon.process_data("abc\n" <> a265)
  end

  test "daemon inspects non-utf8 strings" do
    output =
      capture_io(:user, fn ->
        {:ok, pid} =
          start_supervised(daemon_spec(test_path("echo_junk.test"), [], log_output: :error))

        wait_for_output(pid, 15, 500)
        Logger.flush()
      end)

    refute output =~ "FORMATTER ERROR: bad return value"
    refute output =~ "** (RuntimeError) bad return value from Logger formatter Logger.Formatter"

    if Version.match?(System.version(), ">= 1.16.0") do
      assert output =~ "��ti�g!c"
    else
      assert output =~ "** MuonTrap filtered 14 non-UTF8 bytes **"
    end
  end

  test "daemon captures only stderr when capture_stderr_only is set" do
    fun = fn ->
      {:ok, pid} =
        start_supervised(
          daemon_spec(test_path("echo_both.test"), [],
            log_output: :error,
            capture_stderr_only: true
          )
        )

      wait_for_output(pid, 15, 500)
      Logger.flush()
    end

    log = capture_log(fun)
    assert log =~ "stderr message"
    refute log =~ "stdout message"
  end

  test "daemon captures stderr only without log_output (no crash)" do
    {:ok, pid} =
      start_supervised(
        daemon_spec(test_path("echo_stderr.test"), [],
          # no log_output & no logger_fun, so no logging
          # even if capture_stderr_only is true
          capture_stderr_only: true
        )
      )

    os_pid = Daemon.os_pid(pid)
    assert_os_pid_running(os_pid)

    wait_for_output(pid, 15, 500)

    :ok = stop_supervised(:test_daemon)

    wait_for_close_check()
    assert_os_pid_exited(os_pid)
  end

  test "daemon captures both stdout and stderr when both options are used" do
    fun = fn ->
      {:ok, pid} =
        start_supervised(
          daemon_spec(test_path("echo_both.test"), [],
            log_output: :error,
            stderr_to_stdout: true
          )
        )

      wait_for_output(pid, 30, 500)
      Logger.flush()
    end

    log = capture_log(fun)
    assert log =~ "stderr message"
    assert log =~ "stdout message"
  end

  test "daemon captures only stdout when stderr_to_stdout is false" do
    fun = fn ->
      {:ok, pid} =
        start_supervised(
          daemon_spec(test_path("echo_both.test"), [],
            log_output: :error,
            stderr_to_stdout: false
          )
        )

      wait_for_output(pid, 15, 500)
      Logger.flush()
    end

    log = capture_log(fun)
    refute log =~ "stderr message"
    assert log =~ "stdout message"
  end
end


================================================
FILE: test/do_nothing.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
//
// SPDX-License-Identifier: Apache-2.0

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    // Hang out long enough to satisfy the tests
    sleep(120);
    exit(0);
}


================================================
FILE: test/echo_both.c
================================================
// SPDX-FileCopyrightText: 2024 Frank Hunleth
//
// SPDX-License-Identifier: Apache-2.0

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    fprintf(stdout, "stdout message\n");
    fflush(stdout);
    fprintf(stderr, "stderr message\n");
    fflush(stderr);

    // Hang out long enough to satisfy the tests
    sleep(120);
    exit(0);
}


================================================
FILE: test/echo_junk.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
//
// SPDX-License-Identifier: Apache-2.0

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    const char junk[] = {253, 245, 116, 105, 238, 103, 33, 99, 235, 229, 124, 121, 255, 229, 10};

    fwrite(junk, sizeof(junk), 1, stdout);
    fflush(stdout);

    // Hang out long enough to satisfy the tests
    sleep(200);
    exit(0);
}


================================================
FILE: test/echo_stderr.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
// SPDX-FileCopyrightText: 2019 Timmo Verlaan
//
// SPDX-License-Identifier: Apache-2.0

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    fprintf(stderr, "stderr message\n");
    // Hang out long enough to satisfy the tests
    sleep(120);
    exit(0);
}


================================================
FILE: test/echo_stdio.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
//
// SPDX-License-Identifier: Apache-2.0

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    // Messages are different lengths on purpose to help debug.
    // stderr is dots to make it less ugly when it prints to the console, but
    // I'll probably forget and regret it.
    fprintf(stdout, "stdout here\n");
    fprintf(stderr, "....");
    fflush(stdout);

    // Hang out long enough to satisfy the tests
    sleep(200);
    exit(0);
}


================================================
FILE: test/fork_a_lot.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
//
// SPDX-License-Identifier: Apache-2.0

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

// Fork a tree of children and print out the pids

static void do_fork(int left)
{
    if (left == 0)
        return;

    for (int i = 0; i < 2; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            // Child
            do_fork(left - 1);
            // Hang out long enough to satisfy the tests
            sleep(120);
        }
        printf("%d\n", pid);
        fflush(stdout);
    }

}
int main(int argc, char **argv)
{
    // Fork a tree of children.
    // 4 -> this pid + 2 children + 4 grandchildren + 8 great-grandchildren, etc.
    // for a total of 2^(4+1) - 1 processes
    do_fork(4);

    // parent
    sleep(120);
    exit(0);
}


================================================
FILE: test/ignore_sigterm.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
//
// SPDX-License-Identifier: Apache-2.0

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, NULL);

    sleep(120);
    exit(0);
}


================================================
FILE: test/kill_self_with_signal.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
//
// SPDX-License-Identifier: Apache-2.0

#include <err.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    // This test kills itself with a SIGTERM to see if
    // muontrap reports the expected exit code.
    if (kill(getpid(), SIGTERM) < 0)
        err(EXIT_FAILURE, "kill");

    // Give the OS up to a second to deliver the signal.
    sleep(1);

    errx(EXIT_FAILURE, "expected a signal");
}


================================================
FILE: test/kill_self_with_sigusr1.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
// SPDX-FileCopyrightText: 2023 Eric Rauer
//
// SPDX-License-Identifier: Apache-2.0

#include <err.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    // This test kills itself with a SIGUSR1 to see if
    // muontrap reports the expected exit code.
    if (kill(getpid(), SIGUSR1) < 0)
        err(EXIT_FAILURE, "kill");

    // Give the OS up to a second to deliver the signal.
    sleep(1);

    errx(EXIT_FAILURE, "expected a signal");
}



================================================
FILE: test/muontrap_test.exs
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
# SPDX-FileCopyrightText: 2019 Jason Axelson
# SPDX-FileCopyrightText: 2023 Ben Youngblood
# SPDX-FileCopyrightText: 2023 Jon Carstens
#
# SPDX-License-Identifier: Apache-2.0

defmodule MuonTrapTest do
  use MuonTrapTest.Case

  doctest MuonTrap

  defp run_muontrap(args) do
    # Directly invoke the muontrap port to reduce the amount of code
    # to debug if something breaks.
    port =
      Port.open(
        {:spawn_executable, MuonTrap.muontrap_path()},
        args: args
      )

    # The port starts asynchronously. If the test needs to register
    # a signal handler, this is problematic since we can beat it.
    # The right answer is to handshake with our test helper app.
    # Since that's work, sleep briefly.
    Process.sleep(10)
    port
  end

  test "closing the port kills the process" do
    port = run_muontrap(["./test/do_nothing.test"])

    os_pid = os_pid(port)
    assert_os_pid_running(os_pid)

    Port.close(port)

    wait_for_close_check()
    assert_os_pid_exited(os_pid)
  end

  test "closing the port kills a process that ignores sigterm" do
    port = run_muontrap(["--delay-to-sigkill", "1", "test/ignore_sigterm.test"])

    os_pid = os_pid(port)
    assert_os_pid_running(os_pid)
    Port.close(port)

    wait_for_close_check()
    assert_os_pid_exited(os_pid)
  end

  test "delaying the SIGKILL" do
    port = run_muontrap(["--delay-to-sigkill", "250", "test/ignore_sigterm.test"])

    Process.sleep(10)
    os_pid = os_pid(port)
    assert_os_pid_running(os_pid)
    Port.close(port)

    Process.sleep(100)
    # process should be around for 250ms, so it should be around here.
    assert_os_pid_running(os_pid)

    Process.sleep(200)

    # Now it should be gone
    assert_os_pid_exited(os_pid)
  end

  # The following tests are copied from System.cmd to help ensure that
  # MuonTrap.cmd/3 works similarly.
  test "cmd/2 raises for null bytes" do
    assert_raise ArgumentError,
                 ~r"cannot execute MuonTrap.cmd/3 for program with null byte",
                 fn ->
                   MuonTrap.cmd("null\0byte", [])
                 end
  end

  test "cmd/3 raises with non-binary arguments" do
    assert_raise ArgumentError, ~r"all arguments for MuonTrap.cmd/3 must be binaries", fn ->
      MuonTrap.cmd("ls", [~c"/usr"])
    end
  end

  test "cmd/2" do
    assert {"hello\n", 0} = MuonTrap.cmd("echo", ["hello"])
  end

  test "cmd/3 (with options)" do
    opts = [
      into: [],
      cd: File.cwd!(),
      env: %{"foo" => "bar", "baz" => nil},
      arg0: "echo",
      stderr_to_stdout: true,
      parallelism: true
    ]

    assert {["hello\n"], 0} = MuonTrap.cmd("echo", ["hello"], opts)
  end

  test "cmd/3 that prints a lot w/ default buffer" do
    opts = [
      parallelism: true
    ]

    {output, 0} = MuonTrap.cmd(test_path("print_a_lot.test"), [], opts)
    split = String.split(output, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
    assert length(split) == 1001
  end

  test "cmd/3 that prints a lot w/ smaller buffer" do
    opts = [
      parallelism: true,
      stdio_window: 63
    ]

    {output, 0} = MuonTrap.cmd(test_path("print_a_lot.test"), [], opts)
    split = String.split(output, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
    assert length(split) == 1001
  end

  test "cmd/3 with timeout" do
    opts = [timeout: 250]

    fun = fn ->
      MuonTrap.cmd(test_path("chatty.test"), [], opts)
    end

    assert {elapsed, {output, :timeout}} = :timer.tc(fun)
    elapsed = div(elapsed, 1000)

    assert_in_delta opts[:timeout], elapsed, 50
    assert byte_size(output) > 0
  end

  test "cmd/3 with timeout cleans up timers" do
    opts = [timeout: 100]

    {_, status} = MuonTrap.cmd(test_path("kill_self_with_signal.test"), [], opts)

    refute status == :timeout
    refute_receive :timeout
  end

  test "cmd/3 doesn't eat any messages sent to the process" do
    opts = [timeout: 250]

    message = {:timeout, make_ref()}
    Process.send_after(self(), message, 10)
    Process.send_after(self(), :foo, 10)

    assert {_, :timeout} = MuonTrap.cmd(test_path("do_nothing.test"), [], opts)

    assert_receive ^message
    assert_receive :foo
  end

  # Test adapted from https://github.com/elixir-lang/elixir/blob/v1.15.0/lib/elixir/test/elixir/system_test.exs#L121
  @echo "echo-elixir-test"
  @tag :tmp_dir
  test "cmd/2 with absolute and relative paths", config do
    echo = Path.join(config.tmp_dir, @echo)
    File.mkdir_p!(Path.dirname(echo))
    File.ln_s!(System.find_executable("echo"), echo)

    File.cd!(Path.dirname(echo), fn ->
      # There is a bug in OTP where find_executable is finding
      # entries on the current directory. If this is the case,
      # we should avoid the assertion below.
      if !System.find_executable(@echo) do
        assert :enoent = catch_error(MuonTrap.cmd(@echo, ["hello"]))
      end

      assert {"hello\n", 0} =
               MuonTrap.cmd(Path.join(File.cwd!(), @echo), ["hello"], [{:arg0, "echo"}])
    end)
  end

  test "signals return an exit code of 128 + signal" do
    # SIGTERM == 15
    assert {"", 128 + 15} == MuonTrap.cmd(test_path("kill_self_with_signal.test"), [])
  end

  test "README.md version is up to date" do
    app = :muontrap
    app_version = Application.spec(app, :vsn) |> to_string()
    readme = File.read!("README.md")
    [_, readme_version] = Regex.run(~r/{:#{app}, "(.+)"}/, readme)
    assert Version.match?(app_version, readme_version)
  end
end


================================================
FILE: test/options_test.exs
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
# SPDX-FileCopyrightText: 2023 Ben Youngblood
# SPDX-FileCopyrightText: 2023 Jon Carstens
#
# SPDX-License-Identifier: Apache-2.0

defmodule MuonTrap.OptionsTest do
  use MuonTrapTest.Case

  alias MuonTrap.Options

  test "creates random cgroup path when asked" do
    options = Options.validate(:cmd, "echo", [], cgroup_base: "base")
    assert Map.has_key?(options, :cgroup_path)

    ["base", other] = String.split(options.cgroup_path, "/")
    assert byte_size(other) > 4
  end

  test "disallow both cgroup_path and cgroup_base" do
    assert_raise ArgumentError, fn ->
      Options.validate(:cmd, "echo", [], cgroup_base: "base", cgroup_path: "path")
    end
  end

  test "errors match System.cmd ones" do
    for context <- [:cmd, :daemon] do
      # :enoent on missing executable
      assert catch_error(Options.validate(context, "__this_should_not_exist", [], [])) == :enoent

      assert_raise ArgumentError, fn ->
        Options.validate(context, "echo", [~c"not_a_binary"], [])
      end

      assert_raise ArgumentError, fn ->
        Options.validate(context, "why\0would_someone_do_this", [], [])
      end
    end
  end

  test "cmd and daemon-specific options" do
    # :cmd-only
    assert Map.get(Options.validate(:cmd, "echo", [], into: ""), :into) == ""
    assert Map.get(Options.validate(:cmd, "echo", [], timeout: 1000), :timeout) == 1000

    assert_raise ArgumentError, fn ->
      Options.validate(:daemon, "echo", [], into: "")
    end

    # :daemon-only
    assert Map.get(Options.validate(:daemon, "echo", [], name: Something), :name) == Something

    assert_raise ArgumentError, fn ->
      Options.validate(:cmd, "echo", [], name: Something)
    end

    for level <- [:error, :warn, :info, :debug] do
      assert Map.get(Options.validate(:daemon, "echo", [], log_output: level), :log_output) ==
               level

      assert_raise ArgumentError, fn ->
        Options.validate(:cmd, "echo", [], log_output: level)
      end
    end

    assert_raise ArgumentError, fn ->
      Options.validate(:daemon, "echo", [], log_output: :bad_level)
    end

    assert_raise ArgumentError, fn ->
      Options.validate(:daemon, "echo", [], timeout: 1000)
    end

    assert Map.get(
             Options.validate(:daemon, "echo", [], logger_metadata: [foo: :bar]),
             :logger_metadata
           ) == [foo: :bar]

    assert_raise ArgumentError, fn ->
      Options.validate(:cmd, "echo", [], logger_metadata: [foo: :bar])
    end

    assert is_function(
             Map.get(
               Options.validate(:daemon, "echo", [], logger_fun: &Function.identity/1),
               :logger_fun
             )
           )

    assert {Function, :identity, []} =
             Map.get(
               Options.validate(:daemon, "echo", [], logger_fun: {Function, :identity, []}),
               :logger_fun
             )

    assert {Function, :identity, []} =
             Map.get(
               Options.validate(:daemon, "echo", [], logger_fun: {Function, :identity}),
               :logger_fun
             )

    assert_raise ArgumentError, fn ->
      Options.validate(:daemon, "echo", [], logger_fun: &DateTime.add/2)
    end

    assert_raise ArgumentError, fn ->
      Options.validate(:cmd, "echo", [], logger_fun: &Function.identity/1)
    end
  end

  test "common commands basically work" do
    input = [
      cd: "path",
      arg0: "arg0",
      stderr_to_stdout: true,
      capture_stderr_only: true,
      parallelism: true,
      uid: 5,
      gid: "bill",
      delay_to_sigkill: 1,
      stdio_window: 1024,
      env: [{"KEY", "VALUE"}, {"KEY2", "VALUE2"}],
      cgroup_controllers: ["memory", "cpu"],
      cgroup_base: "base",
      cgroup_sets: [{"memory", "memory.limit_in_bytes", "268435456"}]
    ]

    for context <- [:daemon, :cmd] do
      options = Options.validate(context, "echo", [], input)

      assert Map.get(options, :cd) == "path"
      assert Map.get(options, :arg0) == "arg0"
      assert Map.get(options, :stderr_to_stdout) == true
      assert Map.get(options, :capture_stderr_only) == true
      assert Map.get(options, :parallelism) == true
      assert Map.get(options, :uid) == 5
      assert Map.get(options, :gid) == "bill"
      assert Map.get(options, :delay_to_sigkill) == 1
      assert Map.get(options, :stdio_window) == 1024
      assert Map.get(options, :env) == [{~c"KEY", ~c"VALUE"}, {~c"KEY2", ~c"VALUE2"}]
      assert Map.get(options, :cgroup_controllers) == ["memory", "cpu"]
      assert Map.get(options, :cgroup_base) == "base"
      assert Map.get(options, :cgroup_sets) == [{"memory", "memory.limit_in_bytes", "268435456"}]
    end
  end
end


================================================
FILE: test/port_test.exs
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
# SPDX-FileCopyrightText: 2023 Jon Carstens
#
# SPDX-License-Identifier: Apache-2.0

defmodule MuonTrapPortTest do
  use ExUnit.Case

  test "handles basic port call" do
    options = %{cmd: "/bin/echo", args: ["1", "2", "3"]}
    port_options = MuonTrap.Port.port_options(options)

    assert port_options == [
             :use_stdio,
             :exit_status,
             :binary,
             :hide,
             {:args, ["--", "/bin/echo", "1", "2", "3"]}
           ]
  end

  test "handles cgroup controllers" do
    options = %{cmd: "/bin/echo", args: [], cgroup_controllers: ["cpu", "memory"]}
    port_options = MuonTrap.Port.port_options(options)

    assert Keyword.get(port_options, :args) == [
             "--controller",
             "cpu",
             "--controller",
             "memory",
             "--",
             "/bin/echo"
           ]
  end

  test "handles cgroup path" do
    options = %{cmd: "/bin/echo", args: [], cgroup_path: "test/path"}
    port_options = MuonTrap.Port.port_options(options)

    assert Keyword.get(port_options, :args) == [
             "--group",
             "test/path",
             "--",
             "/bin/echo"
           ]
  end

  test "handles cgroup sets" do
    options = %{cmd: "/bin/echo", args: [], cgroup_sets: [{"cpu", "cpu.cfs_period_us", "100000"}]}
    port_options = MuonTrap.Port.port_options(options)

    assert Keyword.get(port_options, :args) == [
             "--controller",
             "cpu",
             "--set",
             "cpu.cfs_period_us=100000",
             "--",
             "/bin/echo"
           ]
  end

  test "handles cgroup sets 2" do
    options = %{
      cmd: "/bin/echo",
      args: [],
      cgroup_sets: [{"cpu", "cpu.cfs_period_us", "100000"}, {"cpu", "cpu.cfs_quota_us", "50000"}]
    }

    port_options = MuonTrap.Port.port_options(options)

    assert Keyword.get(port_options, :args) == [
             "--controller",
             "cpu",
             "--set",
             "cpu.cfs_period_us=100000",
             "--controller",
             "cpu",
             "--set",
             "cpu.cfs_quota_us=50000",
             "--",
             "/bin/echo"
           ]
  end

  test "handles uid" do
    options = %{
      cmd: "/bin/echo",
      args: [],
      uid: 1234
    }

    port_options = MuonTrap.Port.port_options(options)

    assert Keyword.get(port_options, :args) == [
             "--uid",
             "1234",
             "--",
             "/bin/echo"
           ]

    options = %{
      cmd: "/bin/echo",
      args: [],
      uid: "bob"
    }

    port_options = MuonTrap.Port.port_options(options)

    assert Keyword.get(port_options, :args) == [
             "--uid",
             "bob",
             "--",
             "/bin/echo"
           ]
  end

  test "handles gid" do
    options = %{
      cmd: "/bin/echo",
      args: [],
      gid: 14
    }

    port_options = MuonTrap.Port.port_options(options)

    assert Keyword.get(port_options, :args) == [
             "--gid",
             "14",
             "--",
             "/bin/echo"
           ]

    options = %{
      cmd: "/bin/echo",
      args: [],
      gid: "bob"
    }

    port_options = MuonTrap.Port.port_options(options)

    assert Keyword.get(port_options, :args) == [
             "--gid",
             "bob",
             "--",
             "/bin/echo"
           ]
  end

  test "parses delay-to-sigkill" do
    options = %{
      cmd: "/bin/echo",
      args: [],
      delay_to_sigkill: 123
    }

    port_options = MuonTrap.Port.port_options(options)

    assert Keyword.get(port_options, :args) == [
             "--delay-to-sigkill",
             "123",
             "--",
             "/bin/echo"
           ]
  end

  test "parses stdio-window" do
    options = %{
      cmd: "/bin/echo",
      args: [],
      stdio_window: 32
    }

    port_options = MuonTrap.Port.port_options(options)

    assert Keyword.get(port_options, :args) == [
             "--stdio-window",
             "32",
             "--",
             "/bin/echo"
           ]
  end

  defp encode_acks(number) do
    number
    |> MuonTrap.Port.encode_acks()
    |> IO.iodata_to_binary()
  end

  test "ack calculation" do
    assert encode_acks(1) == <<0>>
    assert encode_acks(10) == <<9>>
    assert encode_acks(256) == <<255>>
    assert encode_acks(257) == <<255, 0>>
    assert encode_acks(512) == <<255, 255>>
    assert encode_acks(513) == <<255, 255, 0>>
  end
end


================================================
FILE: test/print_a_lot.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
// SPDX-FileCopyrightText: 2023 Jon Carstens
//
// SPDX-License-Identifier: Apache-2.0

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    int i;
    for (i = 0; i < 1000; i++) {
        printf("%d-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n", i);
    }
    fflush(stdout);

    // Sleep a little since muontrap doesn't wait for all output to be consumed
    sleep(1);
    exit(0);
}


================================================
FILE: test/succeed_second_time.c
================================================
// SPDX-FileCopyrightText: 2018 Frank Hunleth
//
// SPDX-License-Identifier: Apache-2.0

#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static int read_counter(const char *filename)
{
    FILE *fp = fopen(filename, "r");
    if (!fp)
        return 0;

    int counter;
    if (fscanf(fp, "%d", &counter) != 1)
        counter = 0;
    fclose(fp);
    return counter;
}

static void write_counter(const char *filename, int counter)
{
    FILE *fp = fopen(filename, "w");
    fprintf(fp, "%d\n", counter);
    fclose(fp);
}

int main(int argc, char **argv)
{
    if (argc != 2)
        errx(EXIT_FAILURE, "Pass a filename");

    int counter = read_counter(argv[1]);
    printf("Called %d times\n", counter);
    write_counter(argv[1], counter + 1);

    // Only exit successful on the second call.
    if (counter == 1)
        exit(EXIT_SUCCESS);
    else
        exit(EXIT_FAILURE);
}


================================================
FILE: test/support/test_case.ex
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
#
# SPDX-License-Identifier: Apache-2.0

defmodule MuonTrapTest.Case do
  @moduledoc false
  use ExUnit.CaseTemplate

  using do
    quote do
      import unquote(__MODULE__)
      alias MuonTrapTest.Case
    end
  end

  @timeout_before_close_check 20

  @spec test_path(Path.t()) :: Path.t()
  def test_path(cmd) do
    Path.join([File.cwd!(), "test", cmd])
  end

  @spec cpu_cgroup_exists(String.t()) :: boolean
  def cpu_cgroup_exists(path) do
    {rc, 0} = System.cmd("cgget", ["-g", "cpu", path], stderr_to_stdout: true)
    String.match?(rc, ~r/cpu.shares/)
  end

  @spec memory_cgroup_exists(String.t()) :: boolean
  def memory_cgroup_exists(path) do
    {rc, 0} = System.cmd("cgget", ["-g", "memory", path], stderr_to_stdout: true)
    String.match?(rc, ~r/memory.stat/)
  end

  @spec random_cgroup_path :: String.t()
  def random_cgroup_path() do
    "muontrap_test/test#{:rand.uniform(10000)}"
  end

  @spec os_pid_around?(non_neg_integer()) :: boolean
  def os_pid_around?(os_pid) do
    {_, rc} = System.cmd("ps", ["-p", "#{os_pid}"])
    rc == 0
  end

  @spec assert_os_pid_running(non_neg_integer()) :: :ok
  def assert_os_pid_running(os_pid) do
    os_pid_around?(os_pid) || flunk("Expected OS pid #{os_pid} to still be running")
    :ok
  end

  @spec assert_os_pid_exited(non_neg_integer()) :: :ok
  def assert_os_pid_exited(os_pid) do
    os_pid_around?(os_pid) && flunk("Expected OS pid #{os_pid} to be killed")
    :ok
  end

  @spec os_pid(port()) :: non_neg_integer()
  def os_pid(port) do
    {:os_pid, os_pid} = Port.info(port, :os_pid)
    os_pid
  end

  @spec wait_for_close_check(non_neg_integer()) :: :ok
  def wait_for_close_check(timeout \\ @timeout_before_close_check) do
    Process.sleep(timeout)
  end
end


================================================
FILE: test/test_helper.exs
================================================
# SPDX-FileCopyrightText: 2018 Frank Hunleth
# SPDX-FileCopyrightText: 2023 Jon Carstens
#
# SPDX-License-Identifier: Apache-2.0

ExUnit.start()

defmodule MuonTrapTestHelpers do
  @spec check_cgroup_support() :: :ok | no_return()
  def check_cgroup_support() do
    if !System.find_executable("cgget") do
      IO.puts(:stderr, "\nPlease install cgroup-tools so that cgcreate and cgget are available.")
      IO.puts(:stderr, "\nTo skip cgroup tests, run `mix test --exclude cgroup`")
      System.halt(1)
    end

    if !(MuonTrapTest.Case.cpu_cgroup_exists("muontrap_test") and
           MuonTrapTest.Case.memory_cgroup_exists("muontrap_test")) do
      IO.puts(:stderr, "\nPlease create the muontrap_test cgroup")
      IO.puts(:stderr, "sudo cgcreate -a $(whoami) -g memory,cpu:muontrap_test")
      IO.puts(:stderr, "\nTo skip cgroup tests, run `mix test --exclude cgroup`")
      System.halt(1)
    end
  end

  @spec cgroup_excluded?() :: boolean
  def cgroup_excluded?() do
    excludes = ExUnit.configuration()[:exclude]

    :cgroup in excludes or truthy?(Keyword.get(excludes, :cgroup))
  end

  defp truthy?("false"), do: false
  defp truthy?(false), do: false
  defp truthy?(nil), do: false
  defp truthy?(_), do: true
end

if !MuonTrapTestHelpers.cgroup_excluded?() do
  case :os.type() do
    {:unix, :linux} ->
      MuonTrapTestHelpers.check_cgroup_support()

    _ ->
      IO.puts(:stderr, "Not on Linux so skipping tests that use cgroups...")
      ExUnit.configure(exclude: :cgroup)
  end
end
Download .txt
gitextract_g35awbco/

├── .circleci/
│   └── config.yml
├── .credo.exs
├── .formatter.exs
├── .gitignore
├── CHANGELOG.md
├── LICENSES/
│   ├── Apache-2.0.txt
│   ├── CC-BY-4.0.txt
│   └── CC0-1.0.txt
├── Makefile
├── NOTICE
├── README.md
├── REUSE.toml
├── c_src/
│   ├── Makefile
│   └── muontrap.c
├── lib/
│   ├── muontrap/
│   │   ├── cgroups.ex
│   │   ├── daemon.ex
│   │   ├── options.ex
│   │   └── port.ex
│   └── muontrap.ex
├── mix.exs
└── test/
    ├── Makefile
    ├── cgroup_test.exs
    ├── chatty.c
    ├── daemon_test.exs
    ├── do_nothing.c
    ├── echo_both.c
    ├── echo_junk.c
    ├── echo_stderr.c
    ├── echo_stdio.c
    ├── fork_a_lot.c
    ├── ignore_sigterm.c
    ├── kill_self_with_signal.c
    ├── kill_self_with_sigusr1.c
    ├── muontrap_test.exs
    ├── options_test.exs
    ├── port_test.exs
    ├── print_a_lot.c
    ├── succeed_second_time.c
    ├── support/
    │   └── test_case.ex
    └── test_helper.exs
Download .txt
SYMBOL INDEX (162 symbols across 26 files)

FILE: c_src/muontrap.c
  type option (line 47) | struct option
  type controller_var (line 65) | struct controller_var {
  type controller_info (line 71) | struct controller_info {
  type controller_info (line 80) | struct controller_info
  function usage (line 101) | static void usage()
  function microsecs (line 121) | static int microsecs()
  function sigchild_handler (line 128) | void sigchild_handler(int signum)
  function enable_signal_handlers (line 135) | void enable_signal_handlers()
  function disable_signal_handlers (line 148) | void disable_signal_handlers()
  function fork_exec (line 156) | static int fork_exec(const char *path, char *const *argv)
  function mkdir_p (line 234) | static int mkdir_p(const char *abspath, int start_index)
  function create_cgroups (line 260) | static void create_cgroups()
  function write_file (line 275) | static int write_file(const char *group_path, const char *value)
  function update_cgroup_settings (line 286) | static void update_cgroup_settings()
  function move_pid_to_cgroups (line 301) | static void move_pid_to_cgroups(pid_t pid)
  function destroy_cgroups (line 312) | static void destroy_cgroups()
  function procfile_killall (line 325) | static int procfile_killall(const char *group_path, int sig)
  function kill_children (line 343) | static int kill_children(int sig)
  function read_proc_cmdline (line 354) | static void read_proc_cmdline(int pid, char *cmdline)
  function procfile_dump_children (line 374) | static void procfile_dump_children(const char *group_path)
  function dump_all_children_from_cgroups (line 393) | static void dump_all_children_from_cgroups()
  function finish_controller_init (line 401) | static void finish_controller_init()
  function wait_for_sigchld (line 409) | static int wait_for_sigchld(pid_t pid_to_match, int timeout_ms)
  function cleanup_all_children (line 466) | static void cleanup_all_children()
  function kill_child_nicely (line 500) | static void kill_child_nicely(pid_t child)
  type controller_info (line 521) | struct controller_info
  type controller_info (line 524) | struct controller_info
  type controller_info (line 529) | struct controller_info
  type controller_info (line 529) | struct controller_info
  function add_controller_setting (line 539) | static void add_controller_setting(struct controller_info *controller, c...
  function process_stdio (line 549) | static int process_stdio(int from_fd)
  function process_stdio (line 568) | static int process_stdio(int from_fd)
  function child_wait_loop (line 596) | static int child_wait_loop(pid_t child_pid, int *still_running)
  function main (line 719) | int main(int argc, char *argv[])

FILE: lib/muontrap.ex
  class MuonTrap (line 6) | defmodule MuonTrap

FILE: lib/muontrap/cgroups.ex
  class MuonTrap.Cgroups (line 5) | defmodule MuonTrap.Cgroups
    method cgroups_enabled? (line 14) | def cgroups_enabled?() do
    method get_controllers (line 26) | def get_controllers() do
    method cgget (line 34) | def cgget(controller, cgroup_path, variable_name) do
    method cgset (line 43) | def cgset(controller, cgroup_path, variable_name, value) do

FILE: lib/muontrap/daemon.ex
  class MuonTrap.Daemon (line 12) | defmodule MuonTrap.Daemon
    method child_spec (line 83) | def child_spec([command, args]) do
    method child_spec (line 87) | def child_spec([command, args, opts]) do
    method start_link (line 101) | def start_link(command, args, opts \\ []) do
    method cgget (line 116) | def cgget(server, controller, variable_name) do
    method cgset (line 124) | def cgset(server, controller, variable_name, value) do
    method os_pid (line 132) | def os_pid(server) do
    method statistics (line 144) | def statistics(server) do
    method init (line 149) | def init([command, args, opts]) do
    method logger_fun (line 175) | defp logger_fun(%{logger_fun: {m, f, a}}, _command), do: &apply(m, f, ...
    method logger_fun (line 177) | defp logger_fun(options, command) do
    method handle_call (line 207) | def handle_call({:cgget, controller, variable_name}, _from, %{cgroup_p...
    method handle_call (line 213) | def handle_call(
    method handle_call (line 223) | def handle_call(:os_pid, _from, state) do
    method handle_call (line 233) | def handle_call(:statistics, _from, state) do
    method handle_info (line 239) | def handle_info({port, {:data, message}}, %__MODULE__{port: port} = st...
    method handle_info (line 248) | def handle_info({port, {:exit_status, status}}, %__MODULE__{port: port...
    method handle_info (line 263) | def handle_info(_message, state) do
    method split_and_log (line 267) | defp split_and_log(data, state) do
    method process_data (line 277) | def process_data(data) do
    method process_lines (line 281) | defp process_lines([leftovers], acc) do
    method process_lines (line 285) | defp process_lines([line | rest], acc) do
    method trim_buffer (line 292) | defp trim_buffer(data), do: data

FILE: lib/muontrap/options.ex
  class MuonTrap.Options (line 8) | defmodule MuonTrap.Options
    method resolve_cgroup_path (line 76) | defp resolve_cgroup_path(%{cgroup_path: _path, cgroup_base: _base}) do
    method resolve_cgroup_path (line 80) | defp resolve_cgroup_path(%{cgroup_base: base} = options) do
    method resolve_cgroup_path (line 85) | defp resolve_cgroup_path(other), do: other
    method random_string (line 88) | defp random_string() do
    method validate_options (line 92) | defp validate_options(context, cmd, args, opts) do
    method validate_option (line 101) | defp validate_option(:cmd, {:into, what}, opts), do: Map.put(opts, :in...
    method validate_option (line 116) | defp validate_option(_any, {:env, enum}, opts),
    method validate_option (line 120) | defp validate_option(:daemon, {:name, name}, opts),
    method validate_option (line 147) | defp validate_option(:daemon, {:logger_fun, v}, _opts),
    method validate_option (line 188) | defp validate_option(_any, {key, val}, _opts),
    method validate_env (line 191) | defp validate_env(enum) do
    method assert_no_null_byte! (line 205) | defp assert_no_null_byte!(binary, context) do
    method operation (line 216) | defp operation(:cmd), do: "MuonTrap.cmd/3"
    method operation (line 217) | defp operation(:daemon), do: "MuonTrap.Daemon.start_link/3"

FILE: lib/muontrap/port.ex
  class MuonTrap.Port (line 7) | defmodule MuonTrap.Port
    method muontrap_path (line 11) | def muontrap_path() do
    method cmd (line 23) | def cmd(options) do
    method do_cmd (line 42) | defp do_cmd(port, acc, fun, timeout_message) do
    method port_options (line 58) | def port_options(options, args \\ []) do
    method muontrap_args (line 68) | defp muontrap_args(options) do
    method muontrap_arg (line 72) | defp muontrap_arg({:cgroup_path, path}), do: ["--group", path]
    method muontrap_arg (line 73) | defp muontrap_arg({:delay_to_sigkill, delay}), do: ["--delay-to-sigkil...
    method muontrap_arg (line 74) | defp muontrap_arg({:uid, id}), do: ["--uid", to_string(id)]
    method muontrap_arg (line 75) | defp muontrap_arg({:gid, id}), do: ["--gid", to_string(id)]
    method muontrap_arg (line 76) | defp muontrap_arg({:arg0, arg0}), do: ["--arg0", arg0]
    method muontrap_arg (line 77) | defp muontrap_arg({:stdio_window, count}), do: ["--stdio-window", to_s...
    method muontrap_arg (line 78) | defp muontrap_arg({:stderr_to_stdout, true}), do: ["--capture-stderr"]
    method muontrap_arg (line 79) | defp muontrap_arg({:capture_stderr_only, true}), do: ["--capture-stder...
    method muontrap_arg (line 84) | defp muontrap_arg({:cgroup_controllers, controllers}) do
    method muontrap_arg (line 88) | defp muontrap_arg({:cgroup_sets, sets}) do
    method muontrap_arg (line 94) | defp muontrap_arg(_other), do: []
    method port_option (line 96) | defp port_option({:env, env}), do: [{:env, env}]
    method port_option (line 97) | defp port_option({:cd, bin}), do: [{:cd, bin}]
    method port_option (line 98) | defp port_option({:arg0, bin}), do: [{:arg0, bin}]
    method port_option (line 99) | defp port_option({:parallelism, bool}), do: [{:parallelism, bool}]
    method port_option (line 100) | defp port_option(_other), do: []
    method encode_acks_helper (line 124) | defp encode_acks_helper(0, partial_acks), do: <<partial_acks - 1>>
    method encode_acks_helper (line 125) | defp encode_acks_helper(full_acks, 0), do: :binary.copy(<<255>>, full_...
    method encode_acks_helper (line 127) | defp encode_acks_helper(full_acks, partial_acks),
    method maybe_start_timer (line 139) | defp maybe_start_timer(_), do: {nil, {:timeout, make_ref()}}
    method maybe_stop_timer (line 142) | defp maybe_stop_timer(nil, _), do: :ok
    method maybe_stop_timer (line 144) | defp maybe_stop_timer(timer_ref, timeout_message) do

FILE: mix.exs
  class MuonTrap.MixProject (line 1) | defmodule MuonTrap.MixProject
    method project (line 7) | def project do
    method elixirc_paths (line 28) | defp elixirc_paths(:test), do: ["lib", "test/support"]
    method elixirc_paths (line 29) | defp elixirc_paths(_), do: ["lib"]
    method application (line 31) | def application do
    method cli (line 35) | def cli do
    method deps (line 39) | defp deps() do
    method docs (line 48) | defp docs do
    method package (line 57) | defp package() do

FILE: test/cgroup_test.exs
  class CgroupTest (line 5) | defmodule CgroupTest

FILE: test/chatty.c
  function main (line 8) | int main(void)

FILE: test/daemon_test.exs
  class DaemonTest (line 12) | defmodule DaemonTest
    method daemon_spec (line 19) | defp daemon_spec(cmd, args) do
    method daemon_spec (line 23) | defp daemon_spec(cmd, args, opts) do
    method find_child_pid (line 72) | def find_child_pid(os_pid) do
    method logger_fun_fun (line 290) | def logger_fun_fun(line, prefix \\ "") do
    method wait_for_output (line 299) | defp wait_for_output(pid, count, time_left) do
    method s2n (line 440) | defp s2n(name, default) do
    method s2n_kill_l_name (line 447) | defp s2n_kill_l_name(name) do
    method s2n_kill_l (line 457) | defp s2n_kill_l(name) do

FILE: test/do_nothing.c
  function main (line 9) | int main(int argc, char **argv)

FILE: test/echo_both.c
  function main (line 9) | int main()

FILE: test/echo_junk.c
  function main (line 9) | int main()

FILE: test/echo_stderr.c
  function main (line 10) | int main()

FILE: test/echo_stdio.c
  function main (line 9) | int main()

FILE: test/fork_a_lot.c
  function do_fork (line 13) | static void do_fork(int left)
  function main (line 31) | int main(int argc, char **argv)

FILE: test/ignore_sigterm.c
  function main (line 10) | int main(int argc, char **argv)

FILE: test/kill_self_with_signal.c
  function main (line 10) | int main(int argc, char **argv)

FILE: test/kill_self_with_sigusr1.c
  function main (line 11) | int main(int argc, char **argv)

FILE: test/muontrap_test.exs
  class MuonTrapTest (line 8) | defmodule MuonTrapTest
    method run_muontrap (line 13) | defp run_muontrap(args) do

FILE: test/options_test.exs
  class MuonTrap.OptionsTest (line 7) | defmodule MuonTrap.OptionsTest

FILE: test/port_test.exs
  class MuonTrapPortTest (line 6) | defmodule MuonTrapPortTest
    method encode_acks (line 183) | defp encode_acks(number) do

FILE: test/print_a_lot.c
  function main (line 10) | int main()

FILE: test/succeed_second_time.c
  function read_counter (line 10) | static int read_counter(const char *filename)
  function write_counter (line 23) | static void write_counter(const char *filename, int counter)
  function main (line 30) | int main(int argc, char **argv)

FILE: test/support/test_case.ex
  class MuonTrapTest.Case (line 5) | defmodule MuonTrapTest.Case
    method test_path (line 19) | def test_path(cmd) do
    method cpu_cgroup_exists (line 24) | def cpu_cgroup_exists(path) do
    method memory_cgroup_exists (line 30) | def memory_cgroup_exists(path) do
    method random_cgroup_path (line 36) | def random_cgroup_path() do
    method os_pid_around? (line 41) | def os_pid_around?(os_pid) do
    method assert_os_pid_running (line 47) | def assert_os_pid_running(os_pid) do
    method assert_os_pid_exited (line 53) | def assert_os_pid_exited(os_pid) do
    method os_pid (line 59) | def os_pid(port) do
    method wait_for_close_check (line 65) | def wait_for_close_check(timeout \\ @timeout_before_close_check) do

FILE: test/test_helper.exs
  class MuonTrapTestHelpers (line 8) | defmodule MuonTrapTestHelpers
    method check_cgroup_support (line 10) | def check_cgroup_support() do
    method cgroup_excluded? (line 27) | def cgroup_excluded?() do
    method truthy? (line 33) | defp truthy?("false"), do: false
    method truthy? (line 34) | defp truthy?(false), do: false
    method truthy? (line 35) | defp truthy?(nil), do: false
    method truthy? (line 36) | defp truthy?(_), do: true
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (169K chars).
[
  {
    "path": ".circleci/config.yml",
    "chars": 1899,
    "preview": "version: 2.1\n\nlatest: &latest\n  pattern: \"^1.19.*-erlang-28.*$\"\n\ntags: &tags\n  [\n    1.19.4-erlang-28.2-alpine-3.22.2,\n "
  },
  {
    "path": ".credo.exs",
    "chars": 476,
    "preview": "# .credo.exs\n%{\n  configs: [\n    %{\n      name: \"default\",\n      strict: true,\n      checks: [\n        {Credo.Check.Refa"
  },
  {
    "path": ".formatter.exs",
    "chars": 87,
    "preview": "# Used by \"mix format\"\n[\n  inputs: [\"*.{ex,exs}\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]\n"
  },
  {
    "path": ".gitignore",
    "chars": 617,
    "preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 7092,
    "preview": "# Changelog\n\n## v1.7.0\n\n* New feature\n  * Add `:capture_stderr_only` option to capture only stderr while ignoring stdout"
  },
  {
    "path": "LICENSES/Apache-2.0.txt",
    "chars": 10280,
    "preview": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AN"
  },
  {
    "path": "LICENSES/CC-BY-4.0.txt",
    "chars": 16983,
    "preview": "Creative Commons Attribution 4.0 International\n\n Creative Commons Corporation (“Creative Commons”) is not a law firm and"
  },
  {
    "path": "LICENSES/CC0-1.0.txt",
    "chars": 7048,
    "preview": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n"
  },
  {
    "path": "Makefile",
    "chars": 247,
    "preview": "calling_from_make:\n\tmix compile\n\nall:\n\t$(MAKE) -C c_src all\n\tif [ -f test/Makefile ]; then $(MAKE) -C test; fi\n\nclean:\n\t"
  },
  {
    "path": "NOTICE",
    "chars": 403,
    "preview": "Muontrap is open-source software licensed under the Apache License, Version\n2.0.\n\nCopyright holders include Frank Hunlet"
  },
  {
    "path": "README.md",
    "chars": 12068,
    "preview": "# MuonTrap\n\n[![Hex version](https://img.shields.io/hexpm/v/muontrap.svg \"Hex version\")](https://hex.pm/packages/muontrap"
  },
  {
    "path": "REUSE.toml",
    "chars": 501,
    "preview": "version = 1\n\n[[annotations]]\npath = [\n \".circleci/config.yml\",\n \".credo.exs\",\n \".formatter.exs\",\n \".github/dependabot.ym"
  },
  {
    "path": "c_src/Makefile",
    "chars": 1177,
    "preview": "# Makefile for building the muontrap port process\n#\n# Makefile targets:\n#\n# all/install   build and install\n# clean     "
  },
  {
    "path": "c_src/muontrap.c",
    "chars": 28327,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n// SPDX-FileCopyrightText: 2023 Jon Carstens\n// SPDX-FileCopyrightText: 20"
  },
  {
    "path": "lib/muontrap/cgroups.ex",
    "chars": 1279,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule MuonTrap.Cgroups do\n  @m"
  },
  {
    "path": "lib/muontrap/daemon.ex",
    "chars": 8977,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2018 Matt Ludwigs\n# SPDX-FileCopyrightText: 2021 "
  },
  {
    "path": "lib/muontrap/options.ex",
    "chars": 7680,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Ben Youngblood\n# SPDX-FileCopyrightText: 202"
  },
  {
    "path": "lib/muontrap/port.ex",
    "chars": 5464,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Ben Youngblood\n# SPDX-FileCopyrightText: 202"
  },
  {
    "path": "lib/muontrap.ex",
    "chars": 5066,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Ben Youngblood\n#\n# SPDX-License-Identifier: "
  },
  {
    "path": "mix.exs",
    "chars": 1881,
    "preview": "defmodule MuonTrap.MixProject do\n  use Mix.Project\n\n  @version \"1.7.0\"\n  @source_url \"https://github.com/fhunleth/muontr"
  },
  {
    "path": "test/Makefile",
    "chars": 435,
    "preview": "# Variables to override\n#\n# CC            C compiler\n# CROSSCOMPILE\tcrosscompiler prefix, if any\n# CFLAGS\tcompiler flags"
  },
  {
    "path": "test/cgroup_test.exs",
    "chars": 2110,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule CgroupTest do\n  use Muon"
  },
  {
    "path": "test/chatty.c",
    "chars": 317,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n// SPDX-FileCopyrightText: 2023 Ben Youngblood\n//\n// SPDX-License-Identifi"
  },
  {
    "path": "test/daemon_test.exs",
    "chars": 16315,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2018 Matt Ludwigs\n# SPDX-FileCopyrightText: 2019 "
  },
  {
    "path": "test/do_nothing.c",
    "chars": 263,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <stdio.h>\n#include <st"
  },
  {
    "path": "test/echo_both.c",
    "chars": 365,
    "preview": "// SPDX-FileCopyrightText: 2024 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <stdio.h>\n#include <st"
  },
  {
    "path": "test/echo_junk.c",
    "chars": 405,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <stdio.h>\n#include <st"
  },
  {
    "path": "test/echo_stderr.c",
    "chars": 329,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n// SPDX-FileCopyrightText: 2019 Timmo Verlaan\n//\n// SPDX-License-Identifie"
  },
  {
    "path": "test/echo_stdio.c",
    "chars": 515,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <stdio.h>\n#include <st"
  },
  {
    "path": "test/fork_a_lot.c",
    "chars": 856,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <signal.h>\n#include <s"
  },
  {
    "path": "test/ignore_sigterm.c",
    "chars": 350,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <signal.h>\n#include <s"
  },
  {
    "path": "test/kill_self_with_signal.c",
    "chars": 496,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <err.h>\n#include <sign"
  },
  {
    "path": "test/kill_self_with_sigusr1.c",
    "chars": 540,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n// SPDX-FileCopyrightText: 2023 Eric Rauer\n//\n// SPDX-License-Identifier: "
  },
  {
    "path": "test/muontrap_test.exs",
    "chars": 5551,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2019 Jason Axelson\n# SPDX-FileCopyrightText: 2023"
  },
  {
    "path": "test/options_test.exs",
    "chars": 4706,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Ben Youngblood\n# SPDX-FileCopyrightText: 202"
  },
  {
    "path": "test/port_test.exs",
    "chars": 4523,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Jon Carstens\n#\n# SPDX-License-Identifier: Ap"
  },
  {
    "path": "test/print_a_lot.c",
    "chars": 477,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n// SPDX-FileCopyrightText: 2023 Jon Carstens\n//\n// SPDX-License-Identifier"
  },
  {
    "path": "test/succeed_second_time.c",
    "chars": 917,
    "preview": "// SPDX-FileCopyrightText: 2018 Frank Hunleth\n//\n// SPDX-License-Identifier: Apache-2.0\n\n#include <err.h>\n#include <stdi"
  },
  {
    "path": "test/support/test_case.ex",
    "chars": 1792,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n#\n# SPDX-License-Identifier: Apache-2.0\n\ndefmodule MuonTrapTest.Case do\n  @"
  },
  {
    "path": "test/test_helper.exs",
    "chars": 1517,
    "preview": "# SPDX-FileCopyrightText: 2018 Frank Hunleth\n# SPDX-FileCopyrightText: 2023 Jon Carstens\n#\n# SPDX-License-Identifier: Ap"
  }
]

About this extraction

This page contains the full source code of the fhunleth/muontrap GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (156.6 KB), approximately 41.9k tokens, and a symbol index with 162 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!